From 1047585a4537f7759dc5bbf7f280a4f2e1f26631 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:19:34 +0800 Subject: [PATCH 0001/1461] Let the loader register menu screens, vanilla's method is private on NeoForge --- .../jnhprojects/mcheli/client/ClientHooks.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java index 7fd3b542..60f814a3 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -38,6 +38,8 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; @@ -50,6 +52,8 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; @@ -117,11 +121,17 @@ public static void registerBlockEntityRenderers(BlockEntityRenderers renderers) renderers.register(ModBlockEntities.DRAFTING_TABLE.get(), DraftingTableRenderer::new); } + /** How a loader registers the screen of a menu: vanilla's own method is not public on all of them. */ + public interface MenuScreenRegistrar { + > void register(MenuType type, + MenuScreens.ScreenConstructor screen); + } + /** On the main thread during client setup, once our menu types are registered. */ - public static void registerMenuScreens() { - MenuScreens.register(ModMenus.VEHICLE.get(), VehicleScreen::new); - MenuScreens.register(ModMenus.UAV_STATION.get(), UavStationScreen::new); - MenuScreens.register(ModMenus.DRAFTING_TABLE.get(), DraftingTableScreen::new); + public static void registerMenuScreens(MenuScreenRegistrar screens) { + screens.register(ModMenus.VEHICLE.get(), VehicleScreen::new); + screens.register(ModMenus.UAV_STATION.get(), UavStationScreen::new); + screens.register(ModMenus.DRAFTING_TABLE.get(), DraftingTableScreen::new); } public static List keyMappings() { From 4128fbd3b9aa7857eb7a5da6149d3d38e30d2add Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:19:34 +0800 Subject: [PATCH 0002/1461] Pass vanilla's menu screen registration to the shared hook on Forge --- .../main/java/com/jnhprojects/mcheli/forge/ForgeClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java index 20e31893..41e3115b 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -6,6 +6,7 @@ import com.jnhprojects.mcheli.client.particle.SmokeParticle; import com.jnhprojects.mcheli.registry.ModParticles; import net.minecraft.client.Camera; +import net.minecraft.client.gui.screens.MenuScreens; import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; import net.minecraftforge.client.event.ClientPlayerNetworkEvent; import net.minecraftforge.client.event.EntityRenderersEvent; @@ -28,7 +29,7 @@ private ForgeClient() { static void init(BusGroup modBus) { ClientHooks.init(); - FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(ClientHooks::registerMenuScreens)); + FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); RegisterParticleProvidersEvent.BUS.addListener(event -> { From d0ce86c1c6e798df14bcb7df8b3673c2a5771ee5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:19:35 +0800 Subject: [PATCH 0003/1461] Register screens, block entity renderers, smoke and the zoom hook on NeoForge 26.3 --- .../jnhprojects/mcheli/neoforge/NeoForgeClient.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java index 1cd86980..e8b91cc1 100644 --- a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java +++ b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -2,6 +2,7 @@ import com.jnhprojects.mcheli.client.ClientHooks; import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; import com.jnhprojects.mcheli.registry.ModParticles; import net.neoforged.bus.api.IEventBus; import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; @@ -11,9 +12,11 @@ import net.neoforged.neoforge.client.event.InputEvent; import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent; import net.neoforged.neoforge.client.event.RenderHandEvent; import net.neoforged.neoforge.client.event.RenderLivingEvent; +import net.neoforged.neoforge.client.event.ViewportEvent; import net.neoforged.neoforge.client.gui.VanillaGuiLayers; import net.neoforged.neoforge.common.NeoForge; @@ -30,14 +33,20 @@ static void init(IEventBus modBus) { ClientHooks.init(); modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); - modBus.addListener(RegisterParticleProvidersEvent.class, - event -> event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + modBus.addListener(RegisterMenuScreensEvent.class, event -> ClientHooks.registerMenuScreens(event::register)); + modBus.addListener(RegisterParticleProvidersEvent.class, event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); modBus.addListener(RegisterKeyMappingsEvent.class, event -> ClientHooks.keyMappings().forEach(event::register)); modBus.addListener(AddClientReloadListenersEvent.class, event -> ClientHooks.reloadListeners().forEach(event::addListener)); modBus.addListener(RegisterGuiLayersEvent.class, event -> event.registerAbove(VanillaGuiLayers.SLEEP_OVERLAY, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post.class, event -> ClientHooks.clientTickEnd()); + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeFov.class, event -> event.setFOV(ClientHooks.fov(event.getFOV()))); NeoForge.EVENT_BUS.addListener(RenderHandEvent.class, event -> event.setCanceled(ClientHooks.hidesHand())); NeoForge.EVENT_BUS.addListener(RenderLivingEvent.Pre.class, event -> ClientHooks.beforeRenderLiving(event.getRenderState(), event.getPoseStack(), event.getPartialTick())); From d14ee0d5084ad9dce073839696f9632da2364a40 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:19:35 +0800 Subject: [PATCH 0004/1461] Run common setup on NeoForge 26.3 --- .../java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java index 734b0ed6..b5bd9c45 100644 --- a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java +++ b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java @@ -12,6 +12,7 @@ import net.minecraft.server.level.ServerPlayer; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.AddPackFindersEvent; import net.neoforged.neoforge.event.RegisterCommandsEvent; @@ -40,6 +41,7 @@ public McheliNeoForge(IEventBus modBus) { helper -> helper.register(VehicleIngredient.ID, NeoForgeVehicleIngredient.TYPE)); }); modBus.addListener(RegisterPayloadHandlersEvent.class, McheliNeoForge::registerPackets); + modBus.addListener(FMLCommonSetupEvent.class, event -> event.enqueueWork(CommonHooks::commonSetup)); modBus.addListener(AddPackFindersEvent.class, event -> event.addRepositorySource(CommonHooks.packSource())); NeoForge.EVENT_BUS.addListener(ServerAboutToStartEvent.class, event -> CommonHooks.serverAboutToStart(event.getServer())); From 29fd4e8ad13543eeeaee03bba70bab7fc7503600 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:19:36 +0800 Subject: [PATCH 0005/1461] Run common setup on Fabric 26.3 --- .../main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java | 1 + 1 file changed, 1 insertion(+) diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java index e0f1f298..d7503501 100644 --- a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java @@ -34,6 +34,7 @@ public void onInitialize() { } CustomIngredientSerializer.register(FabricVehicleIngredient.SERIALIZER); registerPackets(); + CommonHooks.commonSetup(); ServerLifecycleEvents.SERVER_STARTING.register(CommonHooks::serverAboutToStart); ServerLifecycleEvents.SERVER_STOPPED.register(server -> CommonHooks.serverStopped()); From 42507bdcdf6a0847c73a376070d3eaa51aba2f61 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:19:36 +0800 Subject: [PATCH 0006/1461] Register screens, block entity renderers and smoke on Fabric 26.3 --- .../com/jnhprojects/mcheli/fabric/McheliFabricClient.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java index 76ff11ce..dced45fe 100644 --- a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java @@ -2,6 +2,7 @@ import com.jnhprojects.mcheli.client.ClientHooks; import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; import com.jnhprojects.mcheli.network.ModNetwork; import com.jnhprojects.mcheli.registry.ModParticles; import net.fabricmc.api.ClientModInitializer; @@ -14,6 +15,8 @@ import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements; import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.packs.PackType; @@ -29,7 +32,10 @@ public final class McheliFabricClient implements ClientModInitializer { public void onInitializeClient() { ClientHooks.init(); ClientHooks.registerEntityRenderers(EntityRendererRegistry::register); + ClientHooks.registerBlockEntityRenderers(BlockEntityRenderers::register); + ClientHooks.registerMenuScreens(MenuScreens::register); ParticleProviderRegistry.getInstance().register(ModParticles.GLOW.get(), GlowParticle.Provider::new); + ParticleProviderRegistry.getInstance().register(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); ClientHooks.keyMappings().forEach(KeyMappingHelper::registerKeyMapping); ClientHooks.reloadListeners().forEach(ResourceLoader.get(PackType.CLIENT_RESOURCES)::registerReloadListener); HudElementRegistry.attachElementAfter(VanillaHudElements.SLEEP, ClientHooks.HUD_LAYER, ClientHooks::drawHud); From 7fc7955a02fe0cdf459eef9386489a17bd8e5e78 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:19:37 +0800 Subject: [PATCH 0007/1461] Apply the sight zoom on Fabric 26.3 from a camera mixin --- .../jnhprojects/mcheli/fabric/mixin/CameraMixin.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java index 32f7075b..2805e7d0 100644 --- a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli.fabric.mixin; +import com.jnhprojects.mcheli.client.ClientHooks; import net.minecraft.client.Camera; import org.joml.Quaternionf; import org.joml.Vector3f; @@ -7,6 +8,9 @@ import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; /** * Vanilla's camera has no roll. This is {@code Camera#setRotation} with the roll Forge patches in, @@ -54,4 +58,10 @@ abstract class CameraMixin implements RollableCamera { LEFT.rotate(this.rotation, this.left); this.matrixPropertiesDirty |= 3; } + + /** Zoom through a sight, where Forge fires its field of view event. */ + @Inject(method = "calculateFov", at = @At("RETURN"), cancellable = true) + private void mcheli$calculateFov(float partialTicks, CallbackInfoReturnable cir) { + cir.setReturnValue(ClientHooks.fov(cir.getReturnValueF())); + } } From 98f3a7f56992bfbfbab8b713d9e5ed8fb916a53e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:38:44 +0800 Subject: [PATCH 0008/1461] Move RollableCamera out of the mixin package on Fabric 26.3, it crashed every client at startup --- .../jnhprojects/mcheli/fabric/{mixin => }/RollableCamera.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/{mixin => }/RollableCamera.java (53%) diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java similarity index 53% rename from fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java rename to fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java index f31feaeb..ff30f0ff 100644 --- a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java @@ -1,6 +1,6 @@ -package com.jnhprojects.mcheli.fabric.mixin; +package com.jnhprojects.mcheli.fabric; -/** Implemented on {@code Camera} by {@link CameraMixin}. */ +/** Implemented on {@code Camera} by {@code CameraMixin}. Outside the mixin package, which Mixin refuses to load classes from. */ public interface RollableCamera { /** Like vanilla's {@code setRotation}, with a roll in degrees as Forge applies it. */ void mcheli$setRotation(float yRot, float xRot, float roll); From 1f3c92b1a8200c0d75a3a329c8456e52ad5508ce Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:38:44 +0800 Subject: [PATCH 0009/1461] Import RollableCamera from its new package in CameraMixin on Fabric 26.3 --- .../java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java | 1 + 1 file changed, 1 insertion(+) diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java index 2805e7d0..4c6bfafe 100644 --- a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -1,6 +1,7 @@ package com.jnhprojects.mcheli.fabric.mixin; import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; import net.minecraft.client.Camera; import org.joml.Quaternionf; import org.joml.Vector3f; From 2dfd7fe03c22a30a8bab01200e170a09fb502490 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:38:45 +0800 Subject: [PATCH 0010/1461] Import RollableCamera from its new package in GameRendererMixin on Fabric 26.3 --- .../com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java | 1 + 1 file changed, 1 insertion(+) diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java index 7f67f18d..ab97facf 100644 --- a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -1,6 +1,7 @@ package com.jnhprojects.mcheli.fabric.mixin; import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; import net.minecraft.client.Camera; import net.minecraft.client.DeltaTracker; import net.minecraft.client.renderer.GameRenderer; From 2e59e6909ea163e93ac2d7f02ea0e4271c2e269a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:38:45 +0800 Subject: [PATCH 0011/1461] Move RollableCamera out of the mixin package on Fabric 1.21.1, it crashed every client at startup --- .../jnhprojects/mcheli/fabric/{mixin => }/RollableCamera.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/{mixin => }/RollableCamera.java (53%) diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java similarity index 53% rename from fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java rename to fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java index f31feaeb..ff30f0ff 100644 --- a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java @@ -1,6 +1,6 @@ -package com.jnhprojects.mcheli.fabric.mixin; +package com.jnhprojects.mcheli.fabric; -/** Implemented on {@code Camera} by {@link CameraMixin}. */ +/** Implemented on {@code Camera} by {@code CameraMixin}. Outside the mixin package, which Mixin refuses to load classes from. */ public interface RollableCamera { /** Like vanilla's {@code setRotation}, with a roll in degrees as Forge applies it. */ void mcheli$setRotation(float yRot, float xRot, float roll); From 8b43e05e9b0cd3282a6f68e959da731c2704dfd5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:38:46 +0800 Subject: [PATCH 0012/1461] Import RollableCamera from its new package in CameraMixin on Fabric 1.21.1 --- .../java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java | 1 + 1 file changed, 1 insertion(+) diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java index 62bf841c..d16eb926 100644 --- a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli.fabric.mixin; +import com.jnhprojects.mcheli.fabric.RollableCamera; import net.minecraft.client.Camera; import org.joml.Quaternionf; import org.joml.Vector3f; From 4342449728298b613a5d7f6e1f660258cba63512 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:38:46 +0800 Subject: [PATCH 0013/1461] Import RollableCamera from its new package in GameRendererMixin on Fabric 1.21.1 --- .../com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java | 1 + 1 file changed, 1 insertion(+) diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java index faba98b2..09c2fcd5 100644 --- a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -1,6 +1,7 @@ package com.jnhprojects.mcheli.fabric.mixin; import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; import net.minecraft.client.Camera; import net.minecraft.client.renderer.GameRenderer; import org.spongepowered.asm.mixin.Final; From 4332a4a73b12bc4d493ffee4e28b20913099a6e1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:49:48 +0800 Subject: [PATCH 0014/1461] Build NeoForge and Fabric on 26.3 again now that they have caught up --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0aca9b95..7c2cfc6e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mod_license=MCHeli Rework Licence 1.0 (see LICENSE) # Platform modules built by this project, one per Minecraft version. Each has its own # gradle.properties with the Minecraft, loader and Java versions. -platforms=forge-26.3 +platforms=forge-26.3,neoforge-26.3,fabric-26.3 # Libraries the common module compiles against. Every supported Minecraft version ships them. slf4j_version=2.0.17 From 666d56627d0d18897738610f903bb789b6b34775 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:49:48 +0800 Subject: [PATCH 0015/1461] Note the 26.3 loaders, the jar contents and the Fabric startup fix in the changelog --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81dd8409..8045cd0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). ## [Unreleased] +### Added + +- NeoForge and Fabric on Minecraft 26.3 catch up with Forge: everything from phases 8 to 12 now runs + there, from the drafting table and vehicle screen to smoke and sight zoom. + +### Changed + +- Forge, NeoForge and Fabric on 26.3 are all built on every pull request again. +- Clients and servers on different loaders cannot play together, which the roadmap used to promise. + Each loader refuses the other at its own handshake, before the mod runs. A pack zip still works on + every loader when copied by hand. + +### Fixed + +- Fabric clients crashed at startup on 26.3 and 1.21.1, because `RollableCamera` sat in the mixin package. + ## [1.1.0-beta.8.1] - 2026-11-06 A hotfix for the Forge 26.3 jar, with no other changes. Worlds from 1.1.0-beta.8 load as before, and From 91d54353da25fa224af154e92d447c94d4e1e45b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:49:48 +0800 Subject: [PATCH 0016/1461] Tick the 26.3 port and drop the cross-loader promise, the loaders refuse each other --- docs/ROADMAP.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0e473659..778c9780 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -226,8 +226,9 @@ almost everything, adds NeoForge and Fabric on 26.3, then brings all three to 1. - [x] NeoForge and Fabric on 26.3, with the same features and checklists as Forge - [x] 1.21.1 (Java 21) on Forge, NeoForge and Fabric: older rendering (no render states), older networking, item models from the item JSON instead of `item_model` -- [ ] Content packs and their sync work across loaders on the same Minecraft version (a Fabric - client on a NeoForge server) +- Dropped: a Fabric client on a NeoForge server, or the reverse. Tried in phase 13, each loader + refuses the other at its own handshake before the mod runs. A pack zip copied by hand still works + on every loader, since the format knows nothing about them. - [x] Legacy importer checked on 1.21.1 (pack format numbers and the asset layout differ) - [x] Jar names carry the loader and the Minecraft version, such as `mcheli-fabric-1.21.1-1.1.0-beta.3.jar`; the release workflow publishes every jar @@ -304,8 +305,11 @@ added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest - [x] A Gradle module for every version and loader pair, the missing ones as placeholders that download their loader and only load and log -- [ ] Every module in `platforms`; phases 8 to 12 ported to NeoForge and Fabric on 26.3 and to all - three loaders on 1.21.1 +- [x] Phases 8 to 12 ported to NeoForge and Fabric on 26.3 +- [x] Every resource the repository holds ships in the jar (betas 4 to 8 lacked recipes, the thermal + shader, the smoke particle and the drafting table model) +- [ ] Phases 8 to 12 ported to all three loaders on 1.21.1 +- [ ] Every module in `platforms` - [ ] `minecraft-26.2`, `minecraft-26.1`, `minecraft-1.21.11`, `minecraft-1.21.5`, `minecraft-1.20.6`, `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its placeholder modules with real glue From 299b903e571c1455df914294581d92cf36d022ec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:49:49 +0800 Subject: [PATCH 0017/1461] Test server joins on the same loader, other loaders are refused by design --- docs/TESTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 5c3e6d88..56b65726 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -44,7 +44,8 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi - [ ] A world made on Forge 26.3 with vehicles in it opens on NeoForge and Fabric 26.3 with the vehicles and items intact, and back. -- [ ] A Fabric client joins a NeoForge server (both with the mod) and downloads its packs. +- [ ] A client on the same loader as the dedicated server joins and downloads its packs. A client on + another loader is refused by the loaders themselves, with a message naming the one it needs. - [ ] A `serverconfig/mcheli-server.toml` written by 1.1.0-beta.2 (Forge) keeps its values. ### 1.21.1 only From eaae3f400a6f5333402aa0e7323bc63fbe4241b1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 20:49:49 +0800 Subject: [PATCH 0018/1461] Describe the jar guard and the platforms list as they are now --- docs/DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index ce0636b0..3a55846c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -40,8 +40,8 @@ wait for the other process to finish and build again. Shared properties live in the root `gradle.properties`: `mod_version`, the mod metadata, the list of loader modules (`platforms`) and the library versions `common` compiles against. Each loader module has its own `gradle.properties` with its loader, Minecraft and Java versions. During the -betas, `platforms` lists only `forge-26.3`, so CI and the release build one jar; the other modules -stay in the repository and come back to the list for each final release (roadmap phase 13). +betas up to beta.8, `platforms` listed only `forge-26.3`. Roadmap phase 13 adds modules back as each +one catches up, currently the three on 26.3; the others stay in the repository until they are ported. `mods.toml`, `neoforge.mods.toml`, `fabric.mod.json` and `pack.mcmeta` are filled in from them at build time. What every loader module shares (source folders, jar contents, manifest, file names) is in `gradle/loader.gradle`. From 3bdd939cac1726f869f0374e0daef74706690bfe Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:09 +0800 Subject: [PATCH 0019/1461] Write equipment ingredients as objects for 1.21.1, the E-767 recipe failed to load --- .../com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java b/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java index 5b47de97..9f4169fb 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java @@ -208,7 +208,7 @@ private static String ingredient(String name, int damage, Predicate modI String lower = name.trim().toLowerCase(Locale.ROOT); if (lower.startsWith("mcheli:")) { if (EQUIPMENT_ITEMS.contains(lower)) { - return '"' + lower + '"'; + return target == ImportTarget.MODERN ? '"' + lower + '"' : "{\"item\": \"" + lower + "\"}"; } if (!modItems.test(lower)) { throw new InvalidRecipeException(lower + " does not exist (yet)"); From 385dfb46dc8ef1b139240e9dc8283ffcccf986b4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:10 +0800 Subject: [PATCH 0020/1461] Test that equipment ingredients follow the import target --- .../mcheli/content/recipe/LegacyRecipeTargetTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/common/src/test/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipeTargetTest.java b/common/src/test/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipeTargetTest.java index 01ce2086..9167c9cc 100644 --- a/common/src/test/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipeTargetTest.java +++ b/common/src/test/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipeTargetTest.java @@ -59,6 +59,14 @@ void minecraft1211RecipesUseObjectsAndCustomModelData() { """, LegacyRecipe.toJson(AH_6X, LINE, true, "mcheli:ah-6"::equals, ImportTarget.V1_21_1)); } + @Test + void equipmentIngredientsFollowTheTarget() { + assertTrue(LegacyRecipe.toJson(AH_6X, "mcheli:uav_station,coal", false, name -> false, ImportTarget.V1_21_1) + .contains("{\"item\": \"mcheli:uav_station\"}")); + assertTrue(LegacyRecipe.toJson(AH_6X, "mcheli:uav_station,coal", false, name -> false, ImportTarget.MODERN) + .contains("\"mcheli:uav_station\"")); + } + @Test void minecraft1211ShapelessIngredientsAreObjectsToo() { String json = LegacyRecipe.toJson(AH_6X, "iron_block,coal", false, name -> false, ImportTarget.V1_21_1); From 783aa40eba1a243d86dbd6cf488e97667c41d094 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:10 +0800 Subject: [PATCH 0021/1461] Number the smoke grenades' icons on 1.21.1 and bump the import format --- .../com/jnhprojects/mcheli/content/LegacyImporter.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java b/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java index 50c50552..2db0f324 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli.content; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.content.recipe.LegacyRecipe; @@ -47,9 +48,9 @@ public final class LegacyImporter { * Bump whenever the output changes, so existing installs import again. 2: recipes craft the * generic vehicle items of 1.1.0. 3: vehicle ingredients name their type for every loader, and * the output depends on the {@link ImportTarget}. 4: recipes may use the equipment - * items, so the E-767 is crafted with a UAV station. + * items, so the E-767 is crafted with a UAV station. 5: on 1.21.1, smoke grenades get icon numbers too. */ - public static final int FORMAT = 4; + public static final int FORMAT = 5; /** * Written at the pack root for {@link ImportTarget#V1_21_1}: each original vehicle's @@ -289,6 +290,9 @@ private Map writeKindModels(ContentRegistry content) throws IOE for (AircraftDefinition def : content.all()) { byKind.computeIfAbsent(def.kind(), k -> new ArrayList<>()).add(def.name()); } + for (ThrowableDefinition def : content.throwables()) { + byKind.computeIfAbsent("throwable", k -> new ArrayList<>()).add(def.name()); + } Map icons = new TreeMap<>(); for (Map.Entry> e : byKind.entrySet()) { List names = e.getValue(); From dadbc2028a6f9dc6c27e0aa37a1df55c4950c4d7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:11 +0800 Subject: [PATCH 0022/1461] Add GameRendererInvoker.java on fabric 1.21.1 --- .../mcheli/fabric/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..cba28b44 --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#loadEffect} private on 1.21.1. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("loadEffect") + void mcheli$loadEffect(ResourceLocation effect); +} From 7db73c8cc2f9704d29cf2737b988217c89b2db14 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:12 +0800 Subject: [PATCH 0023/1461] Add ParticleEngineMixin.java on fabric 1.21.1 --- .../fabric/mixin/ParticleEngineMixin.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java new file mode 100644 index 00000000..87ebfeb7 --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java @@ -0,0 +1,32 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.google.common.collect.ImmutableList; +import com.jnhprojects.mcheli.client.render.Glow; +import net.minecraft.client.particle.ParticleEngine; +import net.minecraft.client.particle.ParticleRenderType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +/** + * Vanilla only draws the particle render types on its own list, which leaves our glow and smoke + * particles invisible. Forge and NeoForge draw every render type in use. + */ +@Mixin(ParticleEngine.class) +abstract class ParticleEngineMixin { + @Shadow + @Final + @Mutable + private static List RENDER_ORDER; + + @Inject(method = "", at = @At("RETURN")) + private static void mcheli$addRenderTypes(CallbackInfo ci) { + RENDER_ORDER = ImmutableList.builder().addAll(RENDER_ORDER).add(Glow.PARTICLE_LAYER, Glow.SMOKE_LAYER).build(); + } +} From 52670c4034abfe05e8966fe4a1762068adc20916 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:12 +0800 Subject: [PATCH 0024/1461] Port DraftingTableBlock.java to 1.21.1 --- .../mcheli/block/DraftingTableBlock.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java new file mode 100644 index 00000000..7c4dbab5 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java @@ -0,0 +1,100 @@ +package com.jnhprojects.mcheli.block; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +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.block.state.properties.IntegerProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jspecify.annotations.Nullable; + +/** + * The drafting table, {@code MCH_DraftingTableBlock}: right-click opens the recipe browser, and + * Shift and right-click switches its lamp. It turns in eighths of a circle to face whoever placed + * it, and its block entity draws the original model. + */ +public class DraftingTableBlock extends BaseEntityBlock { + public static final IntegerProperty ROTATION = IntegerProperty.create("rotation", 0, 7); + public static final BooleanProperty LIT = BooleanProperty.create("lit"); + private static final VoxelShape SHAPE = Block.box(0.0, 0.0, 0.0, 16.0, 15.0, 16.0); + private static final MapCodec CODEC = simpleCodec(DraftingTableBlock::new); + private static final Component TITLE = Component.translatable("block.mcheli.drafting_table"); + + public DraftingTableBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(ROTATION, 0).setValue(LIT, false)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ROTATION, LIT); + } + + /** {@code onBlockPlacedBy}: the nearest eighth of the placer's heading. */ + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + float yaw = context.getPlayer() != null ? context.getPlayer().getYRot() : 0.0F; + int rotation = (int) ((Mth.positiveModulo(yaw, 360.0F) + 22.5F) / 45.0F) % 8; + return defaultBlockState().setValue(ROTATION, rotation); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive()) { + level.setBlock(pos, state.cycle(LIT), Block.UPDATE_ALL); + level.playSound(null, pos, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.BLOCKS, 0.3F, 0.5F); + } else { + player.openMenu(state.getMenuProvider(level, pos)); + } + return InteractionResult.SUCCESS; + } + + @Override + protected @Nullable MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { + return new SimpleMenuProvider((id, inventory, player) -> new DraftingTableMenu(id, inventory, ContainerLevelAccess.create(level, pos)), TITLE); + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + /** The block entity renderer draws the table. */ + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new DraftingTableBlockEntity(pos, state); + } +} From 2fa7e76259aded0abf0ca86956e821ccff4599a4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:13 +0800 Subject: [PATCH 0025/1461] Port DraftingTableBlockEntity.java to 1.21.1 --- .../mcheli/block/DraftingTableBlockEntity.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java new file mode 100644 index 00000000..56238f3f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.block; + +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** {@code MCH_DraftingTableTileEntity}: holds nothing; it is there so the table's model can be drawn. */ +public class DraftingTableBlockEntity extends BlockEntity { + public DraftingTableBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.DRAFTING_TABLE.get(), pos, state); + } +} From 355e3398ff5c2dfc77d48056e03bd4987f10be93 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:13 +0800 Subject: [PATCH 0026/1461] Port GltdController.java to 1.21.1 --- .../mcheli/client/GltdController.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/GltdController.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/GltdController.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/GltdController.java new file mode 100644 index 00000000..a8db0832 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/GltdController.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.network.EquipmentActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +/** + * The local player at a GLTD, {@code MCH_ClientGLTDTickHandler} and {@code MCH_GuiGLTD}: the + * view moves to the sight, the zoom and camera mode keys work as in a vehicle's sight, the use + * key calls the A-10 on the spot under the cross, and a small readout says when it can be called + * again. + */ +final class GltdController { + /** The original zoomed smoothly up to ten; the vehicle sights double each press. */ + private static final int MAX_ZOOM = 8; + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int CROSS = 0xC0FF4040; + + /** Edge of the use key, so holding it calls one strike rather than one per tick. */ + private static boolean useWasDown; + + private GltdController() { + } + + static @Nullable GltdEntity gltdOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof GltdEntity gltd ? gltd : null; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + GltdEntity gltd = gltdOf(player); + if (gltd == null) { + useWasDown = false; + return; + } + while (KeyBindings.FOLD.consumeClick()) { + VehicleCamera.zoomStep(player, MAX_ZOOM); + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + VehicleCamera.nextMode(player, on -> send(gltd, + on ? EquipmentActionPacket.Action.NIGHT_VISION_ON : EquipmentActionPacket.Action.NIGHT_VISION_OFF)); + } + boolean useDown = mc.screen == null && mc.options.keyUse.isDown(); + if (useDown && !useWasDown) { + send(gltd, EquipmentActionPacket.Action.CALL_STRIKE); + } + useWasDown = useDown; + } + + /** The use key belongs to the sight while sitting at a GLTD. */ + static boolean onUseItem() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + /** First person looks out of the sight rather than from behind the tripod. */ + static void onCameraAngles(CameraAngles angles) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd != null && mc.options.getCameraType().isFirstPerson()) { + CrewController.placeCamera(angles.camera(), gltd.sightPosition(angles.partialTick())); + } + } + + static boolean hidesHand() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + static void drawHud(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd == null || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int cx = g.guiWidth() / 2; + int cy = g.guiHeight() / 2; + g.fill(cx - 12, cy, cx - 3, cy + 1, CROSS); + g.fill(cx + 4, cy, cx + 13, cy + 1, CROSS); + g.fill(cx, cy - 12, cx + 1, cy - 3, CROSS); + g.fill(cx, cy + 4, cx + 1, cy + 13, CROSS); + + Font font = mc.font; + int x = cx + 20; + int y = cy + 20; + int wait = gltd.waitTicks(); + if (wait > 0) { + g.drawString(font, Component.translatable("hud.mcheli.gltd.wait", (wait + 19) / 20), x, y, AMBER); + } else { + g.drawString(font, Component.translatable("hud.mcheli.gltd.ready"), x, y, WHITE); + } + g.drawString(font, Component.translatable("hud.mcheli.zoom", Math.round(VehicleCamera.zoom())), x, y + 10, WHITE); + } + + private static void send(GltdEntity gltd, EquipmentActionPacket.Action action) { + ModNetwork.sendToServer(new EquipmentActionPacket(gltd.getId(), action)); + } +} From c97861f7d579b58e54e31beb1833540a37fd4efd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:14 +0800 Subject: [PATCH 0027/1461] Port LauncherController.java to 1.21.1 --- .../mcheli/client/LauncherController.java | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java new file mode 100644 index 00000000..ff22d864 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java @@ -0,0 +1,236 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.network.LauncherPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The Stinger and Javelin in the player's hands, {@code MCH_ClientLightWeaponTickHandler} and + * {@code MCH_GuiLightWeapon}. Raised, the launcher's sight covers the view and its guidance locks + * onto what the cross holds; the attack key fires once the lock is complete. Z steps the zoom, X + * the Javelin's direct and top attack modes. An empty launcher held down reloads itself from a + * missile in the inventory. + */ +final class LauncherController { + /** Ticks of raising before the sight and the lock start, as in the original. */ + private static final int RAISE_TICKS = 10; + private static final int RELOAD_TICKS = 40; + /** Top attack climbs first and needs this much room, measured flat. */ + private static final double TOP_ATTACK_MIN_DISTANCE = 40.0; + private static final int GREEN = 0xFF2AD848; + private static final int RED = 0xFFDF0808; + private static final int WHITE = 0xFFFFFFFF; + + private static @Nullable String weapon; + private static @Nullable Guidance guidance; + private static int zoomIndex; + private static int mode; + private static int reloadTicks; + private static int lockSoundTicks; + private static boolean attackWasDown; + + private LauncherController() { + } + + private static ItemStack launcher(@Nullable LocalPlayer player) { + if (player == null || !LauncherItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getItemInHand(InteractionHand.MAIN_HAND); + return stack.getItem() instanceof LauncherItem ? stack : ItemStack.EMPTY; + } + + /** Whether the player holds a launcher up to the eye. */ + static boolean isRaised() { + LocalPlayer player = Minecraft.getInstance().player; + ItemStack stack = launcher(player); + return !stack.isEmpty() && player.isUsingItem() && player.getUseItem() == stack && player.getTicksUsingItem() > RAISE_TICKS; + } + + private static @Nullable WeaponDefinition definition(ItemStack stack) { + return MCHeli.content().weapon(((LauncherItem) stack.getItem()).weapon()); + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = launcher(player); + WeaponDefinition def = stack.isEmpty() ? null : definition(stack); + if (def == null) { + weapon = null; + guidance = null; + reloadTicks = 0; + attackWasDown = false; + return; + } + String name = ((LauncherItem) stack.getItem()).weapon(); + if (!name.equals(weapon) || guidance == null) { + weapon = name; + boolean air = def.type != WeaponType.AT_MISSILE; + guidance = new Guidance(air, !air, def.ridableOnly, def.lockTime, e -> e != player && !e.isAlliedTo(player)); + zoomIndex = 0; + mode = 0; + } + boolean raised = isRaised(); + boolean attackDown = raised && mc.screen == null && mc.options.keyAttack.isDown(); + boolean attackPressed = attackDown && !attackWasDown; + attackWasDown = attackDown; + + if (LauncherItem.isLoaded(stack)) { + reloadTicks = 0; + if (raised) { + if (guidance.lock(player, player.getEyePosition())) { + lockSoundTicks = 0; + } + playLockSound(player); + } else { + guidance.clear(); + } + } else { + guidance.clear(); + reload(player, stack); + } + guidance.tick(); + + if (!raised) { + return; + } + while (KeyBindings.FOLD.consumeClick()) { + if (def.zoom.length > 1) { + zoomIndex = (zoomIndex + 1) % def.zoom.length; + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + if (def.modeNum > 1) { + mode = (mode + 1) % def.modeNum; + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 0.9F); + } + } + if (attackPressed) { + Entity target = guidance.lastLocked(); + if (LauncherItem.isLoaded(stack) && guidance.isLocked() && target != null && canFire(player, target)) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.FIRE, target.getId(), mode)); + guidance.clear(); + } else { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + } + + /** Top attack needs room to climb and dive. */ + private static boolean canFire(LocalPlayer player, @Nullable Entity target) { + if (mode == 0 || target == null) { + return true; + } + double dx = target.getX() - player.getX(); + double dz = target.getZ() - player.getZ(); + return Math.sqrt(dx * dx + dz * dz) >= TOP_ATTACK_MIN_DISTANCE; + } + + /** The tone quickens as the lock builds. */ + private static void playLockSound(LocalPlayer player) { + if (guidance == null || guidance.lockCount() <= 0) { + lockSoundTicks = 0; + return; + } + if (lockSoundTicks > 0) { + lockSoundTicks--; + return; + } + lockSoundTicks = Math.max(2, (int) (7 * (1.0 - (double) guidance.lockCount() / guidance.lockCountMax()))); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + + /** Held down and empty, with a missile to hand: two seconds to load it. */ + private static void reload(LocalPlayer player, ItemStack stack) { + LauncherItem item = (LauncherItem) stack.getItem(); + boolean hasMissile = player.getAbilities().instabuild || player.getInventory().findSlotMatchingItem(new ItemStack(item.missile())) >= 0; + if (!hasMissile || player.isUsingItem()) { + reloadTicks = 0; + return; + } + reloadTicks++; + if (reloadTicks == 10) { + player.level().playLocalSound(player, ModSounds.get("fim92_reload"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (reloadTicks == RELOAD_TICKS) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.RELOAD, 0, 0)); + } + } + + static float fov(float fov) { + if (!isRaised()) { + return fov; + } + WeaponDefinition def = definition(launcher(Minecraft.getInstance().player)); + if (def == null || def.zoom.length == 0) { + return fov; + } + float zoom = def.zoom[Math.min(zoomIndex, def.zoom.length - 1)]; + return zoom > 1.0F ? fov / zoom : fov; + } + + static void drawHud(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (!mc.options.getCameraType().isFirstPerson()) { + return; + } + ItemStack stack = launcher(player); + if (stack.isEmpty()) { + return; + } + Font font = mc.font; + if (!isRaised()) { + if (!LauncherItem.isLoaded(stack) && reloadTicks > 0) { + g.drawString(font, Component.translatable("hud.mcheli.launcher.reloading"), g.guiWidth() / 2 - 30, g.guiHeight() / 2 + 20, WHITE); + } + return; + } + WeaponDefinition def = definition(stack); + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + String texture = def != null && def.type == WeaponType.AT_MISSILE ? "javelin" : "stinger"; + g.blit(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + texture + ".png"), (w - size) / 2, (h - size) / 2, size, size, 0.0F, 0.0F, 256, 256, 256, 256); + + int x = w / 2 + 20; + int y = h / 2 + 20; + if (!LauncherItem.isLoaded(stack)) { + g.drawString(font, Component.translatable("hud.mcheli.launcher.empty"), x, y, RED); + } else if (guidance != null && guidance.isLocked()) { + Entity target = guidance.lastLocked(); + g.drawString(font, Component.translatable("hud.mcheli.locked"), x, y, canFire(player, target) ? GREEN : RED); + if (target != null) { + g.drawString(font, String.format("[%.0f m]", target.distanceTo(player)), x, y + 10, GREEN); + } + } else if (guidance != null && guidance.lockCount() > 0) { + g.drawString(font, Component.translatable("hud.mcheli.locking", guidance.lockCount() * 100 / Math.max(guidance.lockCountMax(), 1)), x, y, WHITE); + } + if (def != null && def.modeNum > 1) { + g.drawString(font, Component.translatable(mode == 0 ? "hud.mcheli.launcher.direct" : "hud.mcheli.launcher.top_attack"), x, y + 20, WHITE); + } + if (def != null && def.zoom.length > 0) { + g.drawString(font, Component.translatable("hud.mcheli.zoom", String.format("%.1f", def.zoom[Math.min(zoomIndex, def.zoom.length - 1)])), + x, y + 30, WHITE); + } + } +} From 709ebbb0ae632043e6e7976c36b9f230fb6ba9e3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:14 +0800 Subject: [PATCH 0028/1461] Port RangefinderController.java to 1.21.1 --- .../mcheli/client/RangefinderController.java | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java new file mode 100644 index 00000000..62841616 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java @@ -0,0 +1,154 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.SpotPacket; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.Locale; + +/** + * Looking through the rangefinder, {@code MCH_ClientToolTickHandler} and {@code MCH_GuiRangeFinder}. + * Once it has been held up for a moment the scope covers the view and reads the range; the zoom + * key and the weapon mode key zoom in and out while held, the flare key picks what to spot, and + * the attack key spots. + */ +final class RangefinderController { + private static final ResourceLocation SCOPE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/rangefinder.png"); + /** Ticks of holding it up before the scope shows, as in the original. */ + private static final int RAISE_TICKS = 8; + private static final int COOLDOWN = 60; + private static final float MIN_ZOOM = 1.2F; + private static final float MAX_ZOOM = 10.0F; + private static final double RANGE = 300.0; + private static final int GREEN = 0xFF10FF20; + private static final int RED = 0xFFFF1010; + private static final int WHITE = 0xFFFFFFFF; + private static final int GREY = 0xFF404040; + + private static float zoom = 2.0F; + private static SpotPacket.Mode mode = SpotPacket.Mode.PLAYERS_AND_VEHICLES; + private static int cooldown; + + private RangefinderController() { + } + + /** The rangefinder the player is looking through, or empty. */ + private static ItemStack scope(LocalPlayer player) { + if (player == null || !player.isUsingItem() || player.getTicksUsingItem() <= RAISE_TICKS || !RangefinderItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getUseItem(); + return stack.getItem() instanceof RangefinderItem ? stack : ItemStack.EMPTY; + } + + static boolean isScoped() { + return !scope(Minecraft.getInstance().player).isEmpty(); + } + + static void onClientTick() { + if (cooldown > 0) { + cooldown--; + } + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty()) { + zoom = 2.0F; + return; + } + if (KeyBindings.FOLD.isDown() && zoom < MAX_ZOOM) { + zoom = Math.min(zoom + zoom / 10.0F, MAX_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 1.0F); + } + if (KeyBindings.WEAPON_MODE.isDown() && zoom > MIN_ZOOM) { + zoom = Math.max(zoom - zoom / 10.0F, MIN_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 0.9F); + } + while (KeyBindings.FLARE.consumeClick()) { + mode = SpotPacket.Mode.values()[(mode.ordinal() + 1) % SpotPacket.Mode.values().length]; + // Alone in a world there is no one else to spot, as the original reasoned. + boolean alone = mc.hasSingleplayerServer() && !mc.getSingleplayerServer().isPublished(); + if (alone && mode == SpotPacket.Mode.PLAYERS_AND_VEHICLES) { + mode = SpotPacket.Mode.MOBS; + } + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 0.9F); + } + if (mc.options.keyAttack.isDown() && cooldown == 0) { + if (mode != SpotPacket.Mode.MARK_POINT && RangefinderItem.charges(stack) <= 0) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 1.0F, 1.0F); + } else { + cooldown = COOLDOWN; + ModNetwork.sendToServer(new SpotPacket(mode)); + } + } + } + + static float fov(float fov) { + return isScoped() ? fov / zoom : fov; + } + + static void drawHud(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty() || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + g.blit(SCOPE, (w - size) / 2, (h - size) / 2, size, size, 0.0F, 0.0F, 256, 256, 256, 256); + + // The readouts sit on the scope texture's own labels: lay them out in its 512 wide space, + // centred, as the original did, and scale that with the scope. + float k = size / 512.0F; + g.pose().pushPose(); + g.pose().translate(w / 2.0F, h / 2.0F, 0.0F); + g.pose().scale(k, k, 1.0F); + drawReadouts(g, mc.font, player, stack); + g.pose().popPose(); + } + + /** In scope space: the centre of the view is at 0, 0. */ + private static void drawReadouts(GuiGraphics g, Font font, LocalPlayer player, ItemStack stack) { + int charge = RangefinderItem.charges(stack) * 100 / RangefinderItem.CHARGES; + // Over BATT. + g.drawString(font, String.format("%3d%%", charge), -100, 55, charge > 0 ? GREEN : RED); + if (charge <= 0) { + g.drawString(font, Component.translatable("hud.mcheli.rangefinder.empty"), -40, 55, RED); + } else { + // Over RANGE METERS. + Vec3 eye = player.getEyePosition(); + HitResult hit = player.level().clip(new ClipContext(eye, eye.add(player.getLookAngle().scale(RANGE)), + ClipContext.Block.OUTLINE, ClipContext.Fluid.ANY, player)); + String range = hit.getType() == HitResult.Type.MISS ? "----" : String.format("%4d", Math.round(hit.getLocation().distanceTo(eye))); + g.drawString(font, range, 40, 55, hit.getType() == HitResult.Type.MISS ? RED : GREEN); + } + // The spot cooldown bar, and the zoom above it. + g.fill(-100, 51, -70, 53, GREEN); + g.fill(-100, 51, -100 + cooldown / 2, 53, RED); + g.drawString(font, String.format("x%.1f", zoom), -100, 31, WHITE); + int y = 21; + for (SpotPacket.Mode m : SpotPacket.Mode.values()) { + if (m == mode) { + g.drawString(font, ">", 30, y, WHITE); + } + g.drawString(font, Component.translatable("hud.mcheli.rangefinder." + m.name().toLowerCase(Locale.ROOT)), 40, y, m == mode ? WHITE : GREY); + y += 10; + } + } +} From 9f8f6c74488361e7c0c8202a73f8cbafe2ed9d73 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:15 +0800 Subject: [PATCH 0029/1461] Port TargetMarkers.java to 1.21.1 --- .../mcheli/client/TargetMarkers.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java new file mode 100644 index 00000000..a7d8f86a --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.joml.Vector4f; +import org.jspecify.annotations.Nullable; + +import java.util.Map; + +/** + * {@code MCH_GuiTargetMarker}: the team's spotted targets and marked point on screen. A target + * gets a triangle over its head with its distance; one off screen gets a diamond at the edge on + * its side. The marked point gets a diamond with its distance and coordinates. + */ +final class TargetMarkers { + private static final int TARGET = 0xFFFF4040; + private static final int POINT = 0xFFFFD000; + private static final int EDGE = 12; + + private TargetMarkers() { + } + + static void tick() { + Markers.tick(); + } + + static void draw(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || mc.player == null) { + return; + } + float partialTicks = delta.getGameTimeDeltaPartialTick(false); + for (Map.Entry spotted : Markers.spotted().entrySet()) { + Entity entity = mc.level.getEntity(spotted.getKey()); + if (entity == null || entity == mc.player || !entity.isAlive()) { + continue; + } + Vec3 top = entity.getPosition(partialTicks).add(0.0, entity.getBbHeight() + 0.5, 0.0); + mark(g, mc, top, "â–¼", TARGET, "%.0fm".formatted(mc.player.distanceTo(entity)), null); + } + BlockPos point = Markers.point(); + if (point != null) { + Vec3 at = Vec3.atCenterOf(point).add(0.0, 1.0, 0.0); + String coordinates = "%d %d %d".formatted(point.getX(), point.getY(), point.getZ()); + mark(g, mc, at, "â—†", POINT, "%.0fm".formatted(Math.sqrt(mc.player.distanceToSqr(at))), coordinates); + } + } + + private static void mark(GuiGraphics g, Minecraft mc, Vec3 at, String symbol, int color, String distance, @Nullable String extra) { + Camera camera = mc.gameRenderer.getMainCamera(); + Vector3fc forward = camera.getLookVector(); + Vec3 offset = at.subtract(camera.getPosition()); + boolean ahead = offset.x * forward.x() + offset.y * forward.y() + offset.z * forward.z() > 0.0; + // 26.x has projectPointToScreen for this; the view bobbing and the sprint widening are left out. + Vector3f view = new Vector3f((float) offset.x, (float) offset.y, (float) offset.z).rotate(camera.rotation().conjugate(new Quaternionf())); + Vector4f clip = mc.gameRenderer.getProjectionMatrix(ClientHooks.fov(mc.options.fov().get())) + .transform(new Vector4f(view, 1.0F)); + Vec3 ndc = new Vec3(clip.x() / clip.w(), clip.y() / clip.w(), clip.z() / clip.w()); + int w = g.guiWidth(); + int h = g.guiHeight(); + double x = (ndc.x + 1.0) / 2.0 * w; + double y = (1.0 - ndc.y) / 2.0 * h; + Font font = mc.font; + if (ahead && x >= 0 && x <= w && y >= 0 && y <= h) { + g.drawCenteredString(font, symbol, (int) x, (int) y - 4, color); + g.drawCenteredString(font, distance, (int) x, (int) y + 6, color); + if (extra != null) { + g.drawCenteredString(font, extra, (int) x, (int) y + 16, color); + } + return; + } + // Behind or off to the side: point the way along the screen edge. + double dx = ahead ? x - w / 2.0 : w / 2.0 - x; + double dy = ahead ? y - h / 2.0 : h / 2.0 - y; + double scale = Math.max(Math.abs(dx) / (w / 2.0 - EDGE), Math.abs(dy) / (h / 2.0 - EDGE)); + if (scale <= 0.0) { + return; + } + int ex = Mth.clamp((int) (w / 2.0 + dx / scale), EDGE, w - EDGE); + int ey = Mth.clamp((int) (h / 2.0 + dy / scale), EDGE, h - EDGE); + g.drawCenteredString(font, "â—†", ex, ey - 4, color); + } +} From 0f0d470b0032a8421bb4ab1a9cb1ac409d0dace8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:15 +0800 Subject: [PATCH 0030/1461] Port VehicleCamera.java to 1.21.1 --- .../mcheli/client/VehicleCamera.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java new file mode 100644 index 00000000..3429a2d1 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java @@ -0,0 +1,126 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.PostChain; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; + +import java.util.function.Consumer; + +/** + * The local crew member's sight, {@code MCH_Camera}: zoom for vehicles with a {@code CameraZoom} + * above one, and night vision and thermal view for those with {@code EnableNightVision}. Both + * reset whenever the player changes seat or leaves the vehicle. + */ +public final class VehicleCamera { + /** The original's camera modes; the HUD reads the ordinal as {@code cam_mode}. */ + public enum Mode { + NORMAL, + NIGHT_VISION, + /** Night vision seen through a white hot filter, the original's "pencil" shader mode. */ + THERMAL + } + + /** The post effect of the thermal view, {@code assets/mcheli/shaders/post/thermal.json}. */ + private static final ResourceLocation THERMAL_EFFECT = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "shaders/post/thermal.json"); + + private static float zoom = 1.0F; + private static Mode mode = Mode.NORMAL; + + private VehicleCamera() { + } + + public static float zoom() { + return zoom; + } + + public static Mode mode() { + return mode; + } + + /** Back to the plain view, on a seat change or after leaving the vehicle. */ + static void reset() { + zoom = 1.0F; + mode = Mode.NORMAL; + } + + /** Keeps the thermal filter loaded while the thermal view is on, and only then. */ + static void tick() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + return; + } + PostChain current = mc.gameRenderer.currentEffect(); + boolean active = current != null && current.getName().equals(THERMAL_EFFECT.toString()); + if (mode == Mode.THERMAL && !active) { + ClientHooks.postEffects().load(THERMAL_EFFECT); + } else if (mode != Mode.THERMAL && active) { + mc.gameRenderer.shutdownEffect(); + } + } + + /** + * {@code zoomCamera}: each press doubles the zoom up to the vehicle's {@code CameraZoom}, and + * the press after that goes back to one. The caller checks that the seat looks through a sight. + */ + static void onZoomKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + zoomStep(player, def != null && !vehicle.isDestroyed() ? def.cameraZoom : 1); + } + + /** One press of the zoom key on a sight that zooms up to {@code max}. */ + static void zoomStep(LocalPlayer player, int max) { + if (max <= 1) { + refuse(player); + return; + } + zoom = zoom >= max - 0.01F ? 1.0F : Math.min(zoom * 2.0F, max); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + + /** + * {@code switchCameraMode}: steps from the plain view to night vision, to thermal view and + * back. The server applies night vision as an effect in both of the last two; the thermal + * filter is drawn here. + */ + static void onCameraModeKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + if (def == null || !def.nightVision || vehicle.isDestroyed()) { + refuse(player); + return; + } + nextMode(player, on -> ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), + on ? CrewActionPacket.Action.NIGHT_VISION_ON : CrewActionPacket.Action.NIGHT_VISION_OFF))); + } + + /** + * Steps to the next camera mode on a sight that has night vision. {@code nightVision} asks + * the server to switch the effect on or off, only when that changes. + */ + static void nextMode(LocalPlayer player, Consumer nightVision) { + Mode next = Mode.values()[(mode.ordinal() + 1) % Mode.values().length]; + if (mode == Mode.NORMAL || next == Mode.NORMAL) { + nightVision.accept(next != Mode.NORMAL); + } + mode = next; + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + + /** The view's field of view, narrowed by the zoom. */ + public static float fov(float fov) { + return zoom > 1.0F ? fov / zoom : fov; + } + + /** {@code playSoundNG}: the key does nothing in this seat. */ + private static void refuse(LocalPlayer player) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } +} From e8c95a7d1813914acc3d6732c7957f905a798fda Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:16 +0800 Subject: [PATCH 0031/1461] Port SmokeParticle.java to 1.21.1 --- .../mcheli/client/particle/SmokeParticle.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java new file mode 100644 index 00000000..3efefa3f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java @@ -0,0 +1,85 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.ParticleRenderType; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.particle.TextureSheetParticle; +import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; + +/** + * {@code mcheli:smoke}: a puff of coloured smoke, {@code MCH_EntityParticleSmoke}. It swells to its + * full size over the first part of its life, its colour drifts towards white, it rises slowly and + * fades out at the end. The sprites are vanilla's campfire smoke. + */ +public class SmokeParticle extends TextureSheetParticle { + private final float size; + + SmokeParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, SmokeParticleOptions options, + SpriteSet sprites, RandomSource random) { + super(level, x, y, z); + setSprite(sprites.get(random)); + setParticleSpeed(xd, yd, zd); + // The original drew a puff 0.1 x its size out from the middle, so size 10 is two blocks across. + this.size = options.size() * 0.1F * (0.8F + random.nextFloat() * 0.4F); + this.quadSize = size * 0.4F; + this.lifetime = options.lifetime() > 0 ? options.lifetime() : 60 + random.nextInt(40); + this.friction = 0.96F; + this.gravity = -0.002F; + this.hasPhysics = true; + setColor((FastColor.ARGB32.red(options.color()) / 255.0F), (FastColor.ARGB32.green(options.color()) / 255.0F), (FastColor.ARGB32.blue(options.color()) / 255.0F)); + this.alpha = 0.9F; + } + + @Override + public void tick() { + super.tick(); + // toWhite: pale towards the brightest channel while the colour is still strong. + float max = Math.max(rCol, Math.max(gCol, bCol)); + float min = Math.min(rCol, Math.min(gCol, bCol)); + if (max - min > 0.2F) { + rCol += (max - rCol) * 0.016F; + gCol += (max - gCol) * 0.016F; + bCol += (max - bCol) * 0.016F; + } + } + + @Override + public void render(VertexConsumer buffer, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + alpha = 0.9F * Math.min(1.0F, (1.0F - t) * 4.0F); + super.render(buffer, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return size * (0.4F + 0.6F * Math.min(1.0F, t * 3.0F)); + } + + @Override + public ParticleRenderType getRenderType() { + return Glow.SMOKE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public Particle createParticle(SmokeParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd) { + return new SmokeParticle(level, x, y, z, xd, yd, zd, options, sprites, level.random); + } + } +} From 7d2d56028d9dc8bf70a8964b986a21bdec071f7f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:17 +0800 Subject: [PATCH 0032/1461] Port ChainRenderer.java to 1.21.1 --- .../mcheli/client/render/ChainRenderer.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java new file mode 100644 index 00000000..7279b902 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +/** + * {@code MCH_RenderChain}: a line of links from the towed end to the end that pulls, one link + * every 0.95 blocks, each turned along the chain. + */ +public class ChainRenderer extends EntityRenderer { + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/chain.png"); + private static final double LINK = 0.95; + private static final int GREY = 0xFF808080; + + public ChainRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + /** The chain hangs over the end that pulls but reaches to the other end; keep both in view. */ + @Override + public boolean shouldRender(ChainEntity chain, Frustum frustum, double x, double y, double z) { + Entity towed = chain.towed(); + return super.shouldRender(chain, frustum, x, y, z) || towed != null && frustum.isVisible(towed.getBoundingBox()); + } + + @Override + public void render(ChainEntity chain, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + Entity tow = chain.tow(); + Entity towed = chain.towed(); + Mesh mesh = MeshCache.INSTANCE.get("", "chain"); + if (mesh != null && tow != null && towed != null) { + Vec3 start = towed.getPosition(partialTicks); + Vec3 from = start.subtract(chain.getPosition(partialTicks)); + Vec3 span = tow.getPosition(partialTicks).subtract(start); + double length = span.length(); + if (length > LINK) { + Vec3 step = span.scale(LINK / length); + float yaw = (float) Math.toDegrees(Math.atan2(step.z, step.x)); + float pitch = (float) Math.toDegrees(Math.atan2(step.y, Math.sqrt(step.x * step.x + step.z * step.z))); + VertexConsumer buffer = buffers.getBuffer(RenderType.entityCutout(TEXTURE)); + pose.pushPose(); + pose.translate(from.x, from.y, from.z); + for (double left = length; left > LINK; left -= LINK) { + pose.translate(step.x, step.y, step.z); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-yaw)); + pose.mulPose(Axis.ZP.rotationDegrees(pitch)); + MeshRenderer.emit(mesh.groups(), pose.last(), buffer, light, OverlayTexture.NO_OVERLAY, GREY); + pose.popPose(); + } + pose.popPose(); + } + } + super.render(chain, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(ChainEntity chain) { + return TEXTURE; + } +} From a9206437270f7f840c2dcb576ed1a9ee06232728 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:17 +0800 Subject: [PATCH 0033/1461] Port DraftingTableRenderer.java to 1.21.1 --- .../client/render/DraftingTableRenderer.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java new file mode 100644 index 00000000..fc238c88 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java @@ -0,0 +1,38 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.Vec3; + +/** {@code MCH_DraftingTableRenderer}: the original model, turned to the block's eighth of a circle. */ +public class DraftingTableRenderer implements BlockEntityRenderer { + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/block/drafting_table.png"); + /** The original greyed the table a little. */ + private static final int TINT = 0xFFBFBFBF; + + public DraftingTableRenderer(BlockEntityRendererProvider.Context context) { + } + + @Override + public void render(DraftingTableBlockEntity table, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light, int overlay) { + Mesh mesh = MeshCache.INSTANCE.get("blocks", "drafting_table"); + if (mesh == null) { + return; + } + pose.pushPose(); + pose.translate(0.5F, 0.0F, 0.5F); + pose.mulPose(Axis.YP.rotationDegrees(-table.getBlockState().getValue(DraftingTableBlock.ROTATION) * 45.0F + 180.0F)); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(TEXTURE)), light, OverlayTexture.NO_OVERLAY, TINT); + pose.popPose(); + } +} From bd6ad7e03d4e8e2ee814c3f3ee4552b7a777163f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:18 +0800 Subject: [PATCH 0034/1461] Port EquipmentRenderer.java to 1.21.1 --- .../client/render/EquipmentRenderer.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java new file mode 100644 index 00000000..42782a0e --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; + +/** + * Equipment drawn as one model from the models root with the texture of the same name, turned to + * the entity's heading: the container, {@code MCH_RenderContainer}. + */ +public class EquipmentRenderer extends EntityRenderer { + private final String model; + private final ResourceLocation texture; + private final float yOffset; + private final int tint; + + /** + * @param model the file name under {@code models/}, without extension + * @param yOffset how far the model sits below the entity's position + * @param tint ARGB the model is multiplied by; the original greyed the container a little + */ + public EquipmentRenderer(EntityRendererProvider.Context context, String model, float yOffset, int tint, float shadow) { + super(context); + this.model = model; + this.texture = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + model + ".png"); + this.yOffset = yOffset; + this.tint = tint; + this.shadowRadius = shadow; + } + + @Override + public void render(T entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + Mesh mesh = MeshCache.INSTANCE.get("", model); + if (mesh != null) { + pose.pushPose(); + pose.translate(0.0F, -yOffset, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + pose.mulPose(Axis.XP.rotationDegrees(entity.getViewXRot(partialTicks))); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(texture)), light, OverlayTexture.NO_OVERLAY, tint); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(T entity) { + return texture; + } +} From 7818fde45ed87d2bcb6c4d8f96e5be416bd06286 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:18 +0800 Subject: [PATCH 0035/1461] Port GltdRenderer.java to 1.21.1 --- .../mcheli/client/render/GltdRenderer.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java new file mode 100644 index 00000000..3c2fd9bf --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java @@ -0,0 +1,89 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * {@code MCH_RenderGLTD}: the tripod, its head turned to where the viewer aims, and a faint red + * laser out of the head while someone uses it. The viewer's own head part is left out in first + * person, where it would fill the view. + */ +public class GltdRenderer extends EntityRenderer { + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gltd.png"); + private static final float HEAD_PIVOT = 0.45F; + private static final double LASER_LENGTH = 100.0; + private static final int LASER = 0xFF2020; + + public GltdRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + @Override + public void render(GltdEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + float bodyYaw = entity.getViewYRot(partialTicks); + Entity viewer = entity.getFirstPassenger(); + Minecraft mc = Minecraft.getInstance(); + boolean drawHead = viewer == null || viewer != mc.player || !mc.options.getCameraType().isFirstPerson(); + float headYaw = viewer != null ? viewer.getViewYRot(partialTicks) : bodyYaw; + float headPitch = viewer != null ? viewer.getViewXRot(partialTicks) : 0.0F; + Mesh mesh = MeshCache.INSTANCE.get("", "gltd"); + if (mesh != null) { + VertexConsumer buffer = buffers.getBuffer(RenderType.entityCutout(TEXTURE)); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-bodyYaw)); + draw(mesh.part("$body"), pose, buffer, light); + pose.popPose(); + if (drawHead) { + pose.pushPose(); + pose.translate(0.0F, HEAD_PIVOT, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-headYaw)); + pose.mulPose(Axis.XP.rotationDegrees(headPitch)); + pose.translate(0.0F, -HEAD_PIVOT, 0.0F); + draw(mesh.part("$head"), pose, buffer, light); + pose.popPose(); + } + } + if (viewer != null) { + float yaw = headYaw * Mth.DEG_TO_RAD; + float pitch = headPitch * Mth.DEG_TO_RAD; + Vec3 look = new Vec3(-Mth.sin(yaw) * Mth.cos(pitch), -Mth.sin(pitch), Mth.cos(yaw) * Mth.cos(pitch)); + Vec3 head = new Vec3(0.0, HEAD_PIVOT + 0.2, 0.0); + Vec3 toCamera = entity.getPosition(partialTicks).add(head).subtract(entityRenderDispatcher.camera.getPosition()); + pose.pushPose(); + pose.translate(head.x, head.y, head.z); + // Brightest at the tripod, fading out along the beam. + Glow.streak(pose, buffers, toCamera, look.scale(LASER_LENGTH), 0.02F, LASER, 0.25F); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(GltdEntity entity) { + return TEXTURE; + } + + private static void draw(List groups, PoseStack pose, VertexConsumer buffer, int light) { + if (!groups.isEmpty()) { + MeshRenderer.emit(groups, pose.last(), buffer, light, OverlayTexture.NO_OVERLAY); + } + } +} From d8d6bedd80c7e186b0be8a4b886196acff79ef6d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:19 +0800 Subject: [PATCH 0036/1461] Port ParachuteRenderer.java to 1.21.1 --- .../client/render/ParachuteRenderer.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java new file mode 100644 index 00000000..7d71fa7d --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +/** + * {@code MCH_RenderParachute}: the item's round canopy, or the ejection seat with its canopy, + * from the models the import brought in. + */ +public class ParachuteRenderer extends EntityRenderer { + private static final ResourceLocation ROUND = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute1.png"); + private static final ResourceLocation SEAT = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute2.png"); + + public ParachuteRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + @Override + public void render(ParachuteEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + List groups = groups(entity); + if (!groups.isEmpty()) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + MeshRenderer.emit(groups, pose.last(), buffers.getBuffer(RenderType.entityCutout(getTextureLocation(entity))), light, OverlayTexture.NO_OVERLAY); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(ParachuteEntity entity) { + return entity.kind() == ParachuteEntity.Kind.ITEM ? ROUND : SEAT; + } + + private static List groups(ParachuteEntity entity) { + if (entity.kind() == ParachuteEntity.Kind.ITEM) { + Mesh mesh = MeshCache.INSTANCE.get("", "parachute1"); + return mesh != null ? mesh.groups() : List.of(); + } + Mesh mesh = MeshCache.INSTANCE.get("", "parachute2"); + boolean canopy = entity.kind() == ParachuteEntity.Kind.DROP || entity.isOpen(); + return mesh != null ? mesh.part(canopy ? "$parachute" : "$seat") : List.of(); + } +} From 9efc732da5a1da57a828a7db761563062e692e1c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:19 +0800 Subject: [PATCH 0037/1461] Port ThrowableRenderer.java to 1.21.1 --- .../client/render/ThrowableRenderer.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java new file mode 100644 index 00000000..3eb52fe0 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java @@ -0,0 +1,39 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +/** {@code MCH_RenderThrowable}: the grenade's own model from {@code models/throwable}. */ +public class ThrowableRenderer extends EntityRenderer { + public ThrowableRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + @Override + public void render(ThrowableEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + String name = entity.definitionName(); + Mesh mesh = name.isEmpty() ? null : MeshCache.INSTANCE.get("throwable", name); + if (mesh != null) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(getTextureLocation(entity))), light, OverlayTexture.NO_OVERLAY); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(ThrowableEntity entity) { + return ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/throwable/" + entity.definitionName() + ".png"); + } +} From 56eaeffaf33dadf58321d8a87881e437284e5e45 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:20 +0800 Subject: [PATCH 0038/1461] Port UavStationRenderer.java to 1.21.1 --- .../client/render/UavStationRenderer.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java new file mode 100644 index 00000000..ab90fa68 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +/** + * The UAV station and the portable controller, {@code MCH_RenderUavStation}. The station's screen + * lights up while a drone is flown from it; the controller's lid, keyboard half and display fold + * open as it is opened, and light up once fully open. + */ +public class UavStationRenderer extends EntityRenderer { + private static final String[] MODEL = {"uav_station", "uav_portable_controller"}; + /** The original greyed both a little. */ + private static final int TINT = 0xFFBFBFBF; + + public UavStationRenderer(EntityRendererProvider.Context context) { + super(context); + shadowRadius = 1.0F; + } + + @Override + public void render(UavStationEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + int index = entity.kind() == UavStationEntity.PORTABLE ? 1 : 0; + float cover = entity.cover(partialTicks); + boolean lit = index == 1 ? cover > 0.95F : entity.isLinked(); + Mesh mesh = MeshCache.INSTANCE.get("", MODEL[index]); + if (mesh != null) { + ResourceLocation texture = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + MODEL[index] + (lit ? "_on" : "") + ".png"); + VertexConsumer buffer = buffers.getBuffer(RenderType.entityCutout(texture)); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + if (index == 0) { + draw(pose, buffer, mesh.groups(), light); + } else { + draw(pose, buffer, mesh.part("$body"), light); + folding(pose, buffer, mesh.part("$cover"), cover * 60.0F, -0.1812F, -0.3186F, light); + folding(pose, buffer, mesh.part("$laptop_cover"), cover * 95.0F, -0.1808F, -0.0422F, light); + folding(pose, buffer, mesh.part("$display"), cover * -85.0F, -0.1807F, 0.2294F, light); + } + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(UavStationEntity entity) { + return ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + MODEL[entity.kind() == UavStationEntity.PORTABLE ? 1 : 0] + ".png"); + } + + /** {@code renderRotPart}: turns a part about the x axis through its hinge. */ + private static void folding(PoseStack pose, VertexConsumer buffer, List part, float angle, float y, float z, int light) { + pose.pushPose(); + pose.translate(0.0F, y, z); + pose.mulPose(Axis.XN.rotationDegrees(angle)); + pose.translate(0.0F, -y, -z); + draw(pose, buffer, part, light); + pose.popPose(); + } + + private static void draw(PoseStack pose, VertexConsumer buffer, List groups, int light) { + if (!groups.isEmpty()) { + MeshRenderer.emit(groups, pose.last(), buffer, light, OverlayTexture.NO_OVERLAY, TINT); + } + } +} From 1cad6b4eb69802ca6481b3ce536d767fbf2f16f1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:21 +0800 Subject: [PATCH 0039/1461] Port DraftingTableScreen.java to 1.21.1 --- .../client/screen/DraftingTableScreen.java | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java new file mode 100644 index 00000000..be430bf1 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java @@ -0,0 +1,326 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.workshop.WorkshopHomeScreen; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.network.DraftingTablePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.item.crafting.ShapedRecipe; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * The drafting table's recipe browser, {@code MCH_DraftingTableGui}. Pick a list (helicopters, + * planes, tanks, ground vehicles or equipment), step through its recipes, and Create makes the one + * shown into the output slot. The right page shows the original's illustrated description for + * equipment and a vehicle's numbers for vehicles; the original turned a 3D model there instead. + * + *

The recipes come from the client's recipe manager, which the server fills on join. + */ +public class DraftingTableScreen extends AbstractContainerScreen { + private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/drafting_table.png"); + private static final int TEXTURE_SIZE = 512; + /** The description pages take this part of their 512 square texture. */ + private static final int PAGE_WIDTH = 340; + private static final int PAGE_HEIGHT = 380; + private static final int PAGE_X = 210; + private static final int PAGE_Y = 20; + private static final int GRID_X = 121; + private static final int GRID_Y = 34; + private static final int TEXT = 0xFF6A3A00; + private static final int WHITE = 0xFFFFFFFF; + private static final String[] LISTS = {"helicopter", "plane", "vehicle", "tank", "item"}; + + private final List>> lists = new ArrayList<>(); + private int list; + private int index; + private int page; + private int pageCount; + private @Nullable Button create; + private @Nullable Button previous; + private @Nullable Button next; + private @Nullable Button previousPage; + private @Nullable Button nextPage; + + public DraftingTableScreen(DraftingTableMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 400; + imageHeight = 240; + inventoryLabelX = 30; + inventoryLabelY = 130; + } + + @Override + protected void init() { + super.init(); + collectRecipes(); + for (int i = 0; i < LISTS.length; i++) { + int which = i; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.list." + LISTS[i]), b -> selectList(which)) + .bounds(leftPos + 20, topPos + 20 + i * 20, 90, 20).build()); + } + create = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.create"), b -> create()) + .bounds(leftPos + 120, topPos + 89, 50, 20).build()); + previous = addRenderableWidget(Button.builder(Component.literal("<<"), b -> step(-1)).bounds(leftPos + 120, topPos + 111, 36, 20).build()); + next = addRenderableWidget(Button.builder(Component.literal(">>"), b -> step(1)).bounds(leftPos + 155, topPos + 111, 35, 20).build()); + previousPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.previous_page"), b -> turnPage(-1)) + .bounds(leftPos + 210, topPos + 210, 60, 20).build()); + nextPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.next_page"), b -> turnPage(1)) + .bounds(leftPos + 270, topPos + 210, 60, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.workshop"), b -> openWorkshop()) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.drafting_table.workshop_hint"))) + .bounds(leftPos + 334, topPos + 210, 56, 20).build()); + selectList(list); + } + + /** The workshop is a screen of its own and needs no block, so the table's menu closes first. */ + private void openWorkshop() { + minecraft.player.closeContainer(); + minecraft.setScreen(new WorkshopHomeScreen()); + } + + /** Every MCHeli crafting recipe, sorted into the five lists by what it makes. */ + private void collectRecipes() { + lists.clear(); + for (int i = 0; i < LISTS.length; i++) { + lists.add(new ArrayList<>()); + } + for (RecipeHolder holder : minecraft.level.getRecipeManager().getAllRecipesFor(RecipeType.CRAFTING)) { + ItemStack result = resultOf(holder); + if (holder.id().getNamespace().equals(MCHeli.MOD_ID) && !result.isEmpty() + && BuiltInRegistries.ITEM.getKey(result.getItem()).getNamespace().equals(MCHeli.MOD_ID)) { + lists.get(listOf(result.getItem())).add(holder); + } + } + for (List> entries : lists) { + entries.sort(Comparator.comparing(e -> resultOf(e).getHoverName().getString())); + } + } + + private ItemStack resultOf(RecipeHolder holder) { + return holder.value().getResultItem(minecraft.level.registryAccess()); + } + + private static int listOf(Item item) { + if (item == ModItems.HELICOPTER.get()) { + return 0; + } + if (item == ModItems.PLANE.get()) { + return 1; + } + if (item == ModItems.VEHICLE.get()) { + return 2; + } + if (item == ModItems.TANK.get()) { + return 3; + } + return 4; + } + + private void selectList(int which) { + list = which; + index = 0; + showRecipe(); + } + + private void step(int direction) { + int size = lists.get(list).size(); + if (size > 0) { + index = Math.floorMod(index + direction, size); + showRecipe(); + } + } + + private void turnPage(int direction) { + page = Math.clamp(page + direction, 0, Math.max(pageCount - 1, 0)); + updateButtons(); + } + + private void showRecipe() { + page = 0; + ItemStack result = result(); + pageCount = descriptionPages(result); + updateButtons(); + } + + private @Nullable RecipeHolder current() { + List> entries = lists.get(list); + return entries.isEmpty() ? null : entries.get(Math.min(index, entries.size() - 1)); + } + + private ItemStack result() { + RecipeHolder entry = current(); + return entry != null ? resultOf(entry) : ItemStack.EMPTY; + } + + /** The original's description pages for equipment: {@code drafting_table_desc/_.png}. */ + private int descriptionPages(ItemStack result) { + if (result.isEmpty() || AircraftItem.vehicleOf(result) != null) { + return 0; + } + String name = descriptionName(result); + int pages = 0; + while (pages < 10 && minecraft.getResourceManager().getResource(pageTexture(name, pages)).isPresent()) { + pages++; + } + return pages; + } + + private static String descriptionName(ItemStack result) { + if (result.getItem() == ModItems.THROWABLE.get()) { + String throwable = result.get(ModDataComponents.THROWABLE.get()); + return throwable != null ? throwable : "throwable"; + } + return BuiltInRegistries.ITEM.getKey(result.getItem()).getPath(); + } + + private static ResourceLocation pageTexture(String name, int page) { + return ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/drafting_table_desc/" + name + "_" + page + ".png"); + } + + private boolean canCreate() { + RecipeHolder entry = current(); + if (entry == null || menu.getSlot(menu.slots.size() - 1).hasItem() && !minecraft.player.getAbilities().instabuild) { + return false; + } + if (minecraft.player.getAbilities().instabuild) { + return true; + } + StackedContents contents = new StackedContents(); + minecraft.player.getInventory().fillStackedContents(contents); + return contents.canCraft(entry.value(), null); + } + + private void create() { + RecipeHolder entry = current(); + if (entry != null) { + ModNetwork.sendToServer(new DraftingTablePacket(entry.id())); + } + } + + private void updateButtons() { + if (create == null || previous == null || next == null || previousPage == null || nextPage == null) { + return; + } + boolean any = current() != null; + create.active = any && canCreate(); + previous.active = lists.get(list).size() > 1; + next.active = lists.get(list).size() > 1; + previousPage.visible = pageCount > 1; + nextPage.visible = pageCount > 1; + previousPage.active = page > 0; + nextPage.active = page < pageCount - 1; + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + renderTooltip(graphics, mouseX, mouseY); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + graphics.blit(BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, TEXTURE_SIZE, TEXTURE_SIZE); + if (pageCount > 0) { + String name = descriptionName(result()); + graphics.blit(pageTexture(name, page), leftPos + PAGE_X, topPos + PAGE_Y, PAGE_WIDTH / 2, PAGE_HEIGHT / 2, 0.0F, 0.0F, + PAGE_WIDTH, PAGE_HEIGHT, TEXTURE_SIZE, TEXTURE_SIZE); + } + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, TEXT, false); + RecipeHolder entry = current(); + if (entry == null) { + graphics.drawString(font, Component.translatable("gui.mcheli.drafting_table.empty"), 120, 20, TEXT, false); + return; + } + ItemStack result = result(); + graphics.drawString(font, font.plainSubstrByWidth(result.getHoverName().getString(), 85), 120, 20, WHITE, true); + graphics.drawString(font, (index + 1) + " / " + lists.get(list).size(), 120, 134, TEXT, false); + ingredients(graphics, entry.value(), mouseX, mouseY); + graphics.renderItem(result, 178, 52); + if (pageCount == 0) { + details(graphics, result); + } + } + + /** {@code drawItemRecipe}: the 3x3 grid, each slot cycling through what it accepts once a second. */ + private void ingredients(GuiGraphics graphics, CraftingRecipe recipe, int mouseX, int mouseY) { + List slots = recipe.getIngredients(); + int width = recipe instanceof ShapedRecipe shaped ? shaped.getWidth() : 3; + int tick = (int) (minecraft.level.getGameTime() / 20); + for (int i = 0; i < slots.size() && i < 9; i++) { + ItemStack[] options = slots.get(i).getItems(); + if (options.length == 0) { + continue; + } + ItemStack stack = options[tick % options.length]; + int x = GRID_X + (i % width) * 18; + int y = GRID_Y + (i / width) * 18; + graphics.renderItem(stack, x, y); + if (mouseX - leftPos >= x && mouseX - leftPos < x + 16 && mouseY - topPos >= y && mouseY - topPos < y + 16) { + graphics.renderTooltip(font, stack, mouseX - leftPos, mouseY - topPos); + } + } + } + + /** The vehicle's numbers, where the original turned its model. */ + private void details(GuiGraphics graphics, ItemStack result) { + String name = AircraftItem.vehicleOf(result); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + return; + } + List lines = new ArrayList<>(); + lines.add(Component.translatable("gui.mcheli.drafting_table.hp", def.maxHp)); + lines.add(Component.translatable("gui.mcheli.drafting_table.speed", "%.2f".formatted(def.speed))); + lines.add(Component.translatable("gui.mcheli.drafting_table.seats", def.seatCount())); + lines.add(Component.translatable("gui.mcheli.drafting_table.fuel", def.maxFuel)); + lines.add(Component.translatable("gui.mcheli.drafting_table.weapons")); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + String weaponName = weapon != null && weapon.displayName != null ? weapon.displayName : group.weaponType(); + lines.add(Component.literal(" " + weaponName + (group.mounts().size() > 1 ? " x" + group.mounts().size() : ""))); + } + int y = PAGE_Y + 20; + for (Component line : lines) { + if (y > PAGE_Y + 180) { + break; + } + graphics.drawString(font, font.split(line, 170).getFirst(), PAGE_X, y, TEXT, false); + y += 10; + } + } +} From dae4cceadad7ceee2ba9cd6010ddbb1bcb69f246 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:21 +0800 Subject: [PATCH 0040/1461] Port TeamManagerScreen.java to 1.21.1 --- .../client/screen/TeamManagerScreen.java | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java new file mode 100644 index 00000000..3ec2606f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java @@ -0,0 +1,136 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.TeamPacket; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.scores.PlayerTeam; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * The team manager, {@code MCH_GuiScoreboard_Main} and {@code MCH_GuiScoreboard_CreateTeam}, for + * operators setting up a match: switch PvP, shuffle everyone into the teams, make a team, send + * everyone to their spawn point and clear away every vehicle. The teams and their players are + * listed on the right. + */ +public class TeamManagerScreen extends Screen { + private static final int WIDTH = 150; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + + private @Nullable EditBox name; + private int color = TeamManager.COLORS.indexOf(ChatFormatting.RED); + private boolean friendlyFire; + private @Nullable Button colorButton; + private @Nullable Button friendlyFireButton; + private @Nullable Button createButton; + + public TeamManagerScreen() { + super(Component.translatable("gui.mcheli.team.title")); + } + + @Override + protected void init() { + int left = width / 2 - WIDTH - 10; + int y = 40; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.pvp"), b -> send(TeamPacket.Action.PVP)) + .bounds(left, y, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.shuffle"), b -> send(TeamPacket.Action.SHUFFLE)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.shuffle_hint"))) + .bounds(left, y + 22, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.jump_spawn"), b -> send(TeamPacket.Action.JUMP_TO_SPAWN)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.jump_spawn_hint"))) + .bounds(left, y + 44, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.destroy_all"), b -> send(TeamPacket.Action.DESTROY_ALL)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.destroy_all_hint"))) + .bounds(left, y + 66, WIDTH, 20).build()); + + int create = y + 104; + name = addRenderableWidget(new EditBox(font, left, create, WIDTH, 20, Component.translatable("gui.mcheli.team.name"))); + name.setMaxLength(16); + name.setHint(Component.translatable("gui.mcheli.team.name")); + name.setResponder(text -> updateButtons()); + colorButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + color = (color + 1) % TeamManager.COLORS.size(); + updateButtons(); + }).bounds(left, create + 22, WIDTH, 20).build()); + friendlyFireButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + friendlyFire = !friendlyFire; + updateButtons(); + }).bounds(left, create + 44, WIDTH, 20).build()); + createButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.create"), b -> { + ModNetwork.sendToServer(new TeamPacket(TeamPacket.Action.CREATE_TEAM, name.getValue(), color, friendlyFire)); + name.setValue(""); + }).bounds(left, create + 66, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 - 50, height - 28, 100, 20).build()); + updateButtons(); + } + + private void send(TeamPacket.Action action) { + ModNetwork.sendToServer(new TeamPacket(action)); + } + + private void updateButtons() { + if (colorButton == null || friendlyFireButton == null || createButton == null || name == null) { + return; + } + ChatFormatting teamColor = TeamManager.COLORS.get(color); + colorButton.setMessage(Component.translatable("gui.mcheli.team.color", + Component.literal(teamColor.getName()).withStyle(teamColor))); + friendlyFireButton.setMessage(Component.translatable(friendlyFire ? "gui.mcheli.team.friendly_fire_on" : "gui.mcheli.team.friendly_fire_off")); + String value = name.getValue().trim(); + createButton.active = !value.isEmpty() && !value.contains(" "); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 15, TEXT); + int left = width / 2 - WIDTH - 10; + graphics.drawString(font, Component.translatable("gui.mcheli.team.new_team"), left, 132, DIM); + teams(graphics, width / 2 + 10, 40); + } + + /** The scoreboard's teams and who is in them, as this client sees the scoreboard. */ + private void teams(GuiGraphics graphics, int x, int y) { + if (minecraft.level == null) { + return; + } + List teams = new ArrayList<>(minecraft.level.getScoreboard().getPlayerTeams()); + if (teams.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.team.none"), x, y, DIM); + return; + } + int bottom = height - 40; + for (PlayerTeam team : teams) { + if (y > bottom) { + break; + } + graphics.drawString(font, team.getFormattedDisplayName(), x, y, TEXT); + y += 11; + for (String player : team.getPlayers()) { + if (y > bottom) { + break; + } + graphics.drawString(font, " " + player, x, y, DIM); + y += 10; + } + y += 4; + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From fb4d7e282194a88918f1208762289fd032276974 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:22 +0800 Subject: [PATCH 0041/1461] Port UavStationScreen.java to 1.21.1 --- .../client/screen/UavStationScreen.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java new file mode 100644 index 00000000..c70fc4bc --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java @@ -0,0 +1,100 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_GuiUavStation}: the slot for the drone, the launch offset + * per axis with its buttons, and Continue to take the last drone flown from here again. + */ +public class UavStationScreen extends AbstractContainerScreen { + private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/uav_station.png"); + private static final String[] AXES = {"X", "Y", "Z"}; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF0000; + + private @Nullable Button continueButton; + + public UavStationScreen(UavStationMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = 166; + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + int x = width / 2 - 5; + int y = height / 2 - 76; + for (int axis = 0; axis < 3; axis++) { + for (int step = 0; step < UavStationMenu.STEPS.length; step++) { + int s = UavStationMenu.STEPS[step]; + int id = axis * 4 + step; + addRenderableWidget(Button.builder(Component.literal((s > 0 ? "+" : "") + s), b -> press(id)) + .bounds(x + step * 20, y + axis * 22, 20, 20).build()); + } + } + continueButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.uav_station.continue"), b -> press(UavStationMenu.BUTTON_CONTINUE)) + .bounds(x - 77, y + 44, 50, 20).build()); + continueButton.active = menu.canContinue(); + } + + @Override + protected void containerTick() { + super.containerTick(); + if (continueButton != null) { + continueButton.active = menu.canContinue(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + renderTooltip(graphics, mouseX, mouseY); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + graphics.blit(BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, 256, 256); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + UavStationEntity station = menu.station(); + ItemStack stack = menu.getSlot(0).getItem(); + AircraftDefinition def = stack.isEmpty() ? null : UavStationEntity.definitionOf(stack); + Component label; + int color = WHITE; + if (!stack.isEmpty() && (def == null || !def.uav)) { + label = Component.translatable("gui.mcheli.uav_station.not_uav"); + color = RED; + } else if (station != null && station.kind() == UavStationEntity.PORTABLE && def != null && !def.smallUav) { + label = Component.translatable("gui.mcheli.uav_station.small_only"); + color = RED; + } else { + label = title; + } + graphics.drawString(font, label, 8, 6, color, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, WHITE, false); + for (int axis = 0; axis < 3; axis++) { + graphics.drawString(font, String.format("%s.%+d", AXES[axis], menu.offset(axis)), 58, 15 + axis * 22, WHITE, false); + } + } +} From 8146da8880470154129e701c6e39de6d1f359e92 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:22 +0800 Subject: [PATCH 0042/1461] Port VehicleScreen.java to 1.21.1 --- .../mcheli/client/screen/VehicleScreen.java | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java new file mode 100644 index 00000000..e4e69d52 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java @@ -0,0 +1,186 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGui}: fuel and parachute slots on the left, the weapon + * panel on the right with its reload cost and button, and a button into the cargo chest for + * vehicles that have one. The panel is drawn from the chest texture so it looks like vanilla. + */ +public class VehicleScreen extends AbstractContainerScreen { + private static final ResourceLocation CHEST = ResourceLocation.withDefaultNamespace("textures/gui/container/generic_54.png"); + private static final ResourceLocation SLOT = ResourceLocation.withDefaultNamespace("container/slot"); + /** Height of the part above the player inventory. */ + private static final int TOP = 88; + private static final int PANEL_X = 72; + private static final int PANEL_WIDTH = 96; + private static final int LABEL = 0xFF404040; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF5555; + private static final int GREEN = 0xFF55FF55; + + private int weapon; + private boolean cargoButton; + private @Nullable Button reload; + private @Nullable Button previous; + private @Nullable Button next; + + public VehicleScreen(VehicleMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = TOP + 96; + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + previous = addRenderableWidget(Button.builder(Component.literal("<"), b -> step(-1)) + .bounds(leftPos + PANEL_X, topPos + 68, 16, 16).build()); + reload = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.reload"), b -> press(weapon)) + .bounds(leftPos + PANEL_X + 18, topPos + 68, PANEL_WIDTH - 36, 16).build()); + next = addRenderableWidget(Button.builder(Component.literal(">"), b -> step(1)) + .bounds(leftPos + PANEL_X + PANEL_WIDTH - 16, topPos + 68, 16, 16).build()); + AircraftEntity vehicle = menu.vehicle(); + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + cargoButton = def != null && def.cargoRows() > 0; + if (cargoButton) { + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.cargo"), b -> press(VehicleMenu.BUTTON_CARGO)) + .bounds(leftPos + imageWidth - 52, topPos + 3, 46, 13).build()); + } + updateButtons(); + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + private int weaponCount() { + AircraftEntity vehicle = menu.vehicle(); + VehicleWeapons weapons = vehicle != null ? vehicle.weapons() : null; + return weapons != null ? Math.min(weapons.sets().size(), VehicleMenu.MAX_WEAPONS) : 0; + } + + private void step(int direction) { + int count = weaponCount(); + if (count > 0) { + weapon = Math.floorMod(weapon + direction, count); + updateButtons(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + private void updateButtons() { + if (reload == null || previous == null || next == null) { + return; + } + int count = weaponCount(); + reload.visible = count > 0; + previous.visible = count > 1; + next.visible = count > 1; + reload.active = menu.canReload(weapon); + AircraftEntity vehicle = menu.vehicle(); + boolean grounded = vehicle != null && vehicle.canSupply(); + reload.setTooltip(grounded ? null : Tooltip.create(Component.translatable("gui.mcheli.vehicle.reload_hint"))); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + renderTooltip(graphics, mouseX, mouseY); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + int x = leftPos; + int y = topPos; + // The chest's title bar, its plain middle stretched down to the inventory, then the inventory. + graphics.blit(CHEST, x, y, 0.0F, 0.0F, imageWidth, 17, 256, 256); + graphics.blit(CHEST, x, y + 17, imageWidth, TOP - 17, 0.0F, 12.0F, imageWidth, 1, 256, 256); + graphics.blit(CHEST, x, y + TOP, 0.0F, 126.0F, imageWidth, 96, 256, 256); + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + graphics.blitSprite(SLOT, x + VehicleMenu.FUEL_X + i * 18 - 1, y + VehicleMenu.FUEL_Y - 1, 18, 18); + } + for (int i = 0; i < menu.parachuteSlots(); i++) { + graphics.blitSprite(SLOT, x + VehicleMenu.PARACHUTE_X + i * 18 - 1, y + VehicleMenu.PARACHUTE_Y - 1, 18, 18); + } + graphics.fill(x + PANEL_X - 2, y + 17, x + PANEL_X + PANEL_WIDTH + 2, y + 66, 0xFF373737); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + AircraftEntity vehicle = menu.vehicle(); + // Long names only give way to the Cargo button, where there is one. + int titleWidth = cargoButton ? imageWidth - 64 : imageWidth - 16; + graphics.drawString(font, font.plainSubstrByWidth(title.getString(), titleWidth), titleLabelX, titleLabelY, LABEL, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, LABEL, false); + if (vehicle == null) { + return; + } + if (vehicle.maxFuel() > 0) { + // A percentage and a gauge: the exact units would run into the weapon panel. + int percent = Math.round(vehicle.fuelRatio() * 100.0F); + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.fuel", percent), VehicleMenu.FUEL_X, VehicleMenu.FUEL_Y - 11, LABEL, false); + int gaugeY = VehicleMenu.FUEL_Y + 18; + int width = VehicleInventory.FUEL_SLOTS * 18 - 2; + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width, gaugeY + 3, 0xFF373737); + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width * percent / 100, gaugeY + 3, percent <= 10 ? RED : GREEN); + } + if (menu.parachuteSlots() > 0) { + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.parachute"), VehicleMenu.PARACHUTE_X, VehicleMenu.PARACHUTE_Y - 11, LABEL, false); + } + weaponPanel(graphics, vehicle); + } + + /** Name, rounds carried against the maximum, and what one reload costs. */ + private void weaponPanel(GuiGraphics graphics, AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null && weaponCount() > 0 ? weapons.set(Math.min(weapon, weaponCount() - 1)) : null; + if (set == null) { + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.no_weapons"), PANEL_X, 20, WHITE, false); + return; + } + graphics.drawString(font, font.plainSubstrByWidth(set.displayName(), PANEL_WIDTH), PANEL_X, 20, WHITE, false); + int rounds = menu.rounds(weapon); + int max = set.maxAmmo(); + int color = rounds == 0 && max > 0 ? RED : rounds >= max ? GREEN : WHITE; + graphics.drawString(font, rounds + " / " + max, PANEL_X, 31, color, false); + int itemX = PANEL_X; + for (RoundItem cost : set.definition().roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null) { + continue; + } + graphics.renderItem(new ItemStack(item), itemX, 42); + graphics.drawString(font, String.valueOf(cost.count()), itemX + 17, 47, WHITE, false); + itemX += 32; + if (itemX > PANEL_X + PANEL_WIDTH - 16) { + break; + } + } + } +} From 414dc43ed93326ec6e9495f978a71dce2a9b1eeb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:23 +0800 Subject: [PATCH 0043/1461] Port OneShotSound.java to 1.21.1 --- .../mcheli/client/sound/OneShotSound.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java new file mode 100644 index 00000000..31825a4f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java @@ -0,0 +1,22 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** A sound from a content pack played once at a point, such as a gun firing. */ +public final class OneShotSound extends RuntimeSoundInstance { + public OneShotSound(String sound, double x, double y, double z, float volume, float pitch) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.x = x; + this.y = y; + this.z = z; + this.volume = volume; + this.pitch = pitch; + } + + @Override + public void tick() { + // It stays where it was started and ends when the file does. + } +} From fa11778c17c64d2771d65977f52540ed61371eee Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:23 +0800 Subject: [PATCH 0044/1461] Port WorkshopClient.java to 1.21.1 --- .../client/workshop/WorkshopClient.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java new file mode 100644 index 00000000..26401727 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSubmitPacket; +import com.jnhprojects.mcheli.network.TestFlightPacket; +import net.minecraft.client.Minecraft; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; + +/** + * What the workshop does beyond its screens: takes the player to a test flight and back, and sends + * a pack to the server. The screen stays alive during a flight, so the undo history and the view are + * as they were when the pilot gets out. + */ +public final class WorkshopClient { + /** If no vehicle has come after this long, the server turned the flight down and said why. */ + private static final int SPAWN_TIMEOUT_TICKS = 100; + /** About 1.2 MB a second, so a download-sized pack does not crowd out the game's own packets. */ + private static final int SUBMIT_CHUNKS_PER_TICK = 2; + + private static @Nullable WorkshopScreen waiting; + private static boolean airborne; + private static int ticks; + private static byte @Nullable [] submitting; + private static int submitted; + + private WorkshopClient() { + } + + static void startTestFlight(WorkshopScreen screen, String vehicle) { + waiting = screen; + airborne = false; + ticks = 0; + Minecraft.getInstance().setScreen(null); + ModNetwork.sendToServer(new TestFlightPacket(vehicle)); + } + + /** Starts sending a pack's zip to the server, a few chunks a tick. */ + static void submit(byte[] zip) { + submitting = zip; + submitted = 0; + } + + /** How far along the zip being sent is, from 0 to 100, or -1 when none is. */ + static int submitProgress() { + byte[] zip = submitting; + return zip != null ? (int) (submitted * 100L / zip.length) : -1; + } + + public static void tick() { + tickSubmit(); + WorkshopScreen screen = waiting; + Minecraft mc = Minecraft.getInstance(); + if (screen == null || mc.player == null) { + return; + } + boolean flying = mc.player.getVehicle() instanceof AircraftEntity; + airborne |= flying; + ticks++; + if (airborne ? !flying : ticks > SPAWN_TIMEOUT_TICKS) { + waiting = null; + // Not over another screen the player has opened in flight, such as the pause menu. + if (mc.screen == null) { + mc.setScreen(screen); + } + } + } + + private static void tickSubmit() { + byte[] zip = submitting; + if (zip == null) { + return; + } + for (int i = 0; i < SUBMIT_CHUNKS_PER_TICK && submitted < zip.length; i++) { + int end = Math.min(submitted + PackSubmitPacket.CHUNK_SIZE, zip.length); + ModNetwork.sendToServer(new PackSubmitPacket(submitted, zip.length, Arrays.copyOfRange(zip, submitted, end))); + submitted = end; + } + if (submitted >= zip.length) { + submitting = null; + } + } + + public static void loggedOut() { + submitting = null; + WorkshopScreen screen = waiting; + waiting = null; + if (screen != null) { + screen.releaseAssets(); + } + } +} From 4882db40b6d35f25bbce169f5a9cf5a18c949202 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:24 +0800 Subject: [PATCH 0045/1461] Port WorkshopHomeScreen.java to 1.21.1 --- .../client/workshop/WorkshopHomeScreen.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java new file mode 100644 index 00000000..cc44546a --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java @@ -0,0 +1,201 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.workshop.ModelImport; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopKind; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Where the workshop starts: the vehicles in the content packs on the left, ready to open, and on + * the right a form to start a new one from an OBJ file, which can be dropped on the window. + */ +public class WorkshopHomeScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int FORM_WIDTH = 190; + private static final int MAX_LIST_WIDTH = 300; + private static final int LINE_HEIGHT = 10; + + private final List entries; + private int scroll; + private WorkshopKind kind = WorkshopKind.HELICOPTER; + private String vehicleText = ""; + private String packText = ""; + private String modelText = ""; + private @Nullable Component error; + private @Nullable EditBox vehicleBox; + private @Nullable EditBox packBox; + private @Nullable EditBox modelBox; + + public WorkshopHomeScreen() { + super(Component.translatable("gui.mcheli.workshop.title")); + entries = WorkshopProject.list(MCHeli.packs().folder()); + } + + @Override + protected void init() { + listRows(); + form(); + } + + private int listWidth() { + return Math.clamp(width - FORM_WIDTH - 30, 100, MAX_LIST_WIDTH); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + private void listRows() { + int rows = visibleRows(); + for (int i = scroll; i < entries.size() && i < scroll + rows; i++) { + WorkshopProject.Entry entry = entries.get(i); + String suffix = entry.zipped() ? " (zip)" : ""; + addRenderableWidget(Button.builder(Component.literal(entry.pack() + " / " + entry.vehicle() + suffix), b -> open(entry)) + .bounds(10, TOP + (i - scroll) * ROW, listWidth(), 20).build()); + } + } + + private void form() { + int x = width - FORM_WIDTH - 10; + int y = TOP; + vehicleBox = box(x, y + 10, Component.translatable("gui.mcheli.workshop.new.vehicle"), vehicleText, 64); + vehicleBox.setResponder(text -> { + // The pack follows the vehicle's name until the player gives it its own. + if (packBox != null && packBox.getValue().equals(vehicleText)) { + packBox.setValue(text); + } + vehicleText = text; + }); + packBox = box(x, y + 44, Component.translatable("gui.mcheli.workshop.new.pack"), packText, 64); + packBox.setResponder(text -> packText = text); + addRenderableWidget(Button.builder(kindName(), b -> { + kind = WorkshopKind.values()[(kind.ordinal() + 1) % WorkshopKind.values().length]; + b.setMessage(kindName()); + }).bounds(x, y + 68, FORM_WIDTH, 20).build()); + modelBox = box(x, y + 102, Component.translatable("gui.mcheli.workshop.new.model"), modelText, 1024); + modelBox.setResponder(text -> modelText = text); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.new.create"), b -> create()) + .bounds(x, y + 126, FORM_WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.home.packs"), b -> minecraft.setScreen(new WorkshopPacksScreen())) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private EditBox box(int x, int y, Component name, String value, int maxLength) { + EditBox box = new EditBox(font, x, y, FORM_WIDTH, 18, name); + box.setMaxLength(maxLength); + box.setValue(value); + return addRenderableWidget(box); + } + + private Component kindName() { + return Component.translatable("gui.mcheli.workshop.new.kind", Component.translatable("gui.mcheli.workshop.kind." + kind.id())); + } + + private void open(WorkshopProject.Entry entry) { + try { + minecraft.setScreen(new WorkshopScreen(WorkshopProject.open(MCHeli.packs().folder(), entry))); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + private void create() { + String vehicle = vehicleText.trim(); + AircraftDefinition existing = MCHeli.content().any(vehicle); + if (existing != null && existing.pack().equals(ContentSource.ORIGINAL)) { + // The import's models come from a resource pack, which would win over this vehicle's. + error = Component.translatable("gui.mcheli.workshop.new.name_taken", vehicle); + return; + } + try { + Path obj = Path.of(modelText.trim().replace("\"", "")); + String author = minecraft.player != null ? minecraft.player.getGameProfile().getName() : ""; + WorkshopProject project = WorkshopProject.create(MCHeli.packs().folder(), packText.trim(), kind, vehicle, author, + obj, ModelImport.findTexture(obj)); + minecraft.setScreen(new WorkshopScreen(project)); + } catch (InvalidPathException e) { + error = Component.translatable("gui.mcheli.workshop.new.bad_path", modelText); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + /** A file dropped on the window fills in the form, its name standing in for the vehicle's. */ + @Override + public void onFilesDrop(List files) { + if (files.isEmpty() || modelBox == null || vehicleBox == null) { + return; + } + Path file = files.getFirst(); + modelBox.setValue(file.toString()); + if (vehicleText.isEmpty()) { + String name = file.getFileName().toString(); + int dot = name.lastIndexOf('.'); + vehicleBox.setValue((dot > 0 ? name.substring(0, dot) : name).toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "-")); + } + error = null; + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int max = Math.max(entries.size() - visibleRows(), 0); + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, max); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 10, TEXT); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.open"), 10, TOP - 12, DIM); + if (entries.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.none"), 10, TOP, DIM); + } + int x = width - FORM_WIDTH - 10; + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.new"), x, TOP - 12, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.vehicle"), x, TOP, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.pack"), x, TOP + 34, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.model"), x, TOP + 92, DIM); + int y = TOP + 150; + for (var line : font.split(Component.translatable("gui.mcheli.workshop.home.drop"), FORM_WIDTH)) { + graphics.drawString(font, line, x, y, DIM); + y += LINE_HEIGHT; + } + if (error != null) { + y += 2; + for (var line : font.split(error, FORM_WIDTH)) { + graphics.drawString(font, line, x, y, ERROR); + y += LINE_HEIGHT; + } + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From 767c6dbdee265e34419a6b152b555a3d972e90b7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:25 +0800 Subject: [PATCH 0046/1461] Port WorkshopPacksScreen.java to 1.21.1 --- .../client/workshop/WorkshopPacksScreen.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java new file mode 100644 index 00000000..b8abc314 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java @@ -0,0 +1,218 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.ClientPacks; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.workshop.PackCatalog; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import net.minecraft.Util; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * The package browser: the packs in {@code mcheli/packs} and those the server in use sent, who made + * them and what is in them. A zip someone gave the player can be dropped on it to install. + * + *

This only ever reads the packs and adds to the folder. Removing one is left to the file manager + * the Open folder button opens, since a pack can be the only copy of someone's work. + */ +public class WorkshopPacksScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int GOOD = 0xFF80E080; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int LIST_WIDTH = 170; + private static final int LINE_HEIGHT = 10; + private static final int VEHICLES_SHOWN = 8; + + /** A pack in the list, and where it came from. */ + private record Row(PackCatalog.Info info, boolean fromServer) { + } + + private final List rows = new ArrayList<>(); + private final List problems = new ArrayList<>(); + private int scroll; + private int selected; + private @Nullable Component message; + private int messageColor = DIM; + + public WorkshopPacksScreen() { + super(Component.translatable("gui.mcheli.workshop.packs.title")); + read(); + } + + private void read() { + rows.clear(); + problems.clear(); + for (ContentPack pack : ClientPacks.INSTANCE.serverPacks()) { + try { + rows.add(new Row(PackCatalog.info(pack), true)); + } catch (IOException e) { + problems.add(new PackProblem(pack.id(), "could not be read: " + e)); + } + } + PackCatalog.Listing own = PackCatalog.read(MCHeli.packs().folder()); + for (PackCatalog.Info info : own.packs()) { + // A pack the player has that the server also uses is listed once, as the server's. + if (rows.stream().noneMatch(row -> row.info().hash().equals(info.hash()))) { + rows.add(new Row(info, false)); + } + } + problems.addAll(own.problems()); + selected = Math.clamp(selected, 0, Math.max(rows.size() - 1, 0)); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + @Override + protected void init() { + scroll = Math.clamp(scroll, 0, Math.max(rows.size() - visibleRows(), 0)); + for (int i = scroll; i < rows.size() && i < scroll + visibleRows(); i++) { + int index = i; + Button row = addRenderableWidget(Button.builder(Component.literal(rows.get(i).info().manifest().name()), b -> { + selected = index; + rebuildWidgets(); + }).bounds(10, TOP + (i - scroll) * ROW, LIST_WIDTH, 20).build()); + row.active = i != selected; + } + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.packs.open_folder"), b -> openFolder()) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private void openFolder() { + Path folder = MCHeli.packs().folder(); + try { + Files.createDirectories(folder); + Util.getPlatform().openFile(folder.toFile()); + } catch (IOException e) { + say(Component.literal(e.toString()), ERROR); + } + } + + private void say(Component text, int color) { + message = text; + messageColor = color; + } + + @Override + public void onFilesDrop(List files) { + for (Path file : files) { + if (!file.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) { + say(Component.translatable("gui.mcheli.workshop.packs.not_zip", file.getFileName().toString()), ERROR); + continue; + } + try { + PackManifest installed = PackCatalog.install(file, MCHeli.packs().folder()); + say(Component.translatable("gui.mcheli.workshop.packs.installed", installed.name()), GOOD); + read(); + rebuildWidgets(); + } catch (WorkshopException e) { + say(Component.literal(e.getMessage()), ERROR); + } + } + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, Math.max(rows.size() - visibleRows(), 0)); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void onClose() { + minecraft.setScreen(new WorkshopHomeScreen()); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 10, TEXT); + if (rows.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.packs.none"), 10, TOP, DIM); + } + int x = 10 + LIST_WIDTH + 10; + int w = width - x - 10; + int y = TOP; + if (selected < rows.size()) { + y = details(graphics, rows.get(selected), x, y, w) + LINE_HEIGHT; + } + for (PackProblem problem : problems) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.problem", problem.pack(), problem.message()), x, y, w, ERROR); + } + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.drop"), x, y + 4, w, DIM); + if (message != null) { + wrapped(graphics, message, x, y + 4, w, messageColor); + } + } + + private int details(GuiGraphics graphics, Row row, int x, int y, int w) { + PackCatalog.Info info = row.info(); + PackManifest m = info.manifest(); + y = wrapped(graphics, Component.literal(m.name() + " " + m.version()), x, y, w, TEXT); + if (!m.author().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.author", m.author()), x, y, w, TEXT); + } + if (!m.description().isEmpty()) { + y = wrapped(graphics, Component.literal(m.description()), x, y + 2, w, DIM); + } + y += 4; + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.id", m.id()), x, y, w, DIM); + // A small pack is still something, and 0.0 MB reads as an empty one. + String megabytes = "%.1f".formatted(Math.max(info.size() / 1048576.0, 0.1)); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.size", megabytes, ContentPack.shortHash(info.hash())), x, y, w, DIM); + Component source = row.fromServer() ? Component.translatable("gui.mcheli.workshop.packs.from_server") + : Component.literal(info.source().getFileName().toString() + (info.zipped() ? "" : "/")); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.source", source), x, y, w, DIM); + y += 4; + if (!info.vehicles().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.vehicles", info.vehicles().size(), shortList(info.vehicles())), x, y, w, TEXT); + } + if (!info.weapons().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.weapons", info.weapons().size(), shortList(info.weapons())), x, y, w, TEXT); + } + return y; + } + + private static String shortList(List names) { + if (names.size() <= VEHICLES_SHOWN) { + return String.join(", ", names); + } + return String.join(", ", names.subList(0, VEHICLES_SHOWN)) + ", ..."; + } + + private int wrapped(GuiGraphics graphics, Component text, int x, int y, int w, int color) { + for (FormattedCharSequence line : font.split(text, w)) { + graphics.drawString(font, line, x, y, color); + y += LINE_HEIGHT; + } + return y; + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From ae0371e295d310fdcc63b694e2980f60ba07c347 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:25 +0800 Subject: [PATCH 0047/1461] Port WorkshopScreen.java to 1.21.1 --- .../client/workshop/WorkshopScreen.java | 1671 +++++++++++++++++ 1 file changed, 1671 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java new file mode 100644 index 00000000..c3a2fb46 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java @@ -0,0 +1,1671 @@ +package com.jnhprojects.mcheli.client.workshop; + +import net.minecraft.client.Minecraft; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import org.lwjgl.opengl.GL11; +import org.joml.Matrix4f; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.LightTexture; +import com.mojang.math.Axis; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.platform.Lighting; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.render.AircraftRenderState; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.workshop.AssetImport; +import com.jnhprojects.mcheli.workshop.EditHistory; +import com.jnhprojects.mcheli.workshop.PartField; +import com.jnhprojects.mcheli.workshop.PartType; +import com.jnhprojects.mcheli.workshop.TuningKey; +import com.jnhprojects.mcheli.workshop.WorkshopCamera; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopPart; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.TreeSet; +import java.util.function.Consumer; + +/** + * The workshop's editor. The vehicle turns in the middle, drawn by the game's own vehicle renderer + * through a GUI entity, so what shows is what will fly. The parts list is on the left, the values + * of what is selected on the right, and gizmos for every part lie over the model. + * + *

Gizmos are plain GUI drawing at the points {@link WorkshopCamera#project} gives. They always + * show, even when the part is inside the hull, which is where most seats and pivots are. + * + *

Left drag on empty space turns the view, right drag moves it, the wheel zooms. Dragging a + * handle moves the part in the plane of the screen and an arrow along its axis, snapping to a + * sixteenth of a block unless Shift is held. + */ +public class WorkshopScreen extends Screen { + private static final int LEFT = 112; + private static final int RIGHT = 144; + private static final int TOP = 26; + private static final int BOTTOM = 26; + private static final int LIST_ROW = 16; + private static final int FIELD_ROW = 20; + private static final int LABEL_ROW = 10; + + private static final int PANEL = 0xFF181818; + private static final int VIEW = 0xFF2A2F36; + private static final int GRID = 0xFF3A414A; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int WARNING = 0xFFFFC040; + private static final int INVALID = 0xFFFF6060; + private static final int X_COLOR = 0xFFFF5050; + private static final int Y_COLOR = 0xFF60E060; + private static final int Z_COLOR = 0xFF5090FF; + private static final int SELECTED = 0xFFFFFFFF; + private static final int HIT_BOX = 0xFF00E0FF; + private static final int AXIS = 0xFFFF60FF; + + private static final int HANDLE_RADIUS = 3; + private static final double PICK_DISTANCE = 6.0; + private static final double ARROW_LENGTH = 34.0; + private static final double SNAP = 16.0; + private static final int GRID_HALF_SIZE = 16; + private static final int GRID_STEP = 2; + private static final double ZOOM_STEP = 1.15; + + private static final int PREVIEW_SIZE = 64; + private static final int PLOT_HEIGHT = 56; + private static final float PITCH_SNAP = 20.0F; + private static final String DEFAULT_CURVE = "0:0.7, 0.25:0.85, 0.5:1, 0.75:1.15, 1:1.3"; + + private enum Tab { + PARTS, PAINT, SOUND, TUNING, PACK + } + + private enum Drag { + NONE, ORBIT, PAN, PLANE, AXIS_X, AXIS_Y, AXIS_Z, CURVE + } + + private record Label(Component text, int x, int y, int color) { + } + + private record WrappedLine(FormattedCharSequence text, int x, int y) { + } + + private record MergeKey(int line, String field, int component) { + } + + private final WorkshopProject project; + private final WorkshopCamera camera = new WorkshopCamera(); + private final EditHistory> history = new EditHistory<>(); + private final List

The original kept the player at the station and had the drone read its pilot from there. + * Here the pilot takes the drone's reserved seat for as long as they fly it, hidden and unhurt, + * so the drone uses the same controls, cameras, HUD and weapons as every other vehicle. Leaving + * the drone, or losing it, puts the pilot back beside the station. + */ +public class UavStationEntity extends Entity { + public static final int STATION = 1; + public static final int PORTABLE = 2; + /** Further out than this, measured flat, the link breaks and the drone is lost, as in the original. */ + public static final double RANGE = 123.0; + /** The launch offset goes up to this far from the station along each axis. */ + public static final int MAX_OFFSET = 50; + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_OPEN = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_OFFSET = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BLOCK_POS); + private static final EntityDataAccessor DATA_LINKED = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + + private final SimpleContainer slot = new SimpleContainer(1); + private @Nullable UUID lastUav; + /** The lid of the portable controller, 0 shut to 1 open. */ + private float cover; + private float previousCover; + + public UavStationEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, (byte) STATION); + builder.define(DATA_OPEN, true); + builder.define(DATA_OFFSET, new BlockPos(12, 2, 12)); + builder.define(DATA_LINKED, false); + } + + public int kind() { + return entityData.get(DATA_KIND); + } + + public void setKind(int kind) { + entityData.set(DATA_KIND, (byte) (kind == PORTABLE ? PORTABLE : STATION)); + } + + public boolean isOpen() { + return entityData.get(DATA_OPEN); + } + + /** Where a launched drone appears, relative to the station. */ + public BlockPos offset() { + return entityData.get(DATA_OFFSET); + } + + public void setOffset(BlockPos offset) { + entityData.set(DATA_OFFSET, new BlockPos(Mth.clamp(offset.getX(), -MAX_OFFSET, MAX_OFFSET), + Mth.clamp(offset.getY(), -MAX_OFFSET, MAX_OFFSET), Mth.clamp(offset.getZ(), -MAX_OFFSET, MAX_OFFSET))); + } + + /** Whether a drone is flown from here right now; the screen lights up. */ + public boolean isLinked() { + return entityData.get(DATA_LINKED); + } + + public void setLinked(boolean linked) { + entityData.set(DATA_LINKED, linked); + } + + public SimpleContainer slot() { + return slot; + } + + public float cover(float partialTicks) { + return Mth.lerp(partialTicks, previousCover, cover); + } + + /** + * {@code initUavPostion}: the first launch offset is twelve blocks out on the diagonal the + * station faces, two up. + */ + public void initOffset() { + int quarter = (int) (Mth.positiveModulo(getYRot() + 45.0F, 360.0F) / 90.0F) % 4; + int x = quarter == 0 || quarter == 3 ? 12 : -12; + int z = quarter == 0 || quarter == 1 ? 12 : -12; + setOffset(new BlockPos(x, 2, z)); + } + + @Override + public void tick() { + super.tick(); + previousCover = cover; + cover = Mth.clamp(cover + (isOpen() ? 0.1F : -0.1F), 0.0F, 1.0F); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.03, 0.0); + move(MoverType.SELF, new Vec3(0.0, m.y, 0.0)); + setDeltaMovement(0.0, onGround() ? 0.0 : m.y * 0.96, 0.0); + + Entity user = getFirstPassenger(); + if (user instanceof ServerPlayer player && !slot.getItem(0).isEmpty()) { + launch(player, slot.getItem(0)); + } + } + + /** What the slot holds, if it is a vehicle at all. */ + public static @Nullable AircraftDefinition definitionOf(ItemStack stack) { + String name = AircraftItem.vehicleOf(stack); + return name != null ? MCHeli.content().any(name) : null; + } + + /** Whether this station can put up the vehicle in {@code stack}. */ + public boolean accepts(@Nullable AircraftDefinition def) { + return def != null && def.uav && (kind() != PORTABLE || def.smallUav); + } + + /** {@code handleItem}: puts up the drone in the slot and hands it to {@code player}. */ + private void launch(ServerPlayer player, ItemStack stack) { + AircraftDefinition def = definitionOf(stack); + if (!accepts(def) || !(level() instanceof ServerLevel server)) { + return; + } + if (ServerConfig.isDisabled(def.name())) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName()), true); + return; + } + AircraftEntity uav = ModEntities.typeFor(def).create(server); + if (uav == null) { + return; + } + BlockPos o = offset(); + double y = Math.max(getY() + o.getY(), server.getMinBuildHeight() + 2.0); + uav.setType(def); + uav.moveTo(getX() + o.getX(), y, getZ() + o.getZ(), getYRot() - 180.0F, 0.0F); + if (!server.noCollision(uav, uav.getBoundingBox().deflate(0.1))) { + player.displayClientMessage(Component.translatable("message.mcheli.uav.blocked"), true); + slot.setItem(0, ItemStack.EMPTY); + Containers.dropItemStack(server, player.getX(), player.getY(), player.getZ(), stack); + return; + } + slot.setItem(0, ItemStack.EMPTY); + uav.setFuelLevel(def.targetDrone ? uav.maxFuel() : (int) (uav.maxFuel() * 0.05F)); + server.addFreshEntity(uav); + if (!def.targetDrone) { + player.closeContainer(); + control(player, uav); + } + } + + /** {@code controlLastAircraft}: takes the last drone flown from here again. */ + public boolean continueControl(ServerPlayer player) { + AircraftEntity uav = lastUav(); + if (uav == null || getFirstPassenger() != player || !uav.canTakeRemotePilot()) { + return false; + } + player.closeContainer(); + return control(player, uav); + } + + public @Nullable AircraftEntity lastUav() { + if (lastUav == null || !(level() instanceof ServerLevel server)) { + return null; + } + Entity e = server.getEntity(lastUav); + return e instanceof AircraftEntity uav && uav.isAlive() && !uav.isDestroyed() && inRange(uav) ? uav : null; + } + + public boolean inRange(Entity uav) { + double dx = uav.getX() - getX(); + double dz = uav.getZ() - getZ(); + return dx * dx + dz * dz <= RANGE * RANGE; + } + + private boolean control(ServerPlayer player, AircraftEntity uav) { + player.stopRiding(); + if (!uav.takeRemotePilot(player, this)) { + return false; + } + lastUav = uav.getUUID(); + setLinked(true); + player.setYRot(uav.getYRot()); + return true; + } + + /** Where a pilot comes back to when the drone is left or lost: the seat in front of the station. */ + public Vec3 returnPosition() { + float yaw = getYRot() * Mth.DEG_TO_RAD; + return new Vec3(getX() - Mth.sin(yaw) * 0.9, getY(), getZ() + Mth.cos(yaw) * 0.9); + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand) { + if (isVehicle()) { + return InteractionResult.PASS; + } + if (kind() == PORTABLE && player.isSecondaryUseActive()) { + if (!level().isClientSide()) { + entityData.set(DATA_OPEN, !isOpen()); + } + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive() || kind() == PORTABLE && !isOpen()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer server) { + player.setYRot(getYRot() - 180.0F); + if (player.startRiding(this)) { + server.openMenu(new SimpleMenuProvider((id, inventory, p) -> new UavStationMenu(id, inventory, this), + Component.translatable(kind() == PORTABLE ? "item.mcheli.uav_station2" : "item.mcheli.uav_station"))); + } + } + return InteractionResult.SUCCESS; + } + + /** The original sat the player down in front of the console, lower for the portable one. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + double y = kind() == PORTABLE ? -0.6 : 0.35; + return new Vec3(0.0, y, 0.9).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + @Override + public boolean hurt(DamageSource source, float damage) { + if (!(level() instanceof ServerLevel level)) { + return false; + } + if (isInvulnerableTo(source) || isRemoved() || damage <= 0.0F) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + AircraftEntity uav = lastUav(); + if (uav != null) { + uav.releaseRemotePilot(); + } + Containers.dropContents(level, blockPosition(), slot); + if (!creative) { + spawnAtLocation(item()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), source.getEntity() instanceof Player ? "hit" : "helidmg", 1.0F, 1.0F); + discard(); + return true; + } + + private Item item() { + return kind() == PORTABLE ? ModItems.UAV_CONTROLLER.get() : ModItems.UAV_STATION.get(); + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith() { + return true; + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(item()); + } + @Override + protected void readAdditionalSaveData(CompoundTag input) { + setKind((input.contains("Kind") ? input.getInt("Kind") : STATION)); + entityData.set(DATA_OPEN, (input.contains("Open") ? input.getBoolean("Open") : true)); + setOffset(new BlockPos((input.contains("UavX") ? input.getInt("UavX") : 12), (input.contains("UavY") ? input.getInt("UavY") : 2), (input.contains("UavZ") ? input.getInt("UavZ") : 12))); + lastUav = input.hasUUID("LastUav") ? input.getUUID("LastUav") : null; + slot.setItem(0, ItemStack.parseOptional(registryAccess(), input.getCompound("Item"))); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + output.putInt("Kind", kind()); + output.putBoolean("Open", isOpen()); + BlockPos o = offset(); + output.putInt("UavX", o.getX()); + output.putInt("UavY", o.getY()); + output.putInt("UavZ", o.getZ()); + if (lastUav != null) { + output.putUUID("LastUav", lastUav); + } + if (!slot.getItem(0).isEmpty()) { + output.put("Item", slot.getItem(0).save(registryAccess())); + } + } +} From e07dd0d25f3dc9ad57ccad19651f0400a4c6ef5b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:30 +0800 Subject: [PATCH 0055/1461] Port VehicleInventory.java to 1.21.1 --- .../mcheli/entity/VehicleInventory.java | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java new file mode 100644 index 00000000..5ea8dcd2 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java @@ -0,0 +1,144 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.supply.SupplyRules; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; + +/** + * What a vehicle carries besides its crew, {@code MCH_AircraftInventory} and the container half of + * {@code W_EntityContainer}: the equipment slots of the vehicle screen (three fuel cans and a + * parachute per ejection seat) and the cargo chest of vehicles with an {@code InventorySize}. + * + *

The cargo chest always has room for the largest size, 54 slots, and the screen shows as many + * rows as the definition asks for. Items in rows a content reload took away are kept rather than + * lost, and still drop when the vehicle is destroyed. + */ +public final class VehicleInventory { + public static final int FUEL_SLOTS = 3; + public static final int PARACHUTE_SLOTS = 2; + public static final int EQUIPMENT_SLOTS = FUEL_SLOTS + PARACHUTE_SLOTS; + public static final int CARGO_SLOTS = 54; + /** How far from the vehicle's centre a player outside may keep a screen open, past its width. */ + private static final double REACH = 12.0; + + private final AircraftEntity vehicle; + private final SimpleContainer equipment = new GuardedContainer(EQUIPMENT_SLOTS); + private final SimpleContainer cargo = new GuardedContainer(CARGO_SLOTS); + + VehicleInventory(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + public SimpleContainer equipment() { + return equipment; + } + + public SimpleContainer cargo() { + return cargo; + } + + /** {@code isUseableByPlayer}: the crew, or someone standing next to the vehicle. */ + public boolean stillValid(Player player) { + if (vehicle.isRemoved() || vehicle.definition() == null) { + return false; + } + if (player.getVehicle() == vehicle) { + return true; + } + double reach = REACH + vehicle.definition().bodyWidth / 2.0; + return player.distanceToSqr(vehicle) <= reach * reach; + } + + // Fuel + + /** + * {@code updateFuel}: every half second, while the vehicle stands on something, each fuel can + * in a fuel slot pours up to 100 units into the tank. Server only. + * + * @return whether any fuel went in + */ + boolean pourFuelCans(int ticks) { + if (ticks % SupplyRules.FUEL_SLOT_INTERVAL != 0 || !vehicle.canSupply()) { + return false; + } + boolean poured = false; + for (int slot = 0; slot < FUEL_SLOTS; slot++) { + ItemStack can = equipment.getItem(slot); + if (!(can.getItem() instanceof FuelItem)) { + continue; + } + int amount = SupplyRules.fuelFromCan(vehicle.maxFuel() - vehicle.fuel(), FuelItem.contents(can)); + if (amount > 0) { + vehicle.refuel(amount); + can.setDamageValue(can.getDamageValue() + amount); + poured = true; + } + } + if (poured) { + equipment.setChanged(); + } + return poured; + } + + // Parachutes + + /** Parachute slots the vehicle screen offers: one per ejection seat. */ + public static int parachuteSlots(AircraftDefinition def) { + return Math.min(def.ejectionSeats(), PARACHUTE_SLOTS); + } + + /** + * {@code consumeParachute}: takes one parachute out of the parachute slots for a crew member + * who bails out. + * + * @return false when there is none left + */ + boolean takeParachute() { + for (int slot = FUEL_SLOTS; slot < EQUIPMENT_SLOTS; slot++) { + if (equipment.getItem(slot).getItem() instanceof ParachuteItem) { + equipment.removeItem(slot, 1); + return true; + } + } + return false; + } + + // Saving and dropping + + void save(CompoundTag output) { + output.put("Equipment", ContainerHelper.saveAllItems(new CompoundTag(), equipment.getItems(), false, vehicle.registryAccess())); + output.put("Cargo", ContainerHelper.saveAllItems(new CompoundTag(), cargo.getItems(), false, vehicle.registryAccess())); + } + + void load(CompoundTag input) { + equipment.clearContent(); + cargo.clearContent(); + ContainerHelper.loadAllItems(input.getCompound("Equipment"), equipment.getItems(), vehicle.registryAccess()); + ContainerHelper.loadAllItems(input.getCompound("Cargo"), cargo.getItems(), vehicle.registryAccess()); + } + + /** {@code setDead}: everything aboard falls out where the vehicle was. Server only. */ + void dropAll() { + Containers.dropContents(vehicle.level(), vehicle, equipment); + Containers.dropContents(vehicle.level(), vehicle, cargo); + } + + /** A container that only stays open for players {@link #stillValid(Player)} allows. */ + private final class GuardedContainer extends SimpleContainer { + GuardedContainer(int size) { + super(size); + } + + @Override + public boolean stillValid(Player player) { + return VehicleInventory.this.stillValid(player); + } + } +} From 838a495bfed471d3a397b5ea9189c1e3d8a0989e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:30 +0800 Subject: [PATCH 0056/1461] Port VehicleParticles.java to 1.21.1 --- .../mcheli/entity/VehicleParticles.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java new file mode 100644 index 00000000..46ed88a8 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java @@ -0,0 +1,180 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.ParticleSplash; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; + +/** + * The spray, dust and exhaust a vehicle throws up, all client side: + *

    + *
  • {@code onUpdate_ParticleSandCloud}: a helicopter's downwash raises a ring of dust off the + * ground or water below it; boats and amphibians with {@code EnableSeaSurfaceParticle} raise + * one off the sea while under way.
  • + *
  • {@code onUpdate_ParticleSplash}: each {@code AddParticleSplash} point sprays while it moves + * through water.
  • + *
  • {@code onUpdate_ParticleNozzle}: jet nozzles smoke with the throttle, more when the blast + * hits the ground, as on a Harrier in the hover.
  • + *
  • {@code onUpdate_ParticleLandingGear}: a plane rolling fast kicks up bits of the ground.
  • + *
+ */ +final class VehicleParticles { + private static final int WHITE = 0xFFFFFF; + + private VehicleParticles() { + } + + static void tick(AircraftEntity vehicle, AircraftDefinition def) { + if (vehicle.isDestroyed()) { + return; + } + switch (vehicle) { + case HelicopterEntity heli -> sandCloud(heli, def, false); + case PlaneEntity plane -> { + if (def instanceof PlaneDefinition planeDef) { + nozzles(plane, planeDef); + } + landingGear(plane); + splashes(plane, def); + sandCloud(plane, def, true); + } + case TankEntity tank -> { + splashes(tank, def); + sandCloud(tank, def, true); + } + default -> { + } + } + } + + private static void sandCloud(AircraftEntity vehicle, AircraftDefinition def, boolean seaOnly) { + if (seaOnly && !def.seaSurfaceParticles || def.particlesScale <= 0.01F) { + return; + } + Level level = vehicle.level(); + RandomSource random = vehicle.getRandom(); + float scale = def.particlesScale * 3.0F * (seaOnly ? 2.0F : 1.0F); + double throttle = Math.min(vehicle.visibleThrottle() * 2.0, 1.0); + // A boat needs some water under it before it sprays; the original counted water blocks. + int count = seaOnly ? (int) (scale * 7.0F) : 0; + int range = (int) (scale * 10.0F) + 1; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + int bx = Mth.floor(vehicle.getX() + 0.5); + int by = Mth.floor(vehicle.getY() + 0.5); + int bz = Mth.floor(vehicle.getZ() + 0.5); + int y = 0; + double groundY = 0.0; + boolean found = false; + for (; y < range && !found; y++) { + for (int x = -1; x <= 1 && !found; x++) { + for (int z = -1; z <= 1 && !found; z++) { + BlockState state = level.getBlockState(pos.set(bx + x, by - y, bz + z)); + if (state.isAir()) { + continue; + } + if (seaOnly && state.getFluidState().is(FluidTags.WATER)) { + count--; + } + if (count <= 0) { + groundY = vehicle.getY() + 1.0 + scale / 5.0F - y; + found = true; + } + } + } + } + if (!found) { + return; + } + double density = (range - y + 1) / (5.0 * scale) / 2.0; + for (int k = 0; k < (int) (throttle * 6.0 * density); k++) { + float angle = random.nextFloat() * Mth.TWO_PI; + double dx = Mth.cos(angle); + double dz = Mth.sin(angle); + level.addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, scale * 5.0F, (int) (10.0F * scale) + 1), true, + vehicle.getX() + dx * scale * 3.0, groundY + (random.nextDouble() - 0.5) * scale, vehicle.getZ() + dz * scale * 3.0, + scale * dx * 0.3, scale * -0.02, scale * dz * 0.3); + } + } + + private static void splashes(AircraftEntity vehicle, AircraftDefinition def) { + if (def.splashes.isEmpty()) { + return; + } + double mx = vehicle.getX() - vehicle.xo; + double mz = vehicle.getZ() - vehicle.zo; + double dist = Math.min(mx * mx + mz * mz, 1.0); + RandomSource random = vehicle.getRandom(); + for (ParticleSplash splash : def.splashes) { + for (int i = 0; i < splash.count(); i++) { + if (dist > 0.03 + random.nextFloat() * 0.1) { + splash(vehicle, splash, -mx * splash.acceleration(), -mz * splash.acceleration(), splash.size() * (0.5 + dist * 0.5)); + } + } + } + } + + private static void splash(AircraftEntity vehicle, ParticleSplash splash, double mx, double mz, double size) { + RandomSource random = vehicle.getRandom(); + Vec3 at = vehicle.toWorld(splash.position()).add(random.nextDouble() - 0.5, (random.nextDouble() - 0.5) * 0.5, random.nextDouble() - 0.5); + BlockPos block = BlockPos.containing(at.x + 0.5, at.y, at.z + 0.5); + if (!vehicle.level().getFluidState(block).is(FluidTags.WATER)) { + return; + } + int age = splash.age() + (int) (random.nextFloat() * 0.5F * splash.age()); + vehicle.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, (float) size * (random.nextFloat() * 0.2F + 0.8F), age), true, + at.x, at.y, at.z, mx + (random.nextFloat() - 0.5) * 0.7, splash.motionY(), mz + (random.nextFloat() - 0.5) * 0.7); + } + + private static void nozzles(PlaneEntity plane, PlaneDefinition def) { + float throttle = plane.visibleThrottle(); + if (def.nozzles.isEmpty() || throttle <= 0.1F) { + return; + } + RandomSource random = plane.getRandom(); + PartState nozzle = plane.nozzle(); + float tilt = nozzle != null ? nozzle.angle() * Mth.DEG_TO_RAD : 0.0F; + // Out of the back, turned down as the nozzles swing towards the hover. + Vector3f blast = plane.toWorldOffset(new Vector3(0.0, -Mth.sin(tilt), -Mth.cos(tilt))); + for (MovingPart part : def.nozzles) { + if (random.nextFloat() > throttle * 1.5F) { + continue; + } + Vec3 at = plane.toWorld(part.position()).add(blast.x, blast.y, blast.z); + BlockPos ahead = BlockPos.containing(at.x + blast.x * 3.0, at.y + blast.y * 3.0, at.z + blast.z * 3.0); + float spread = plane.level().getBlockState(ahead).isAir() ? 0.7F : 2.0F; + plane.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, 5.0F * def.particlesScale, 30), true, at.x, at.y, at.z, + blast.x + (random.nextFloat() - 0.5F) * spread, blast.y, blast.z + (random.nextFloat() - 0.5F) * spread); + } + } + + private static void landingGear(PlaneEntity plane) { + double dx = plane.getX() - plane.xo; + double dz = plane.getZ() - plane.zo; + if (!plane.onGround() || dx * dx + dz * dz <= 0.01) { + return; + } + BlockPos below = BlockPos.containing(plane.getX(), plane.getY() - 0.5, plane.getZ()); + BlockState state = plane.level().getBlockState(below); + if (state.getRenderShape() == RenderShape.INVISIBLE) { + return; + } + RandomSource random = plane.getRandom(); + float width = plane.getBbWidth(); + plane.level().addParticle(new BlockParticleOption(ParticleTypes.BLOCK, state), + plane.getX() + (random.nextFloat() - 0.5) * width, plane.getBoundingBox().minY + 0.1, plane.getZ() + (random.nextFloat() - 0.5) * width, + -dx * 4.0, 1.5, -dz * 4.0); + } +} From 783f29a7bc59cbeba5efcd35455f0c65364e2c71 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:31 +0800 Subject: [PATCH 0057/1461] Port ThrowableEntity.java to 1.21.1 --- .../entity/projectile/ThrowableEntity.java | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java new file mode 100644 index 00000000..1f198726 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java @@ -0,0 +1,191 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.Explosions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.FastColor; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A thrown grenade, {@code MCH_EntityThrowable}. It flies under its definition's gravity, bounces + * off blocks losing speed, and when its fuse runs out either explodes or, for a smoke grenade, + * pours out coloured smoke until it disappears. + */ +public class ThrowableEntity extends Entity { + private static final EntityDataAccessor DATA_NAME = SynchedEntityData.defineId(ThrowableEntity.class, EntityDataSerializers.STRING); + + private @Nullable Entity thrower; + private @Nullable ThrowableDefinition definition; + private int definitionGeneration = -1; + + public ThrowableEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Throws {@code def} from {@code thrower}'s eyes the way it looks. + * + * @param strength 0.2 for a flick up to 1.4 for a full draw, the original's acceleration + */ + public static ThrowableEntity thrown(Level level, Entity thrower, ThrowableDefinition def, float strength) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + Vec3 look = thrower.getLookAngle(); + entity.setPos(thrower.getX() + look.x * 0.3, thrower.getEyeY() - 0.1, thrower.getZ() + look.z * 0.3); + entity.setYRot(thrower.getYRot()); + entity.setDeltaMovement(look.scale(1.5 * strength).add(thrower.getDeltaMovement())); + entity.thrower = thrower; + entity.setDefinition(def); + return entity; + } + + /** One fired from a dispenser: no thrower, straight along {@code direction}. */ + public static ThrowableEntity dispensed(Level level, Vec3 at, Vec3 direction, ThrowableDefinition def) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + entity.setPos(at); + entity.setDeltaMovement(direction.normalize().scale(def.dispenseAcceleration)); + entity.setDefinition(def); + return entity; + } + + private void setDefinition(ThrowableDefinition def) { + entityData.set(DATA_NAME, def.name()); + definition = def; + definitionGeneration = MCHeli.content().generation(); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_NAME, ""); + } + + /** The definition by name, looked up again after a content reload. */ + public @Nullable ThrowableDefinition definition() { + if (definitionGeneration != MCHeli.content().generation()) { + definition = MCHeli.content().throwable(entityData.get(DATA_NAME)); + definitionGeneration = MCHeli.content().generation(); + } + return definition; + } + + public String definitionName() { + return entityData.get(DATA_NAME); + } + + @Override + public void tick() { + super.tick(); + ThrowableDefinition def = definition(); + if (def == null) { + // The original gave a missing definition ten ticks to arrive. + if (tickCount > 10 && !level().isClientSide()) { + discard(); + } + return; + } + if (level().isClientSide()) { + if (def.smokes() && tickCount >= def.timeFuse) { + smoke(def); + } + return; + } + if (tickCount >= def.timeFuse && !def.smokes()) { + Explosions.explode((ServerLevel) level(), getX(), getY(), getZ(), new Explosions.Params(this, thrower, + def.explosion, def.explosion, false, true, 0, isInWater(), null)); + discard(); + return; + } + if (tickCount >= def.aliveTime) { + discard(); + return; + } + fly(def); + } + + /** {@code onUpdate} and {@code boundBullet}: gravity of its own, and a bounce off whatever it hits. */ + private void fly(ThrowableDefinition def) { + boolean water = isInWater(); + Vec3 m = getDeltaMovement().add(0.0, water ? def.gravityInWater : def.gravity, 0.0); + Vec3 from = position(); + move(MoverType.SELF, m); + // Whichever axis came up short hit something. + Vec3 moved = position().subtract(from); + double mx = blocked(moved.x, m.x) ? -m.x * def.bound : m.x; + double my = m.y; + double mz = blocked(moved.z, m.z) ? -m.z * def.bound : m.z; + if (blocked(moved.y, m.y)) { + mx *= 0.9; + mz *= 0.9; + // Off the floor or the ceiling it keeps a share of its speed, unless that would be a crawl. + my = Math.abs(my) > 0.05 ? -my * def.bound : 0.0; + } + double drag = water ? 0.8 : 0.99; + setDeltaMovement(mx * drag, my * drag, mz * drag); + } + + private static boolean blocked(double moved, double wanted) { + return Math.abs(moved) < Math.abs(wanted) - 1.0E-6; + } + + private void smoke(ThrowableDefinition def) { + int color = def.smokeColor; + for (int i = 0; i < def.smokeNum; i++) { + // A little noise per puff, keeping greys grey. + float r = (FastColor.ARGB32.red(color) / 255.0F) * 0.9F + random.nextFloat() * 0.1F; + float g = (FastColor.ARGB32.green(color) / 255.0F) * 0.9F + random.nextFloat() * 0.1F; + float b = (FastColor.ARGB32.blue(color) / 255.0F) * 0.9F + random.nextFloat() * 0.1F; + if (FastColor.ARGB32.red(color) == FastColor.ARGB32.green(color)) { + g = r; + } + if (FastColor.ARGB32.red(color) == FastColor.ARGB32.blue(color)) { + b = r; + } + if (FastColor.ARGB32.green(color) == FastColor.ARGB32.blue(color)) { + b = g; + } + float size = def.smokeSize + random.nextFloat() * def.smokeSize / 3.0F; + level().addAlwaysVisibleParticle(new SmokeParticleOptions(FastColor.ARGB32.colorFromFloat(1.0F, r, g, b) & 0xFFFFFF, size), true, + getX(), getY() + 0.5, getZ(), + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2, + def.smokeVelocityVertical * random.nextFloat() * 0.2, + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2); + } + } + + /** The server moves it; clients ease towards each position it sends. */ + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From 297af095c9e79685f85139e3e9a1a8edf5ac1b76 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:31 +0800 Subject: [PATCH 0058/1461] Port ChainItem.java to 1.21.1 --- .../jnhprojects/mcheli/item/ChainItem.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java new file mode 100644 index 00000000..0f89341b --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java @@ -0,0 +1,85 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.ChainEntity; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; + +import java.util.Map; +import java.util.UUID; +import java.util.WeakHashMap; + +/** + * The tow chain, {@code MCH_ItemChain}. Use it on both ends, in either order, two to sixteen + * blocks apart: the vehicle pulls, so one end must be a vehicle. Use it on either end of a chain + * to take the chain off, or on the hooked end again to let go of it. Vehicles and containers + * hand the click over here, and every step is told to the player above the hotbar. + */ +public class ChainItem extends Item { + private static final double MIN_DISTANCE = 2.0; + private static final double MAX_DISTANCE = 16.0; + + /** The first end each player has hooked, until they hook the second. Server only. */ + private static final Map HOOKED = new WeakHashMap<>(); + + public ChainItem(Properties properties) { + super(properties.stacksTo(1)); + } + + /** A click with the chain on {@code target}. */ + public static InteractionResult useOn(Entity target, Player player) { + if (!(player.level() instanceof ServerLevel level)) { + return InteractionResult.SUCCESS; + } + ChainEntity existing = ChainEntity.on(target); + if (existing != null) { + MCHeli.LOGGER.debug("Chain: {} takes the chain off {}", player.getName().getString(), target); + existing.disconnect(); + HOOKED.remove(player); + player.displayClientMessage(Component.translatable("message.mcheli.chain.removed"), true); + return InteractionResult.SUCCESS; + } + UUID firstId = HOOKED.get(player); + Entity first = firstId != null ? level.getEntity(firstId) : null; + // An end hooked far away was most likely forgotten: start over from this one. + if (first == null || first.isRemoved() || target.distanceTo(first) > MAX_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} hooks {}", player.getName().getString(), target); + HOOKED.put(player, target.getUUID()); + ModSounds.playAt(target, "chain", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.hooked", target.getDisplayName()), true); + return InteractionResult.SUCCESS; + } + if (first == target) { + MCHeli.LOGGER.debug("Chain: {} lets go of {}", player.getName().getString(), target); + HOOKED.remove(player); + player.displayClientMessage(Component.translatable("message.mcheli.chain.released"), true); + return InteractionResult.SUCCESS; + } + double distance = target.distanceTo(first); + if (distance < MIN_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} and {} are only {} blocks apart", first, target, distance); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.too_close", String.format("%.1f", distance)), true); + return InteractionResult.FAIL; + } + if (!ChainEntity.canTow(first) && !ChainEntity.canTow(target)) { + MCHeli.LOGGER.debug("Chain: neither {} nor {} can tow", first, target); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.no_vehicle"), true); + return InteractionResult.FAIL; + } + MCHeli.LOGGER.debug("Chain: {} chains {} and {}", player.getName().getString(), first, target); + ChainEntity chain = ChainEntity.connect(level, first, target); + HOOKED.remove(player); + Entity towed = chain.towed(); + Entity tow = chain.tow(); + player.displayClientMessage(Component.translatable("message.mcheli.chain.connected", + towed != null ? towed.getDisplayName() : first.getDisplayName(), tow != null ? tow.getDisplayName() : target.getDisplayName()), true); + return InteractionResult.SUCCESS; + } +} From 336c4c18bcf056c1d62539dc35dfc5e205378db9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:32 +0800 Subject: [PATCH 0059/1461] Port FuelItem.java to 1.21.1 --- .../com/jnhprojects/mcheli/item/FuelItem.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java new file mode 100644 index 00000000..71b3ed0a --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +/** + * The fuel can, {@code MCH_ItemFuel}. It holds 600 units; the durability bar shows how much is + * left. Using it refills it from the coal and charcoal in the inventory, as in the original. + * + *

A can in a fuel slot of the vehicle screen empties into the tank while the vehicle stands on + * something, as in the original. Not in the original: right-clicking a vehicle with the can pours + * up to 100 units at once, which saves opening the screen. + */ +public class FuelItem extends Item { + /** Units a full can holds, the original's maximum damage. */ + public static final int CAPACITY = 600; + /** Units poured into a vehicle per click: what the original moved per fuel slot per update. */ + private static final int POUR = 100; + private static final int PER_COAL = 100; + private static final int PER_CHARCOAL = 75; + + public FuelItem(Properties properties) { + super(properties.durability(CAPACITY)); + } + + public static int contents(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + /** Fills a vehicle's tank from the can. Server only. */ + public static InteractionResult pour(ItemStack stack, AircraftEntity vehicle, Player player) { + int added = vehicle.refuel(player.getAbilities().instabuild ? POUR : Math.min(contents(stack), POUR)); + if (added <= 0) { + return InteractionResult.FAIL; + } + if (!player.getAbilities().instabuild) { + stack.setDamageValue(stack.getDamageValue() + added); + } + vehicle.level().playSound(null, vehicle.getX(), vehicle.getY(), vehicle.getZ(), SoundEvents.BUCKET_EMPTY, SoundSource.NEUTRAL, 1.0F, 1.0F); + return InteractionResult.SUCCESS; + } + + /** Charcoal first, then coal, each one worth part of the can, until it is full. */ + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + if (!stack.isDamaged() || player.getAbilities().instabuild) { + return InteractionResultHolder.pass(player.getItemInHand(hand)); + } + if (!level.isClientSide()) { + refill(stack, player.getInventory(), Items.CHARCOAL, PER_CHARCOAL); + refill(stack, player.getInventory(), Items.COAL, PER_COAL); + } + return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide()); + } + + private static void refill(ItemStack can, Inventory inventory, Item coal, int worth) { + for (int slot = 0; slot < inventory.getContainerSize() && can.isDamaged(); slot++) { + ItemStack item = inventory.getItem(slot); + while (item.is(coal) && !item.isEmpty() && can.isDamaged()) { + can.setDamageValue(Math.max(can.getDamageValue() - worth, 0)); + item.shrink(1); + } + } + } +} From d9c8c3e89e61ca298bafb63d0b407f4738d64849 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:33 +0800 Subject: [PATCH 0060/1461] Port LauncherItem.java to 1.21.1 --- .../jnhprojects/mcheli/item/LauncherItem.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java new file mode 100644 index 00000000..1829b7ee --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.UseAnim; +import net.minecraft.world.level.Level; + +import java.util.function.Supplier; + +/** + * A shoulder launched missile, {@code MCH_ItemLightWeaponBase}: the FIM-92 Stinger and the + * FGM-148 Javelin. Held up it is a sight that locks onto targets; the attack key fires once + * locked. It holds one missile, and its bar shows whether it is loaded: an empty launcher reloads + * from a missile in the inventory after two seconds held but not raised. + */ +public class LauncherItem extends Item { + /** The original's durability: 0 is loaded, the maximum is empty. */ + private static final int EMPTY = 10; + + private final String weapon; + private final Supplier missile; + + /** + * @param weapon the weapon definition it fires, such as {@code fim92} + * @param missile the item it reloads from + */ + public LauncherItem(String weapon, Supplier missile, Properties properties) { + super(properties.durability(EMPTY)); + this.weapon = weapon; + this.missile = missile; + } + + public String weapon() { + return weapon; + } + + public Item missile() { + return missile.get(); + } + + public static boolean isLoaded(ItemStack stack) { + return stack.getDamageValue() < stack.getMaxDamage(); + } + + public static void setLoaded(ItemStack stack, boolean loaded) { + stack.setDamageValue(loaded ? 0 : stack.getMaxDamage()); + } + + /** Not from a vehicle seat or the GLTD, as in the original. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity) && !(player.getVehicle() instanceof GltdEntity); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResultHolder.pass(player.getItemInHand(hand)); + } + player.startUsingItem(hand); + return InteractionResultHolder.consume(player.getItemInHand(hand)); + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public UseAnim getUseAnimation(ItemStack stack) { + return UseAnim.BOW; + } +} From a2c41646e38292501100c5a57f93b7c30472ecfb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:33 +0800 Subject: [PATCH 0061/1461] Port ParachuteItem.java to 1.21.1 --- .../mcheli/item/ParachuteItem.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java new file mode 100644 index 00000000..9f19eb4f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +/** The parachute, {@code MCH_ItemParachute}: used in mid-air, it opens above the player. */ +public class ParachuteItem extends Item { + public ParachuteItem(Properties properties) { + super(properties.stacksTo(1)); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + if (player.isPassenger() || player.onGround()) { + return InteractionResultHolder.pass(player.getItemInHand(hand)); + } + if (!level.isClientSide()) { + ParachuteEntity.open(level, player, ParachuteEntity.Kind.ITEM); + player.getItemInHand(hand).consume(1, player); + } + return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide()); + } +} From 39d6f3e7c59e602a542de908b34dddcede75463d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:34 +0800 Subject: [PATCH 0062/1461] Port PlacedEntityItem.java to 1.21.1 --- .../mcheli/item/PlacedEntityItem.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java new file mode 100644 index 00000000..3b68bcca --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; + +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Sets up a piece of equipment that lives in the world as an entity: the GLTD, the container, the + * UAV station. Like the originals' items it goes on top of the block clicked, facing the way the + * player looks, and only where it fits. + */ +public class PlacedEntityItem extends Item { + private final Supplier> type; + + public PlacedEntityItem(Supplier> type, Properties properties) { + super(properties); + this.type = type; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + Player player = context.getPlayer(); + BlockPos above = context.getClickedPos().above(); + Entity entity = type.get().create(level); + if (entity == null) { + return InteractionResult.FAIL; + } + entity.moveTo(above.getX() + 0.5, above.getY(), above.getZ() + 0.5, player != null ? player.getYRot() : 0.0F, 0.0F); + prepare(entity, player); + if (!level.noCollision(entity, entity.getBoundingBox().deflate(0.1)) || !level.getEntities(entity, entity.getBoundingBox()).isEmpty()) { + return InteractionResult.FAIL; + } + if (!level.isClientSide()) { + level.addFreshEntity(entity); + level.gameEvent(player, GameEvent.ENTITY_PLACE, entity.position()); + } + ItemStack stack = context.getItemInHand(); + if (player != null) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, player); + return InteractionResult.SUCCESS; + } + + /** Sets up the new entity before it is checked for room and spawned. */ + protected void prepare(Entity entity, @Nullable Player player) { + } +} From 67779095915c4030af8b4c8e258db19afe6cba6c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:34 +0800 Subject: [PATCH 0063/1461] Port RangefinderItem.java to 1.21.1 --- .../mcheli/item/RangefinderItem.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java new file mode 100644 index 00000000..897d51cd --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.UseAnim; +import net.minecraft.world.level.Level; + +/** + * The laser rangefinder, {@code MCH_ItemRangeFinder}. Held up to the eye it is a scope that reads + * the range to whatever it points at; the attack key spots targets for the team, using one of + * its ten charges. Crafting it with redstone charges it again. + */ +public class RangefinderItem extends Item { + public static final int CHARGES = 10; + + public RangefinderItem(Properties properties) { + super(properties.durability(CHARGES)); + } + + /** Not from a vehicle seat: the vehicle's own sight does that job. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity); + } + + /** Charges left, from none to {@link #CHARGES}. */ + public static int charges(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResultHolder.pass(player.getItemInHand(hand)); + } + player.startUsingItem(hand); + return InteractionResultHolder.consume(player.getItemInHand(hand)); + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public UseAnim getUseAnimation(ItemStack stack) { + return UseAnim.SPYGLASS; + } +} From 37d397109a78099996dc1431543db1a233c9b4c9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:35 +0800 Subject: [PATCH 0064/1461] Port ThrowableItem.java to 1.21.1 --- .../mcheli/item/ThrowableItem.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java new file mode 100644 index 00000000..8c98a126 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java @@ -0,0 +1,124 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.Direction; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.dispenser.BlockSource; +import net.minecraft.core.dispenser.DefaultDispenseItemBehavior; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.UseAnim; +import net.minecraft.world.item.component.CustomModelData; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Hand grenades, {@code MCH_ItemThrowable}. Like the vehicles there is one item, and the + * {@link ModDataComponents#THROWABLE} component says which definition a stack throws, so pack + * throwables need no item of their own. Held like a bow: the longer the draw, the further it goes. + */ +public class ThrowableItem extends Item { + private static final int FULL_DRAW = 35; + + public ThrowableItem(Properties properties) { + super(properties); + } + + /** A stack of {@code def}, showing the item model the import wrote for it. */ + public static ItemStack stackOf(ThrowableDefinition def) { + ItemStack stack = new ItemStack(ModItems.THROWABLE.get()); + stack.set(ModDataComponents.THROWABLE.get(), def.name()); + stack.set(DataComponents.MAX_STACK_SIZE, def.stackSize); + if (def.pack().equals(ContentSource.ORIGINAL)) { + MCHeli.legacyContent().iconIndex(def.name()) + .ifPresent(index -> stack.set(DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(index))); + } + return stack; + } + + private static @Nullable ThrowableDefinition definition(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + return name != null ? MCHeli.content().throwable(name) : null; + } + + @Override + public Component getName(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + if (name == null) { + return super.getName(stack); + } + ThrowableDefinition def = MCHeli.content().throwable(name); + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + if (definition(player.getItemInHand(hand)) == null) { + return InteractionResultHolder.fail(player.getItemInHand(hand)); + } + player.startUsingItem(hand); + return InteractionResultHolder.consume(player.getItemInHand(hand)); + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public UseAnim getUseAnimation(ItemStack stack) { + return UseAnim.BOW; + } + + @Override + public void releaseUsing(ItemStack stack, Level level, LivingEntity entity, int remainingTime) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return; + } + int drawn = Math.max(getUseDuration(stack, entity) - remainingTime, 5); + float strength = drawn <= FULL_DRAW ? drawn / 25.0F : 1.0F; + level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), SoundEvents.ARROW_SHOOT, SoundSource.PLAYERS, + 0.5F, 0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)); + if (!level.isClientSide()) { + level.addFreshEntity(ThrowableEntity.thrown(level, entity, def, strength)); + } + if (entity instanceof Player player) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, entity); + } + + /** Dispensers throw them straight out, {@code MCH_ItemThrowableDispenseBehavior}. */ + public static class DispenseBehavior extends DefaultDispenseItemBehavior { + @Override + protected ItemStack execute(BlockSource source, ItemStack stack) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return super.execute(source, stack); + } + Direction facing = source.state().getValue(DispenserBlock.FACING); + Vec3 at = source.center().add(facing.getStepX() * 0.7, facing.getStepY() * 0.7, facing.getStepZ() * 0.7); + Vec3 direction = new Vec3(facing.getStepX(), facing.getStepY() + 0.1, facing.getStepZ()); + source.level().addFreshEntity(ThrowableEntity.dispensed(source.level(), at, direction, def)); + stack.shrink(1); + return stack; + } + } +} From 3872ba5119fda52b277f84a1df76473040ee097e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:35 +0800 Subject: [PATCH 0065/1461] Port UavStationItem.java to 1.21.1 --- .../mcheli/item/UavStationItem.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java new file mode 100644 index 00000000..7e3bc1c8 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java @@ -0,0 +1,33 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station and the portable UAV controller, {@code MCH_ItemUavStation}. Both place the same + * entity; the kind decides its model and which drones it launches. + */ +public class UavStationItem extends PlacedEntityItem { + private final int kind; + + public UavStationItem(int kind, Properties properties) { + super(ModEntities.UAV_STATION, properties); + this.kind = kind; + } + + /** Squared to the nearest quarter turn, facing the player, as the original placed it. */ + @Override + protected void prepare(Entity entity, @Nullable Player player) { + if (entity instanceof UavStationEntity station) { + station.setKind(kind); + float yaw = player != null ? player.getYRot() : 0.0F; + int quarter = Math.floorMod(Math.round(Mth.wrapDegrees(yaw) / 90.0F), 4); + station.setYRot(quarter * 90.0F - 180.0F); + station.initOffset(); + } + } +} From f511e032160fcd4f6c1f46735d3b05d71b74f0b5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:36 +0800 Subject: [PATCH 0066/1461] Port WrenchItem.java to 1.21.1 --- .../jnhprojects/mcheli/item/WrenchItem.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java new file mode 100644 index 00000000..a9ed1555 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +/** + * The wrench, {@code MCH_ItemWrench}. Right-clicking a damaged vehicle repairs 10 hit points, once + * a second, at one point of the wrench's durability. Sneaking switches the vehicle to its next + * paint scheme. + * + *

The original repaired while the use key was held; a click per repair works the same with + * today's item use, which stops as soon as the cursor leaves the hull. + */ +public class WrenchItem extends Item { + /** An iron tool's durability, the original's tool material. */ + private static final int DURABILITY = 250; + private static final int REPAIR = 10; + private static final int COOLDOWN = 20; + + public WrenchItem(Properties properties) { + super(properties.durability(DURABILITY)); + } + + /** Server only. */ + public static InteractionResult useOn(ItemStack stack, AircraftEntity vehicle, Player player, InteractionHand hand) { + if (player.isSecondaryUseActive()) { + return vehicle.nextTexture() ? InteractionResult.SUCCESS : InteractionResult.FAIL; + } + if (player.getCooldowns().isOnCooldown(stack.getItem()) || !vehicle.repair(REPAIR)) { + return InteractionResult.FAIL; + } + player.getCooldowns().addCooldown(stack.getItem(), COOLDOWN); + stack.hurtAndBreak(1, player, LivingEntity.getSlotForHand(hand)); + ModSounds.playAt(vehicle, "wrench", 1.0F, 0.9F + vehicle.getRandom().nextFloat() * 0.2F); + return InteractionResult.SUCCESS; + } +} From ef27dcbfbbf99626967dd31d66ec90624988768d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:37 +0800 Subject: [PATCH 0067/1461] Port DraftingTableMenu.java to 1.21.1 --- .../mcheli/menu/DraftingTableMenu.java | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java new file mode 100644 index 00000000..022c1813 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java @@ -0,0 +1,155 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.ResultContainer; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; + +import java.util.List; + +/** + * The drafting table, {@code MCH_DraftingTableGuiContainer}: the player's inventory and one output + * slot. The screen browses the MCHeli recipes and asks for one by its id; the server finds the + * recipe, takes the ingredients from the inventory and puts the result in the output slot. + */ +public class DraftingTableMenu extends AbstractContainerMenu { + public static final int OUTPUT_X = 178; + public static final int OUTPUT_Y = 90; + + private final ContainerLevelAccess access; + private final ResultContainer output = new ResultContainer(); + private final int outputSlot; + + /** The client side, from the open screen packet. */ + public DraftingTableMenu(int containerId, Inventory inventory) { + this(containerId, inventory, ContainerLevelAccess.NULL); + } + + public DraftingTableMenu(int containerId, Inventory inventory, ContainerLevelAccess access) { + super(ModMenus.DRAFTING_TABLE.get(), containerId); + this.access = access; + addPlayerSlots(inventory, 30, 140); + outputSlot = slots.size(); + addSlot(new Slot(output, 0, OUTPUT_X, OUTPUT_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + } + + /** + * {@code createRecipeItem}: makes one of the recipe behind {@code id}, if it is ours, the output + * slot is free and the player carries the ingredients. Creative players need neither. + */ + public void craft(ServerPlayer player, ResourceLocation id) { + boolean creative = player.getAbilities().instabuild; + Slot slot = slots.get(outputSlot); + if (slot.hasItem() && !creative) { + return; + } + RecipeHolder holder = player.level().getServer().getRecipeManager().byKey(id).orElse(null); + if (holder == null || !id.getNamespace().equals(MCHeli.MOD_ID) || !(holder.value() instanceof CraftingRecipe recipe)) { + return; + } + // A shaped recipe lists its empty grid slots too. + List ingredients = recipe.getIngredients().stream().filter(ingredient -> !ingredient.isEmpty()).toList(); + if (!creative && !take(player.getInventory(), ingredients, true)) { + return; + } + if (!creative) { + take(player.getInventory(), ingredients, false); + } + slot.set(recipe.assemble(CraftingInput.EMPTY, player.registryAccess())); + broadcastChanges(); + } + + /** + * Finds, and unless {@code dryRun} removes, one item per ingredient. A stack serves several + * ingredients as far as its count goes. + */ + private static boolean take(Inventory inventory, List ingredients, boolean dryRun) { + int[] used = new int[inventory.getContainerSize()]; + for (Ingredient ingredient : ingredients) { + int found = -1; + for (int i = 0; i < inventory.getContainerSize() && found < 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && stack.getCount() > used[i] && ingredient.test(stack)) { + found = i; + } + } + if (found < 0) { + return false; + } + used[found]++; + } + if (!dryRun) { + for (int i = 0; i < used.length; i++) { + if (used[i] > 0) { + inventory.removeItem(i, used[i]); + } + } + } + return true; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(access, player, ModBlocks.DRAFTING_TABLE.get()); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + if (index != outputSlot) { + return ItemStack.EMPTY; + } + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (!moveItemStackTo(stack, 0, outputSlot, true)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** What is left in the output goes back to the player, as in the original. */ + @Override + public void removed(Player player) { + super.removed(player); + if (!player.level().isClientSide()) { + clearContainer(player, output); + } + } + + /** The player's inventory and, 58 below its top, the hotbar. */ + private void addPlayerSlots(Inventory inventory, int x, int y) { + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column + row * 9 + 9, x + column * 18, y + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, x + column * 18, y + 58)); + } + } +} From 06907288fcb080c26c79d9bd3788f570574f3e4b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:37 +0800 Subject: [PATCH 0068/1461] Port UavStationMenu.java to 1.21.1 --- .../mcheli/menu/UavStationMenu.java | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java new file mode 100644 index 00000000..6e4b7a0c --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java @@ -0,0 +1,166 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_ContainerUavStation}: one slot for the drone to put up, the + * launch offset with its -10, -1, +1 and +10 buttons per axis, and Continue to take the last drone + * again. The player sits at the station while it is open, so the client finds it as their vehicle. + */ +public class UavStationMenu extends AbstractContainerMenu { + /** Button ids: axis times four plus the step, then Continue. */ + public static final int[] STEPS = {-10, -1, 1, 10}; + public static final int BUTTON_CONTINUE = 12; + + private static final int DATA_SIZE = 4; + private static final int DATA_CAN_CONTINUE = 3; + + private final @Nullable UavStationEntity station; + private final ContainerData data; + + /** The client side, from the open screen packet. */ + public UavStationMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof UavStationEntity s ? s : null, + new SimpleContainer(1), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public UavStationMenu(int containerId, Inventory inventory, UavStationEntity station) { + this(containerId, inventory, station, station.slot(), new ServerData(station)); + } + + private UavStationMenu(int containerId, Inventory inventory, @Nullable UavStationEntity station, Container slot, ContainerData data) { + super(ModMenus.UAV_STATION.get(), containerId); + this.station = station; + this.data = data; + checkContainerSize(slot, 1); + addSlot(new Slot(slot, 0, 20, 20) { + @Override + public boolean mayPlace(ItemStack stack) { + return AircraftItem.vehicleOf(stack) != null; + } + + @Override + public int getMaxStackSize() { + return 1; + } + }); + addPlayerSlots(inventory, 8, 84); + addDataSlots(data); + } + + public @Nullable UavStationEntity station() { + return station; + } + + /** The launch offset along {@code axis} (0 x, 1 y, 2 z), as the server last reported it. */ + public int offset(int axis) { + return (short) data.get(axis); + } + + public boolean canContinue() { + return data.get(DATA_CAN_CONTINUE) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (station == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CONTINUE) { + return station.continueControl(server); + } + if (id < 0 || id >= 12) { + return false; + } + int[] pos = {station.offset().getX(), station.offset().getY(), station.offset().getZ()}; + pos[id / 4] += STEPS[id % 4]; + station.setOffset(new BlockPos(pos[0], pos[1], pos[2])); + return true; + } + + @Override + public boolean stillValid(Player player) { + return station != null && station.isAlive() && player.getVehicle() == station; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (index == 0) { + if (!moveItemStackTo(stack, 1, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (AircraftItem.vehicleOf(stack) == null || !moveItemStackTo(stack, 0, 1, false)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** Whatever is left in the slot goes back to the player, as with a crafting table. */ + @Override + public void removed(Player player) { + super.removed(player); + if (station != null && !player.level().isClientSide()) { + clearContainer(player, station.slot()); + } + } + + private record ServerData(UavStationEntity station) implements ContainerData { + @Override + public int get(int index) { + BlockPos o = station.offset(); + return switch (index) { + case 0 -> o.getX(); + case 1 -> o.getY(); + case 2 -> o.getZ(); + default -> station.lastUav() != null ? 1 : 0; + }; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } + + /** The player's inventory and, 58 below its top, the hotbar. */ + private void addPlayerSlots(Inventory inventory, int x, int y) { + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column + row * 9 + 9, x + column * 18, y + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, x + column * 18, y + 58)); + } + } +} From 4099126b75646eafaaf621402a557b43d4649344 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:38 +0800 Subject: [PATCH 0069/1461] Port VehicleMenu.java to 1.21.1 --- .../jnhprojects/mcheli/menu/VehicleMenu.java | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java new file mode 100644 index 00000000..d69c63ec --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java @@ -0,0 +1,229 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.weapon.Resupply; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGuiContainer}: fuel slots, a parachute slot per ejection + * seat, reloading the weapons from the crew member's own items and the way into the cargo chest. + * + *

Only crew open it, so the client finds the vehicle as the one its player rides. The weapon + * rounds and which weapons can be reloaded right now come from the server through data slots, + * since a client only hears about the ammunition of the seat it sits in. + */ +public class VehicleMenu extends AbstractContainerMenu { + /** Weapons the screen can show; more than any vehicle in the original has. */ + public static final int MAX_WEAPONS = 32; + /** Button ids from zero up reload that weapon; this one opens the cargo chest. */ + public static final int BUTTON_CARGO = 1000; + + public static final int FUEL_X = 8; + public static final int FUEL_Y = 30; + public static final int PARACHUTE_X = 8; + public static final int PARACHUTE_Y = 66; + public static final int INVENTORY_Y = 102; + + private static final int DATA_RELOADABLE = MAX_WEAPONS; + private static final int DATA_SIZE = MAX_WEAPONS + 2; + + private final @Nullable AircraftEntity vehicle; + private final Container equipment; + private final ContainerData data; + private final int parachuteSlots; + private boolean supplied; + + /** The client side, from the open screen packet. */ + public VehicleMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof AircraftEntity v ? v : null, + new SimpleContainer(VehicleInventory.EQUIPMENT_SLOTS), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public VehicleMenu(int containerId, Inventory inventory, AircraftEntity vehicle) { + this(containerId, inventory, vehicle, vehicle.inventory().equipment(), new ServerData(vehicle, inventory.player)); + } + + private VehicleMenu(int containerId, Inventory inventory, @Nullable AircraftEntity vehicle, Container equipment, ContainerData data) { + super(ModMenus.VEHICLE.get(), containerId); + this.vehicle = vehicle; + this.equipment = equipment; + this.data = data; + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + parachuteSlots = def != null ? VehicleInventory.parachuteSlots(def) : 0; + checkContainerSize(equipment, VehicleInventory.EQUIPMENT_SLOTS); + + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + addSlot(new Slot(equipment, i, FUEL_X + i * 18, FUEL_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() instanceof FuelItem; + } + }); + } + for (int i = 0; i < VehicleInventory.PARACHUTE_SLOTS; i++) { + boolean active = i < parachuteSlots; + addSlot(new Slot(equipment, VehicleInventory.FUEL_SLOTS + i, PARACHUTE_X + i * 18, PARACHUTE_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return active && stack.getItem() instanceof ParachuteItem; + } + + @Override + public boolean isActive() { + return active; + } + }); + } + addPlayerSlots(inventory, 8, INVENTORY_Y); + addDataSlots(data); + } + + public @Nullable AircraftEntity vehicle() { + return vehicle; + } + + /** Parachute slots in use: one per ejection seat. */ + public int parachuteSlots() { + return parachuteSlots; + } + + /** Magazine and reserve of a weapon together, as the server last reported them. */ + public int rounds(int weapon) { + return weapon >= 0 && weapon < MAX_WEAPONS ? data.get(weapon) : 0; + } + + /** Whether the server would reload this weapon now: the vehicle is down and the items are there. */ + public boolean canReload(int weapon) { + if (weapon < 0 || weapon >= MAX_WEAPONS) { + return false; + } + int bits = (data.get(DATA_RELOADABLE) & 0xFFFF) | (data.get(DATA_RELOADABLE + 1) & 0xFFFF) << 16; + return (bits & 1 << weapon) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (vehicle == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CARGO) { + return vehicle.openCargo(server); + } + if (id >= 0 && id < MAX_WEAPONS && Resupply.supply(server, vehicle, id)) { + supplied = true; + return true; + } + return false; + } + + @Override + public boolean stillValid(Player player) { + return vehicle != null && equipment.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + int equipmentEnd = VehicleInventory.EQUIPMENT_SLOTS; + if (index < equipmentEnd) { + if (!moveItemStackTo(stack, equipmentEnd, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof FuelItem) { + if (!moveItemStackTo(stack, 0, VehicleInventory.FUEL_SLOTS, false)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof ParachuteItem && parachuteSlots > 0) { + int first = VehicleInventory.FUEL_SLOTS; + if (!moveItemStackTo(stack, first, first + parachuteSlots, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** As in the original, closing the screen after a resupply loads every magazine from its reserve. */ + @Override + public void removed(Player player) { + super.removed(player); + if (supplied && vehicle != null && vehicle.weapons() != null && !player.level().isClientSide()) { + vehicle.weapons().reloadAll(); + } + } + + /** Rounds per weapon and the reload bits, worked out on the server whenever the menu syncs. */ + private record ServerData(AircraftEntity vehicle, Player player) implements ContainerData { + @Override + public int get(int index) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons == null) { + return 0; + } + if (index < MAX_WEAPONS) { + WeaponSet set = weapons.set(index); + // Data slots travel as shorts. + return set != null ? Math.min(set.ammo() + set.restAmmo(), Short.MAX_VALUE) : 0; + } + int bits = 0; + if (player instanceof ServerPlayer server) { + for (int i = 0; i < Math.min(weapons.sets().size(), MAX_WEAPONS); i++) { + if (Resupply.canSupply(server, vehicle, i)) { + bits |= 1 << i; + } + } + } + return index == DATA_RELOADABLE ? bits & 0xFFFF : bits >>> 16; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } + + /** The player's inventory and, 58 below its top, the hotbar. */ + private void addPlayerSlots(Inventory inventory, int x, int y) { + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column + row * 9 + 9, x + column * 18, y + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, x + column * 18, y + 58)); + } + } +} From dd241ac984cadb34cc757fe5b6487c192d220541 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:38 +0800 Subject: [PATCH 0070/1461] Port DraftingTablePacket.java to 1.21.1 --- .../mcheli/network/DraftingTablePacket.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java new file mode 100644 index 00000000..02c394df --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +/** + * The Create button of the drafting table, {@code MCH_DraftingTableCreatePacket}: make one of the + * recipe with this id. + */ +public record DraftingTablePacket(ResourceLocation recipe) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("drafting_table"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(DraftingTablePacket::encode, DraftingTablePacket::decode); + + public static void encode(DraftingTablePacket msg, FriendlyByteBuf buf) { + buf.writeResourceLocation(msg.recipe); + } + + public static DraftingTablePacket decode(FriendlyByteBuf buf) { + return new DraftingTablePacket(buf.readResourceLocation()); + } + + public static void handle(DraftingTablePacket msg, ServerPlayer sender) { + if (sender != null && sender.containerMenu instanceof DraftingTableMenu menu && menu.stillValid(sender)) { + menu.craft(sender, msg.recipe); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 0822a91002110b99ca46512c57877092991f8533 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:39 +0800 Subject: [PATCH 0071/1461] Port EquipmentActionPacket.java to 1.21.1 --- .../mcheli/network/EquipmentActionPacket.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java new file mode 100644 index 00000000..76090f2e --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A request from a player using a piece of equipment they sit at, such as the GLTD. Like + * {@link CrewActionPacket} the client only asks; the equipment checks that the sender is the one + * using it. + */ +public record EquipmentActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("equipment_action"); + public static final StreamCodec STREAM_CODEC = + StreamCodec.ofMember(EquipmentActionPacket::encode, EquipmentActionPacket::decode); + + public enum Action { + /** Mark the spot under the sight and call in the A-10. */ + CALL_STRIKE, + /** Turn night vision on for the sender. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF + } + + public static void encode(EquipmentActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static EquipmentActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown equipment action " + ordinal); + } + return new EquipmentActionPacket(entityId, actions[ordinal]); + } + + public static void handle(EquipmentActionPacket msg, ServerPlayer sender) { + if (sender == null || sender.getVehicle() == null || sender.getVehicle().getId() != msg.entityId) { + return; + } + if (sender.getVehicle() instanceof GltdEntity gltd) { + switch (msg.action) { + case CALL_STRIKE -> gltd.callStrike(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> gltd.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 5b65044e7edb6451927affd505dc9107f1462e46 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:39 +0800 Subject: [PATCH 0072/1461] Port LauncherPacket.java to 1.21.1 --- .../mcheli/network/LauncherPacket.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java new file mode 100644 index 00000000..4306edfa --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java @@ -0,0 +1,118 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +/** + * The player with a launcher in hand, {@code MCH_PacketLightWeaponPlayerControl}: fire at the + * locked target, or finish a reload. The client locks and times the reload; the server checks + * the launcher, the target and the missile in the inventory. + * + * @param target the locked entity, for {@link Action#FIRE} + * @param mode the Javelin's attack mode: 0 direct, 1 top attack + */ +public record LauncherPacket(Action action, int target, int mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("launcher"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LauncherPacket::encode, LauncherPacket::decode); + + /** Locks further than this are refused, with some slack over the lock range for lag. */ + private static final double MAX_TARGET_DISTANCE = 260.0; + + public enum Action { + FIRE, + RELOAD + } + + public static void encode(LauncherPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeVarInt(msg.target); + buf.writeVarInt(msg.mode); + } + + public static LauncherPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown launcher action " + ordinal); + } + return new LauncherPacket(actions[ordinal], buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(LauncherPacket msg, ServerPlayer sender) { + ItemStack stack = sender == null ? ItemStack.EMPTY : sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof LauncherItem launcher) || !LauncherItem.canUse(sender)) { + return; + } + switch (msg.action) { + case FIRE -> fire(sender, stack, launcher, msg.target, msg.mode); + case RELOAD -> reload(sender, stack, launcher); + } + } + + private static void fire(ServerPlayer player, ItemStack stack, LauncherItem launcher, int targetId, int mode) { + WeaponDefinition def = MCHeli.content().weapon(launcher.weapon()); + ServerLevel level = player.serverLevel(); + Entity target = level.getEntity(targetId); + if (def == null || !LauncherItem.isLoaded(stack) || target == null || target.isRemoved() + || target.distanceToSqr(player) > MAX_TARGET_DISTANCE * MAX_TARGET_DISTANCE) { + return; + } + ProjectileEntity missile = def.type == WeaponType.AT_MISSILE + ? ModEntities.AT_MISSILE.get().create(level) + : ModEntities.AA_MISSILE.get().create(level); + if (missile == null) { + return; + } + float yaw = player.getYRot(); + float pitch = player.getXRot(); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + Vec3 at = player.getEyePosition().add(dir); + missile.launch(at.x, at.y, at.z, dir.x, dir.y, dir.z, yaw, pitch, def.acceleration); + missile.setWeapon(launcher.weapon()); + missile.setParameters(def, player, player); + missile.setTarget(target); + if (missile instanceof ATMissileEntity atMissile) { + atMissile.guidanceType = Math.clamp(mode, 0, Math.max(def.modeNum - 1, 0)); + } + level.addFreshEntity(missile); + if (def.sound != null) { + ModSounds.playAt(player, def.sound, def.soundVolume, def.soundPitch); + } + LauncherItem.setLoaded(stack, false); + } + + private static void reload(ServerPlayer player, ItemStack stack, LauncherItem launcher) { + if (LauncherItem.isLoaded(stack)) { + return; + } + if (!player.getAbilities().instabuild) { + int slot = player.getInventory().findSlotMatchingItem(new ItemStack(launcher.missile())); + if (slot < 0) { + return; + } + player.getInventory().removeItem(slot, 1); + } + LauncherItem.setLoaded(stack, true); + } + + @Override + public Type type() { + return TYPE; + } +} From 509fc0d7aadf467a61ac312f7b79782a1234b0f3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:40 +0800 Subject: [PATCH 0073/1461] Port MarkerPacket.java to 1.21.1 --- .../mcheli/network/MarkerPacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java new file mode 100644 index 00000000..1de0bfc3 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to client: the team spotted these entities for {@code ticks}, or marked {@code point}, + * {@code MCH_PacketNotifySpotedEntity} and {@code MCH_PacketNotifyMarkPoint}. Only the spotter and + * their teammates get it. + */ +public record MarkerPacket(int[] entities, int ticks, BlockPos point) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("marker"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(MarkerPacket::encode, MarkerPacket::decode); + /** Spotting sends no point; this stands in for none. */ + private static final BlockPos NO_POINT = new BlockPos(0, Integer.MIN_VALUE, 0); + private static final int MAX_ENTITIES = 256; + + public static MarkerPacket spotted(int[] entities, int ticks) { + return new MarkerPacket(entities, ticks, NO_POINT); + } + + public static MarkerPacket point(BlockPos point) { + return new MarkerPacket(new int[0], 0, point); + } + + public static void encode(MarkerPacket msg, FriendlyByteBuf buf) { + buf.writeVarIntArray(msg.entities); + buf.writeVarInt(msg.ticks); + buf.writeBlockPos(msg.point); + } + + public static MarkerPacket decode(FriendlyByteBuf buf) { + return new MarkerPacket(buf.readVarIntArray(MAX_ENTITIES), buf.readVarInt(), buf.readBlockPos()); + } + + public static void handle(MarkerPacket msg) { + if (msg.entities.length > 0) { + Markers.spotted(msg.entities, msg.ticks); + } + if (!msg.point.equals(NO_POINT)) { + Markers.point(msg.point); + } + } + + @Override + public Type type() { + return TYPE; + } +} From b712bddb331291d75cda58c1fc5d234e252c05b1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:41 +0800 Subject: [PATCH 0074/1461] Port PackSoundPacket.java to 1.21.1 --- .../mcheli/network/PackSoundPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java new file mode 100644 index 00000000..8bac5034 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: play a sound a content pack brought, once, at a point. The game's own sound + * packet ends at the sound manager, which only knows the sounds in {@code sounds.json}, and a pack + * has none: it would be silent. See {@code ModSounds}. + */ +public record PackSoundPacket(String sound, double x, double y, double z, float volume, float pitch) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_sound"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSoundPacket::encode, PackSoundPacket::decode); + + private static final int MAX_NAME = 256; + + public static void encode(PackSoundPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.sound, MAX_NAME); + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.volume); + buf.writeFloat(msg.pitch); + } + + public static PackSoundPacket decode(FriendlyByteBuf buf) { + return new PackSoundPacket(buf.readUtf(MAX_NAME), buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readFloat()); + } + + public static void handle(PackSoundPacket msg) { + ClientAccess.packs().onPackSound(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From ed4a00cf2f2f300d5d2eb6223d209b4f8aca0796 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:41 +0800 Subject: [PATCH 0075/1461] Port PackSubmissions.java to 1.21.1 --- .../mcheli/network/PackSubmissions.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java new file mode 100644 index 00000000..0a01b93a --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java @@ -0,0 +1,185 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.Util; +import org.jspecify.annotations.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Players offering a server the vehicles they built, for its operators to approve. + *

+ *  Submit in the workshop   ── PackSubmitPacket(chunk) ──▶  until the whole zip is here
+ *  checked off the main thread, kept in mcheli/pending, operators told
+ *  /mcheli approve <pack>    moves it to mcheli/packs and reloads, so every player gets it
+ * 
+ * Nothing a client sends is trusted. Submissions are off until the server's owner turns them on, one + * zip can be no larger than the server allows for a pack, a player sends one at a time with a pause + * in between, chunks out of order end the transfer, and a few transfers run at once at most. The + * bytes stay in memory only while they arrive; {@link PendingPacks} checks them as it would any pack. + * + *

Everything here runs on the server thread except the check, which reads the whole pack. + */ +public final class PackSubmissions { + private static final int MAX_UPLOADS = 4; + private static final long COOLDOWN_MILLIS = 30_000L; + + private static final Map UPLOADS = new HashMap<>(); + private static final Map LAST_SUBMISSION = new HashMap<>(); + + private PackSubmissions() { + } + + static void receive(ServerPlayer player, PackSubmitPacket packet) { + UUID id = player.getUUID(); + if (packet.offset() == 0) { + UPLOADS.remove(id); + Component refusal = refusal(player, packet.total()); + if (refusal != null) { + player.sendSystemMessage(refusal); + return; + } + UPLOADS.put(id, new ByteArrayOutputStream()); + } + ByteArrayOutputStream upload = UPLOADS.get(id); + // No upload means it was refused above and the rest of its chunks are still on their way. + if (upload == null) { + return; + } + long end = (long) upload.size() + packet.data().length; + if (packet.offset() != upload.size() || packet.total() > ServerConfig.maxPackBytes() || end > packet.total()) { + UPLOADS.remove(id); + player.sendSystemMessage(Component.translatable("message.mcheli.submit.failed", "chunks arrived out of order")); + return; + } + upload.write(packet.data(), 0, packet.data().length); + if (end == packet.total()) { + UPLOADS.remove(id); + LAST_SUBMISSION.put(id, System.currentTimeMillis()); + check(player, upload.toByteArray()); + } + } + + private static @Nullable Component refusal(ServerPlayer player, int total) { + if (!ServerConfig.allowSubmissions()) { + return Component.translatable("message.mcheli.submit.off"); + } + if (total <= 0 || total > ServerConfig.maxPackBytes()) { + return Component.translatable("message.mcheli.submit.too_large", ServerConfig.maxPackBytes() >> 20); + } + if (UPLOADS.size() >= MAX_UPLOADS) { + return Component.translatable("message.mcheli.submit.busy"); + } + long now = System.currentTimeMillis(); + LAST_SUBMISSION.values().removeIf(time -> now - time >= COOLDOWN_MILLIS); + return LAST_SUBMISSION.containsKey(player.getUUID()) ? Component.translatable("message.mcheli.submit.busy") : null; + } + + private static void check(ServerPlayer player, byte[] zip) { + MinecraftServer server = player.level().getServer(); + UUID id = player.getUUID(); + String name = player.getScoreboardName(); + CompletableFuture.supplyAsync(() -> { + try { + return MCHeli.packs().pending().store(zip, name, id); + } catch (IOException | PackException | RuntimeException e) { + throw new java.util.concurrent.CompletionException(e); + } + }, Util.ioPool()).whenComplete((submission, failure) -> server.execute(() -> { + if (failure != null) { + Throwable cause = failure.getCause() != null ? failure.getCause() : failure; + MCHeli.LOGGER.info("{} sent a pack that was refused: {}", name, cause.toString()); + tell(server, id, Component.translatable("message.mcheli.submit.failed", cause.getMessage() != null ? cause.getMessage() : cause.toString())); + } else { + arrived(server, submission); + } + })); + } + + private static void arrived(MinecraftServer server, PendingPacks.Submission submission) { + MCHeli.LOGGER.info("{} submitted content pack {} {} ({})", submission.submitter(), submission.id(), + submission.manifest().version(), ContentPack.shortHash(submission.hash())); + // Even with approval off, a pack taking over one that is in use waits for a person. + boolean replaces = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (!ServerConfig.requireApproval() && !replaces) { + try { + install(server, submission); + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not install submitted content pack {}", submission.id(), e); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.failed", e.getMessage())); + } + return; + } + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.waiting", submission.id())); + Component note = Component.translatable("message.mcheli.submit.to_operators", submission.submitter(), submission.id(), submission.manifest().version()); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (PackSync.seesProblems(player)) { + player.sendSystemMessage(note); + } + } + } + + /** + * Puts a waiting pack in use: into {@code mcheli/packs}, then a reload that sends every player + * the new list. + * + * @return whether the pack is in use now; the server's allow list or size limits may leave it out + */ + public static boolean install(MinecraftServer server, PendingPacks.Submission submission) throws IOException, PackException { + MCHeli.packs().pending().install(submission, MCHeli.packs().folder()); + ContentReload.reload(server); + MCHeli.LOGGER.info("Content pack {} from {} approved", submission.id(), submission.submitter()); + boolean live = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (live) { + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.approved", submission.id())); + } + return live; + } + + /** Throws a waiting pack away and tells the player who sent it, if they are here. */ + public static void reject(MinecraftServer server, PendingPacks.Submission submission) throws IOException { + MCHeli.packs().pending().remove(submission); + MCHeli.LOGGER.info("Content pack {} from {} rejected", submission.id(), submission.submitter()); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.rejected", submission.id())); + } + + private static void tell(MinecraftServer server, UUID player, Component message) { + ServerPlayer online = server.getPlayerList().getPlayer(player); + if (online != null) { + online.sendSystemMessage(message); + } + } + + /** An operator joining hears what is waiting for them. */ + static void onLogin(ServerPlayer player) { + if (!PackSync.seesProblems(player)) { + return; + } + try { + List waiting = MCHeli.packs().pending().list(); + if (!waiting.isEmpty()) { + player.sendSystemMessage(Component.translatable("message.mcheli.submit.waiting_for_you", waiting.size())); + } + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not list the submitted content packs", e); + } + } + + static void onLogout(ServerPlayer player) { + UPLOADS.remove(player.getUUID()); + } +} From b7846a2ee0dd28593cc803fa457bc8738801ac6f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:42 +0800 Subject: [PATCH 0076/1461] Port PackSubmitPacket.java to 1.21.1 --- .../mcheli/network/PackSubmitPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java new file mode 100644 index 00000000..d142e700 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Player to server: one chunk of a pack the player made, offered to the server's operators. See + * {@link PackSubmissions}, which treats all of this as untrusted. + * + * @param offset where this chunk goes; 0 starts a new submission + * @param total length of the whole zip + */ +public record PackSubmitPacket(int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_submit"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSubmitPacket::encode, PackSubmitPacket::decode); + + /** Under the 32 KiB limit of a serverbound custom payload, which is far lower than the other way. */ + public static final int CHUNK_SIZE = 30 * 1024; + + public static void encode(PackSubmitPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackSubmitPacket decode(FriendlyByteBuf buf) { + return new PackSubmitPacket(buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackSubmitPacket msg, ServerPlayer sender) { + PackSubmissions.receive(sender, msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 1670f03ac579d2e2f298dc18434c41e2ca0c9e60 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:42 +0800 Subject: [PATCH 0077/1461] Port SpotPacket.java to 1.21.1 --- .../mcheli/network/SpotPacket.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java new file mode 100644 index 00000000..1ee72286 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.weapon.Spotting; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; + +/** + * The attack key through a rangefinder, {@code MCH_PacketIndSpotEntity}: spot players and + * vehicles, spot mobs, or mark a point for the team. Spotting uses a charge; marking is free. + */ +public record SpotPacket(Mode mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("spot"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(SpotPacket::encode, SpotPacket::decode); + + /** The rangefinder's modes, in the order its mode key steps through them. */ + public enum Mode { + PLAYERS_AND_VEHICLES(Spotting.PLAYERS | Spotting.GROUND_VEHICLES | Spotting.HELICOPTERS | Spotting.PLANES), + MOBS(Spotting.OTHERS | Spotting.MONSTERS), + MARK_POINT(0); + + final int filter; + + Mode(int filter) { + this.filter = filter; + } + } + + public static void encode(SpotPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.mode.ordinal()); + } + + public static SpotPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Mode[] modes = Mode.values(); + if (ordinal < 0 || ordinal >= modes.length) { + throw new IllegalArgumentException("Unknown spot mode " + ordinal); + } + return new SpotPacket(modes[ordinal]); + } + + public static void handle(SpotPacket msg, ServerPlayer sender) { + if (sender == null || !RangefinderItem.canUse(sender)) { + return; + } + ItemStack stack = sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + stack = sender.getItemInHand(InteractionHand.OFF_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + return; + } + } + boolean done; + if (msg.mode == Mode.MARK_POINT) { + done = Spotting.markPoint(sender); + } else if (RangefinderItem.charges(stack) > 0) { + if (!sender.getAbilities().instabuild) { + // An empty rangefinder stays: it only needs redstone, unlike a worn out tool. + stack.setDamageValue(stack.getDamageValue() + 1); + } + done = Spotting.spot(sender, msg.mode.filter); + } else { + done = false; + } + ModSounds.playAt(sender, done ? "pi" : "ng", 1.0F, 1.0F); + } + + @Override + public Type type() { + return TYPE; + } +} From 07b7af99b2c993dc7cf7f15b2e227f0e9cd866cb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:43 +0800 Subject: [PATCH 0078/1461] Port TeamPacket.java to 1.21.1 --- .../mcheli/network/TeamPacket.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java new file mode 100644 index 00000000..14b2db42 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A button of the team manager, {@code MCH_PacketIndMultiplayCommand}. Operators only; the + * server checks again. + * + * @param name the new team's name, for {@link Action#CREATE_TEAM} + * @param color the new team's colour, as a vanilla team colour id + */ +public record TeamPacket(Action action, String name, int color, boolean friendlyFire) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("team"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TeamPacket::encode, TeamPacket::decode); + + public enum Action { + PVP, + SHUFFLE, + CREATE_TEAM, + JUMP_TO_SPAWN, + DESTROY_ALL + } + + public TeamPacket(Action action) { + this(action, "", 0, false); + } + + public static void encode(TeamPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeUtf(msg.name, 16); + buf.writeVarInt(msg.color); + buf.writeBoolean(msg.friendlyFire); + } + + public static TeamPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown team action " + ordinal); + } + return new TeamPacket(actions[ordinal], buf.readUtf(16), buf.readVarInt(), buf.readBoolean()); + } + + public static void handle(TeamPacket msg, ServerPlayer sender) { + if (sender == null || !TeamManager.mayManage(sender)) { + return; + } + switch (msg.action) { + case PVP -> TeamManager.togglePvp(sender); + case SHUFFLE -> TeamManager.shuffle(sender); + case CREATE_TEAM -> TeamManager.create(sender, msg.name, msg.color, msg.friendlyFire); + case JUMP_TO_SPAWN -> TeamManager.jumpToSpawn(sender); + case DESTROY_ALL -> TeamManager.destroyAll(sender); + } + } + + @Override + public Type type() { + return TYPE; + } +} From d031f4780aeb779ce8e5d8e775dc2a5600955454 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:43 +0800 Subject: [PATCH 0079/1461] Port TestFlightPacket.java to 1.21.1 --- .../mcheli/network/TestFlightPacket.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java new file mode 100644 index 00000000..db12a390 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.workshop.TestFlight; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** The workshop's Test flight button: fly the vehicle just saved. The server checks who is asking. */ +public record TestFlightPacket(String vehicle) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("test_flight"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TestFlightPacket::encode, TestFlightPacket::decode); + private static final int MAX_NAME = 64; + + public static void encode(TestFlightPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.vehicle, MAX_NAME); + } + + public static TestFlightPacket decode(FriendlyByteBuf buf) { + return new TestFlightPacket(buf.readUtf(MAX_NAME)); + } + + public static void handle(TestFlightPacket msg, ServerPlayer sender) { + if (sender != null) { + TestFlight.start(sender, msg.vehicle); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 354f655e367bb7023c92eb2a32f67a9b160a0785 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:44 +0800 Subject: [PATCH 0080/1461] Port SmokeParticleOptions.java to 1.21.1 --- .../mcheli/particle/SmokeParticleOptions.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java new file mode 100644 index 00000000..b8151034 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.particle; + +import net.minecraft.util.ExtraCodecs; +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; + +/** + * A puff of coloured smoke that swells and pales as it drifts, {@code MCH_EntityParticleSmoke} + * with {@code toWhite} set. The smoke grenades use it to mark a spot. + * + * @param color RGB the puff starts with + * @param size the original's particle size; a puff ends up about {@code size / 5} blocks across + * @param lifetime ticks the puff lasts, or 0 for the grenade smoke's three to five seconds + */ +public record SmokeParticleOptions(int color, float size, int lifetime) implements ParticleOptions { + public SmokeParticleOptions(int color, float size) { + this(color, size, 0); + } + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + Codec.INT.fieldOf("color").forGetter(SmokeParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(SmokeParticleOptions::size), + ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("lifetime", 0).forGetter(SmokeParticleOptions::lifetime)) + .apply(i, SmokeParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, SmokeParticleOptions::color, + ByteBufCodecs.FLOAT, SmokeParticleOptions::size, + ByteBufCodecs.VAR_INT, SmokeParticleOptions::lifetime, + SmokeParticleOptions::new); + + @Override + public ParticleType getType() { + return ModParticles.SMOKE.get(); + } +} From df337a4d35053759544c40bef75a92c9989e33c0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:44 +0800 Subject: [PATCH 0081/1461] Port ModBlockEntities.java to 1.21.1 --- .../mcheli/registry/ModBlockEntities.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java new file mode 100644 index 00000000..28270640 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java @@ -0,0 +1,21 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; + + +/** Our block entities: the drafting table's, which draws its model. */ +public final class ModBlockEntities { + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.BLOCK_ENTITY_TYPE, + "drafting_table", key -> BlockEntityType.Builder.of(DraftingTableBlockEntity::new, ModBlocks.DRAFTING_TABLE.get()).build(null)); + + private ModBlockEntities() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From bf400cea723b9715893fa8f9b0f867aabc474196 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:45 +0800 Subject: [PATCH 0082/1461] Port ModBlocks.java to 1.21.1 --- .../mcheli/registry/ModBlocks.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java new file mode 100644 index 00000000..c833e532 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockBehaviour; + +/** Our blocks: the drafting table, lit or not. */ +public final class ModBlocks { + public static final Registered DRAFTING_TABLE = Registration.register(Registries.BLOCK, "drafting_table", + key -> new DraftingTableBlock(BlockBehaviour.Properties.of() + .strength(0.2F) + .sound(SoundType.METAL) + .noOcclusion() + .lightLevel(state -> state.getValue(DraftingTableBlock.LIT) ? 15 : 0))); + + private ModBlocks() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 2bc9312f5f7dcf35cacacaf7cfa0888dcdb3befc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:46 +0800 Subject: [PATCH 0083/1461] Port ModMenus.java to 1.21.1 --- .../jnhprojects/mcheli/registry/ModMenus.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java new file mode 100644 index 00000000..0dac0052 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; + +/** Our screens with slots. The cargo chest uses vanilla's chest menus and needs no entry here. */ +public final class ModMenus { + public static final Registered> VEHICLE = Registration.register(Registries.MENU, "vehicle", + key -> new MenuType<>(VehicleMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.MENU, "drafting_table", + key -> new MenuType<>(DraftingTableMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> UAV_STATION = Registration.register(Registries.MENU, "uav_station", + key -> new MenuType<>(UavStationMenu::new, FeatureFlags.VANILLA_SET)); + + private ModMenus() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 2ae5381397bb24760417957b06dea6b877311408 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:46 +0800 Subject: [PATCH 0084/1461] Port Markers.java to 1.21.1 --- .../com/jnhprojects/mcheli/team/Markers.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/team/Markers.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/team/Markers.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/team/Markers.java new file mode 100644 index 00000000..4d869373 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/team/Markers.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.team; + +import net.minecraft.core.BlockPos; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * What this client's team has spotted, {@code MCH_GuiTargetMarker}: the spotted entities, each for + * as long as it was spotted, and the last marked point. Filled by {@code MarkerPacket} on the + * client's main thread and drawn by the HUD; a dedicated server never touches it. + */ +public final class Markers { + /** A marked point stays up this long, unless another replaces it. */ + public static final int POINT_TICKS = 60 * 20; + + private static final Map SPOTTED = new HashMap<>(); + private static @Nullable BlockPos point; + private static int pointTicks; + + private Markers() { + } + + public static void spotted(int[] entities, int ticks) { + for (int id : entities) { + SPOTTED.merge(id, ticks, Math::max); + } + } + + public static void point(BlockPos pos) { + point = pos; + pointTicks = POINT_TICKS; + } + + /** Entity ids and ticks left. */ + public static Map spotted() { + return SPOTTED; + } + + public static @Nullable BlockPos point() { + return point; + } + + public static void tick() { + SPOTTED.replaceAll((id, ticks) -> ticks - 1); + SPOTTED.values().removeIf(ticks -> ticks <= 0); + if (pointTicks > 0 && --pointTicks == 0) { + point = null; + } + } + + public static void clear() { + SPOTTED.clear(); + point = null; + pointTicks = 0; + } +} From 82806fdee9f489ec2cda6b644f81789bf47b8f12 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:47 +0800 Subject: [PATCH 0085/1461] Port TeamManager.java to 1.21.1 --- .../jnhprojects/mcheli/team/TeamManager.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java new file mode 100644 index 00000000..ae6a59de --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java @@ -0,0 +1,113 @@ +package com.jnhprojects.mcheli.team; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.portal.DimensionTransition; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * The team manager's actions, {@code MCH_Multiplay} and {@code MCH_MultiplayPacketHandler}: for + * operators running a match. Teams are the vanilla scoreboard teams, so {@code /team} and the + * player list see the same ones. + */ +public final class TeamManager { + /** The colours a team can have. */ + public static final List COLORS = Arrays.stream(ChatFormatting.values()).filter(ChatFormatting::isColor).toList(); + + private TeamManager() { + } + + /** Operators only, as the original checked the scoreboard command's permission. */ + public static boolean mayManage(Player player) { + return player.hasPermissions(Commands.LEVEL_GAMEMASTERS); + } + + /** Switches the server's PVP setting and says which way it went. There is no game rule for it here, so it lasts until a restart. */ + public static void togglePvp(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + boolean pvp = !server.isPvpAllowed(); + server.setPvpAllowed(pvp); + broadcast(server, Component.translatable(pvp ? "message.mcheli.team.pvp_on" : "message.mcheli.team.pvp_off")); + } + + /** {@code shuffleTeam}: every player online goes into one of the teams in turn, in random order. */ + public static void shuffle(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + Scoreboard scoreboard = server.getScoreboard(); + List teams = new ArrayList<>(scoreboard.getPlayerTeams()); + if (teams.isEmpty()) { + player.displayClientMessage(Component.translatable("message.mcheli.team.no_teams"), true); + return; + } + List players = new ArrayList<>(server.getPlayerList().getPlayers()); + Collections.shuffle(players); + Collections.shuffle(teams); + for (int i = 0; i < players.size(); i++) { + scoreboard.addPlayerToTeam(players.get(i).getScoreboardName(), teams.get(i % teams.size())); + } + broadcast(server, Component.translatable("message.mcheli.team.shuffled", teams.size())); + } + + /** The original's create team screen: a name, a colour and friendly fire. */ + public static void create(ServerPlayer player, String name, int color, boolean friendlyFire) { + Scoreboard scoreboard = player.level().getServer().getScoreboard(); + String trimmed = name.trim(); + if (trimmed.isEmpty() || trimmed.length() > 16 || trimmed.contains(" ") || scoreboard.getPlayerTeam(trimmed) != null) { + player.displayClientMessage(Component.translatable("message.mcheli.team.bad_name", trimmed), true); + return; + } + PlayerTeam team = scoreboard.addPlayerTeam(trimmed); + team.setColor(COLORS.get(Math.floorMod(color, COLORS.size()))); + team.setAllowFriendlyFire(friendlyFire); + player.displayClientMessage(Component.translatable("message.mcheli.team.created", trimmed), true); + } + + /** {@code jumpSpawnPoint}: every player back to where they would respawn. */ + public static void jumpToSpawn(ServerPlayer player) { + for (ServerPlayer target : List.copyOf(player.level().getServer().getPlayerList().getPlayers())) { + target.stopRiding(); + target.changeDimension(target.findRespawnPositionAndUseSpawnBlock(false, DimensionTransition.DO_NOTHING)); + } + } + + /** {@code destoryAllAircraft}: every loaded vehicle is gone, its crew left standing. */ + public static void destroyAll(ServerPlayer player) { + int count = 0; + for (ServerLevel level : player.level().getServer().getAllLevels()) { + for (AircraftEntity vehicle : level.getEntities(EntityTypeTest.forClass(AircraftEntity.class), e -> true)) { + vehicle.ejectPassengers(); + vehicle.discard(); + count++; + } + } + player.displayClientMessage(Component.translatable("message.mcheli.team.destroyed", count), true); + } + + /** {@code entity} and everyone on its team. */ + public static List teammates(ServerPlayer entity) { + List out = new ArrayList<>(); + for (ServerPlayer player : entity.level().getServer().getPlayerList().getPlayers()) { + if (player == entity || player.isAlliedTo(entity)) { + out.add(player); + } + } + return out; + } + + private static void broadcast(MinecraftServer server, Component message) { + server.getPlayerList().broadcastSystemMessage(message, false); + } +} From 42a72fd8da315d498de9dc6c6cd07ea529e57842 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:47 +0800 Subject: [PATCH 0086/1461] Port CasStrike.java to 1.21.1 --- .../jnhprojects/mcheli/weapon/CasStrike.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java new file mode 100644 index 00000000..81763693 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java @@ -0,0 +1,124 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A close air support run, {@code MCH_WeaponCAS}: three A-10 passes over a spot, 45 ticks apart, + * the second and third offset to either side. Vehicles with a CAS weapon drive theirs from their + * {@link WeaponSet}; the GLTD keeps one of these. + */ +public final class CasStrike { + /** Ticks from the call to the first pass: the time it takes the A-10 to arrive. */ + public static final int FIRST_PASS = 50; + private static final int BETWEEN_PASSES = 45; + private static final int PASSES = 3; + private static final RandomSource RANDOM = RandomSource.create(); + + private final String weapon; + private double x; + private double y; + private double z; + private int direction; + private @Nullable Entity shooter; + private @Nullable Entity user; + private int passes = PASSES; + private int ticksToNext; + + /** @param weapon the CAS weapon definition the A-10s fire, such as {@code a10gau8} */ + public CasStrike(String weapon) { + this.weapon = weapon; + } + + /** Whether passes are still to come. */ + public boolean isRunning() { + return passes < PASSES; + } + + /** + * Calls the strike on {@code at}. The A-10 comes in from one side of the caller's view, + * picked at random, never straight from behind or towards them. + */ + public void start(Vec3 at, float callerYaw, @Nullable Entity shooter, @Nullable Entity user) { + x = at.x; + y = at.y; + z = at.z; + direction = Math.floorMod(approach(callerYaw), 4); + this.shooter = shooter; + this.user = user; + passes = 0; + ticksToNext = FIRST_PASS; + } + + /** The original's approach: the quarter the caller faces, turned one way or the other. */ + static int approach(float callerYaw) { + int facing = (int) Mth.positiveModulo(callerYaw + 45.0F, 360.0F) / 90; + return facing + (RANDOM.nextBoolean() ? -1 : 1); + } + + public void tick(ServerLevel level, WeaponDefinition def) { + if (passes >= PASSES || --ticksToNext > 0) { + return; + } + Vec3 offset = passOffset(passes, direction); + spawnA10(level, weapon, def, direction, x + offset.x, y + 20.0, z + offset.z, shooter, user); + passes++; + ticksToNext = BETWEEN_PASSES; + } + + /** The first pass is dead on; the second strays to one side of the track, the third to the other. */ + static Vec3 passOffset(int pass, int direction) { + if (pass == 0) { + return Vec3.ZERO; + } + double sign = pass == 1 ? 1.0 : -1.0; + if (direction % 2 == 0) { + return new Vec3(RANDOM.nextDouble() * 10.0 * sign, 0.0, (RANDOM.nextDouble() - 0.5) * 10.0); + } + return new Vec3((RANDOM.nextDouble() - 0.5) * 10.0, 0.0, RANDOM.nextDouble() * 10.0 * sign); + } + + /** One A-10, starting 90 blocks short of the spot and flying over it at three blocks a tick. */ + static void spawnA10(ServerLevel level, String weapon, WeaponDefinition def, int direction, double x, double y, double z, + @Nullable Entity shooter, @Nullable Entity user) { + double mx = 0.0; + double mz = 0.0; + switch (direction) { + case 0 -> { + mz = 3.0; + z -= 90.0; + } + case 1 -> { + mx = -3.0; + x += 90.0; + } + case 2 -> { + mz = -3.0; + z += 90.0; + } + default -> { + mx = 3.0; + x -= 90.0; + } + } + A10Entity a10 = ModEntities.A10.get().create(level); + if (a10 == null) { + return; + } + a10.setPos(x, y, z); + a10.setup(weapon, direction, mx, mz, shooter, user); + a10.power = def.power; + a10.explosionPower = def.explosion; + a10.acceleration = def.acceleration; + level.addFreshEntity(a10); + ModSounds.playAt(level, x, y, z, "a-10_snd", 150.0F, 1.0F); + } +} From 624d109f362403695ac56ee550409b994e3c736c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:48 +0800 Subject: [PATCH 0087/1461] Port Spotting.java to 1.21.1 --- .../jnhprojects/mcheli/weapon/Spotting.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java new file mode 100644 index 00000000..10167f4a --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.network.MarkerPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +/** + * Spotting with the rangefinder, {@code MCH_Multiplay.spotEntity} and {@code markPoint}. + * + *

Spotted targets and marked points go to the spotter and their team only, as screen markers + * the HUD draws ({@code MCH_GuiTargetMarker}); nobody else sees them. + */ +public final class Spotting { + // The original's target filter bits. + public static final int OTHERS = 1; + public static final int MONSTERS = 2; + public static final int PLAYERS = 4; + public static final int GROUND_VEHICLES = 8; + public static final int HELICOPTERS = 16; + public static final int PLANES = 32; + + /** {@code RangeFinderSpotDist}. */ + public static final double RANGE = 400.0; + /** {@code RangeFinderSpotTime} in ticks, for players and vehicles. */ + private static final int PLAYER_TIME = 15 * 20; + /** Mobs stay marked for a minute. */ + private static final int MOB_TIME = 60 * 20; + /** Half the width of the cone a spot covers, in degrees: the original's angle times two. */ + private static final double CONE = 40.0; + + private Spotting() { + } + + static boolean matches(int filter, Entity entity) { + int bit; + if (entity instanceof PlaneEntity) { + bit = PLANES; + } else if (entity instanceof HelicopterEntity) { + bit = HELICOPTERS; + } else if (entity instanceof AircraftEntity) { + bit = GROUND_VEHICLES; + } else if (entity instanceof Player) { + bit = PLAYERS; + } else if (entity instanceof Enemy) { + bit = MONSTERS; + } else if (entity instanceof LivingEntity) { + bit = OTHERS; + } else { + return false; + } + return (filter & bit) != 0; + } + + /** + * Marks what {@code spotter} sees within {@link #RANGE} and the cone around their view: never + * a teammate, and only targets in plain sight. + * + * @return whether anything was spotted + */ + public static boolean spot(ServerPlayer spotter, int filter) { + ServerLevel level = spotter.serverLevel(); + Vec3 eye = spotter.getEyePosition(); + Vec3 look = spotter.getLookAngle(); + int ticks = (filter & ~(OTHERS | MONSTERS)) == 0 ? MOB_TIME : PLAYER_TIME; + List spotted = new ArrayList<>(); + for (Entity entity : level.getEntities(spotter, spotter.getBoundingBox().inflate(RANGE), e -> matches(filter, e))) { + if (isFriendly(spotter, entity)) { + continue; + } + Vec3 to = entity.getBoundingBox().getCenter().subtract(eye); + double dist = to.length(); + if (dist < 1.0 || dist > RANGE || Math.toDegrees(Math.acos(to.scale(1.0 / dist).dot(look))) > CONE) { + continue; + } + if (level.clip(new ClipContext(eye, entity.getBoundingBox().getCenter(), ClipContext.Block.COLLIDER, + ClipContext.Fluid.NONE, spotter)).getType() == HitResult.Type.BLOCK) { + continue; + } + spotted.add(entity.getId()); + } + tellTeam(spotter, spotted, ticks); + return !spotted.isEmpty(); + } + + /** Sends the spotted entities to {@code spotter} and their team, {@code MCH_PacketNotifySpotedEntity}. */ + public static void tellTeam(ServerPlayer spotter, List entities, int ticks) { + if (entities.isEmpty()) { + return; + } + MarkerPacket packet = MarkerPacket.spotted(entities.stream().mapToInt(Integer::intValue).toArray(), ticks); + for (ServerPlayer player : TeamManager.teammates(spotter)) { + ModNetwork.sendToPlayer(player, packet); + } + } + + /** Teammates, and a vehicle crewed by one, are never spotted. */ + private static boolean isFriendly(ServerPlayer spotter, Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + for (Entity crew : vehicle.getPassengers()) { + if (crew.isAlliedTo(spotter)) { + return true; + } + } + return false; + } + return entity.isAlliedTo(spotter); + } + + /** + * Tells the spotter and their team where they are looking, as block coordinates. + * + * @return whether the view ended on a block within range + */ + public static boolean markPoint(ServerPlayer spotter) { + return markPoint(spotter, spotter.getEyePosition(), RANGE); + } + + /** + * As above, looking from {@code from} along the spotter's view: a targeting pod marks from + * the vehicle, out to {@code range}. + */ + public static boolean markPoint(ServerPlayer spotter, Vec3 from, double range) { + ServerLevel level = spotter.serverLevel(); + BlockHitResult hit = level.clip(new ClipContext(from, from.add(spotter.getLookAngle().scale(range)), + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, spotter)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + BlockPos pos = hit.getBlockPos(); + MarkerPacket marker = MarkerPacket.point(pos); + Component message = Component.translatable("message.mcheli.mark_point", spotter.getDisplayName(), pos.getX(), pos.getY(), pos.getZ()); + for (ServerPlayer player : level.getServer().getPlayerList().getPlayers()) { + if (player == spotter || player.isAlliedTo(spotter)) { + player.sendSystemMessage(message); + ModNetwork.sendToPlayer(player, marker); + } + } + return true; + } +} From a8d5a986c6ba1eaaebf695105fcc15160a02cea3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:48 +0800 Subject: [PATCH 0088/1461] Port TestFlight.java to 1.21.1 --- .../mcheli/workshop/TestFlight.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java new file mode 100644 index 00000000..4fe57c2c --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.workshop; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.phys.Vec3; + +/** + * Flying a vehicle straight from the workshop. The pack was just saved on the machine this server + * runs on, so loading it again is enough. That only holds for the owner of a single player or LAN + * game: a dedicated server does not get a pack until an operator approves it, and then it is no + * longer a test, so the screen does not offer it there and the server refuses it too. + */ +public final class TestFlight { + /** How far up to look for room when the vehicle does not fit where the player stands. */ + private static final int MAX_RISE = 48; + + private TestFlight() { + } + + public static void start(ServerPlayer player, String vehicle) { + MinecraftServer server = player.level().getServer(); + if (!server.isSingleplayerOwner(player.getGameProfile())) { + player.displayClientMessage(Component.translatable("message.mcheli.workshop.test_flight_owner_only"), true); + return; + } + if (player.isPassenger()) { + player.displayClientMessage(Component.translatable("message.mcheli.workshop.test_flight_riding"), true); + return; + } + ContentReload.reload(server); + AircraftDefinition def = MCHeli.content().any(vehicle); + if (def == null) { + player.sendSystemMessage(Component.translatable("message.mcheli.workshop.test_flight_no_vehicle", vehicle)); + MCHeli.packs().reportProblems(player::sendSystemMessage); + return; + } + ServerLevel level = player.serverLevel(); + AircraftEntity aircraft = ModEntities.typeFor(def).create(level); + if (aircraft == null) { + return; + } + aircraft.setType(def); + Vec3 start = player.position(); + for (int rise = 0; rise <= MAX_RISE; rise++) { + aircraft.moveTo(start.x, start.y + rise, start.z, player.getYRot(), 0.0F); + if (level.noCollision(aircraft, aircraft.getBoundingBox())) { + break; + } + } + aircraft.startTestFlight(start); + level.addFreshEntity(aircraft); + player.startRiding(aircraft, true); + } +} From 5b239b3133170875cb762417f4527ff78a6455fc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:49 +0800 Subject: [PATCH 0089/1461] Port drafting_table.json to 1.21.1 --- .../resources/assets/mcheli/blockstates/drafting_table.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/assets/mcheli/blockstates/drafting_table.json diff --git a/minecraft-1.21.1/src/main/resources/assets/mcheli/blockstates/drafting_table.json b/minecraft-1.21.1/src/main/resources/assets/mcheli/blockstates/drafting_table.json new file mode 100644 index 00000000..82992af9 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/assets/mcheli/blockstates/drafting_table.json @@ -0,0 +1,5 @@ +{ + "multipart": [ + { "apply": { "model": "mcheli:block/drafting_table" } } + ] +} From e350e79219f7e059bb8ed0c6aec15e50e5de54d6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:49 +0800 Subject: [PATCH 0090/1461] Port drafting_table.json to 1.21.1 --- .../mcheli/models/block/drafting_table.json | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/assets/mcheli/models/block/drafting_table.json diff --git a/minecraft-1.21.1/src/main/resources/assets/mcheli/models/block/drafting_table.json b/minecraft-1.21.1/src/main/resources/assets/mcheli/models/block/drafting_table.json new file mode 100644 index 00000000..8dfa5bcc --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/assets/mcheli/models/block/drafting_table.json @@ -0,0 +1,27 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/oak_planks", + "legs": "minecraft:block/iron_block", + "board": "minecraft:block/oak_planks", + "paper": "minecraft:block/white_concrete" + }, + "elements": [ + { "from": [1, 0, 2], "to": [2.5, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 2], "to": [15, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [1, 0, 12.5], "to": [2.5, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 12.5], "to": [15, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [0, 10, 1], "to": [16, 11.5, 15], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { + "north": {"texture": "#board"}, "south": {"texture": "#board"}, "east": {"texture": "#board"}, "west": {"texture": "#board"}, + "up": {"texture": "#board"}, "down": {"texture": "#board"} } }, + { "from": [2, 11.5, 2.5], "to": [14, 11.6, 13.5], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { "up": {"texture": "#paper"} } } + ] +} From 646f48b7655a781a35f2da0386ebb0b27cda3068 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:50 +0800 Subject: [PATCH 0091/1461] Port drafting_table.json to 1.21.1 --- .../resources/assets/mcheli/models/item/drafting_table.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/assets/mcheli/models/item/drafting_table.json diff --git a/minecraft-1.21.1/src/main/resources/assets/mcheli/models/item/drafting_table.json b/minecraft-1.21.1/src/main/resources/assets/mcheli/models/item/drafting_table.json new file mode 100644 index 00000000..355ce4a7 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/assets/mcheli/models/item/drafting_table.json @@ -0,0 +1,3 @@ +{ + "parent": "mcheli:block/drafting_table" +} From c0786418ed72d800d65d5b438c3a07fef86b94b4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:51 +0800 Subject: [PATCH 0092/1461] Port smoke.json to 1.21.1 --- .../resources/assets/mcheli/particles/smoke.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/assets/mcheli/particles/smoke.json diff --git a/minecraft-1.21.1/src/main/resources/assets/mcheli/particles/smoke.json b/minecraft-1.21.1/src/main/resources/assets/mcheli/particles/smoke.json new file mode 100644 index 00000000..977381be --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/assets/mcheli/particles/smoke.json @@ -0,0 +1,16 @@ +{ + "textures": [ + "minecraft:big_smoke_0", + "minecraft:big_smoke_1", + "minecraft:big_smoke_2", + "minecraft:big_smoke_3", + "minecraft:big_smoke_4", + "minecraft:big_smoke_5", + "minecraft:big_smoke_6", + "minecraft:big_smoke_7", + "minecraft:big_smoke_8", + "minecraft:big_smoke_9", + "minecraft:big_smoke_10", + "minecraft:big_smoke_11" + ] +} From b1760b63e7b5357a9f2961aac978145734c57ffa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:51 +0800 Subject: [PATCH 0093/1461] Port thermal.json to 1.21.1 --- .../assets/mcheli/shaders/post/thermal.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/assets/mcheli/shaders/post/thermal.json diff --git a/minecraft-1.21.1/src/main/resources/assets/mcheli/shaders/post/thermal.json b/minecraft-1.21.1/src/main/resources/assets/mcheli/shaders/post/thermal.json new file mode 100644 index 00000000..10f9a6cc --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/assets/mcheli/shaders/post/thermal.json @@ -0,0 +1,17 @@ +{ + "targets": [ + "swap" + ], + "passes": [ + { + "name": "mcheli_thermal", + "intarget": "minecraft:main", + "outtarget": "swap" + }, + { + "name": "blit", + "intarget": "swap", + "outtarget": "minecraft:main" + } + ] +} From 1abc50662bf0c6085a3b90dae62dca2689dba799 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:52 +0800 Subject: [PATCH 0094/1461] Port mcheli_thermal.fsh to 1.21.1 --- .../shaders/program/mcheli_thermal.fsh | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.fsh diff --git a/minecraft-1.21.1/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.fsh b/minecraft-1.21.1/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.fsh new file mode 100644 index 00000000..4a070551 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.fsh @@ -0,0 +1,32 @@ +#version 150 + +// Thermal view, white hot. A post shader only sees colours, so heat is faked: bright and warm +// colours (fire, lava, skin, lamps) read hot, the sky and water cold, and outlines are drawn +// where the brightness changes sharply, like the original's "pencil" shader. + +uniform sampler2D DiffuseSampler; + +in vec2 texCoord; +in vec2 oneTexel; + +out vec4 fragColor; + +const vec3 Gray = vec3(0.3, 0.59, 0.11); + +float heat(vec3 c) { + float luma = dot(c, Gray); + float warmth = clamp(c.r - c.b, 0.0, 1.0); + return clamp(luma * 0.7 + warmth * 0.6, 0.0, 1.0); +} + +void main() { + float c = heat(texture(DiffuseSampler, texCoord).rgb); + float l = heat(texture(DiffuseSampler, texCoord - vec2(oneTexel.x, 0.0)).rgb); + float r = heat(texture(DiffuseSampler, texCoord + vec2(oneTexel.x, 0.0)).rgb); + float u = heat(texture(DiffuseSampler, texCoord - vec2(0.0, oneTexel.y)).rgb); + float d = heat(texture(DiffuseSampler, texCoord + vec2(0.0, oneTexel.y)).rgb); + float edge = clamp(abs(r - l) + abs(d - u), 0.0, 1.0); + + float value = smoothstep(0.08, 0.85, c) + edge * 0.6; + fragColor = vec4(vec3(clamp(value, 0.0, 1.0)), 1.0); +} From 16318ec1ab572345368dfacf924f23ef0bd0e2c4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:52 +0800 Subject: [PATCH 0095/1461] Port mcheli_thermal.json to 1.21.1 --- .../shaders/program/mcheli_thermal.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.json diff --git a/minecraft-1.21.1/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.json b/minecraft-1.21.1/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.json new file mode 100644 index 00000000..bb338e9f --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.json @@ -0,0 +1,18 @@ +{ + "blend": { + "func": "add", + "srcrgb": "one", + "dstrgb": "zero" + }, + "vertex": "sobel", + "fragment": "mcheli_thermal", + "attributes": [ "Position" ], + "samplers": [ + { "name": "DiffuseSampler" } + ], + "uniforms": [ + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "InSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }, + { "name": "OutSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] } + ] +} From bd139ded5d35b07c553ea26e2bec903910f50d83 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:53 +0800 Subject: [PATCH 0096/1461] Port drafting_table.json to 1.21.1 --- .../mcheli/loot_table/blocks/drafting_table.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json b/minecraft-1.21.1/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json new file mode 100644 index 00000000..610afa08 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { "type": "minecraft:item", "name": "mcheli:drafting_table" } + ], + "conditions": [ + { "condition": "minecraft:survives_explosion" } + ] + } + ] +} From fe5f48555b5107875560b631b6e74712fa5692c3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:53 +0800 Subject: [PATCH 0097/1461] Port chain.json to 1.21.1 --- .../main/resources/data/mcheli/recipe/chain.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/chain.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/chain.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/chain.json new file mode 100644 index 00000000..92d099ca --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/chain.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "I I", + "III", + "I I" + ], + "result": { + "id": "mcheli:chain" + } +} From 5f37c0b126857be7e73f2edccb58c4181b1764b5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:54 +0800 Subject: [PATCH 0098/1461] Port container.json to 1.21.1 --- .../resources/data/mcheli/recipe/container.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/container.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/container.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/container.json new file mode 100644 index 00000000..9942f143 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/container.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": {"tag": "c:chests/wooden"}, + "I": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "CCI" + ], + "result": { + "id": "mcheli:container" + } +} From fc44a111d94594d8eeff83f96ebdcd6766cc3267 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:54 +0800 Subject: [PATCH 0099/1461] Port drafting_table.json to 1.21.1 --- .../data/mcheli/recipe/drafting_table.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/drafting_table.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/drafting_table.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/drafting_table.json new file mode 100644 index 00000000..4429cf70 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/drafting_table.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": {"item": "minecraft:crafting_table"}, + "F": {"tag": "minecraft:wooden_fences"}, + "P": {"tag": "minecraft:planks"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "R ", + "PCP", + "F F" + ], + "result": { + "id": "mcheli:drafting_table" + } +} From 89ae4f7ddaaf6597998f77c9249715a1ed7a270e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:55 +0800 Subject: [PATCH 0100/1461] Port fgm148.json to 1.21.1 --- .../resources/data/mcheli/recipe/fgm148.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fgm148.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fgm148.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fgm148.json new file mode 100644 index 00000000..c9c7c87e --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fgm148.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:glass_blocks/colorless"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "III", + "GR " + ], + "result": { + "id": "mcheli:fgm148" + } +} From 63ee5f32439eeba6e6f35df49406f842d5e32b19 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:56 +0800 Subject: [PATCH 0101/1461] Port fgm148_bullet.json to 1.21.1 --- .../data/mcheli/recipe/fgm148_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fgm148_bullet.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fgm148_bullet.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fgm148_bullet.json new file mode 100644 index 00000000..bd8a2af9 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fgm148_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:gunpowders"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + " R ", + " I ", + " G " + ], + "result": { + "count": 2, + "id": "mcheli:fgm148_bullet" + } +} From f37d8e543818c570a9bafc24dd6e62fdcc9ffd61 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:56 +0800 Subject: [PATCH 0102/1461] Port fim92.json to 1.21.1 --- .../resources/data/mcheli/recipe/fim92.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fim92.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fim92.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fim92.json new file mode 100644 index 00000000..064eac79 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fim92.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:glass_blocks/colorless"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "G ", + "III", + "RI " + ], + "result": { + "id": "mcheli:fim92" + } +} From b2e7485322a38b0d056e2c755001c612c93cbff8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:57 +0800 Subject: [PATCH 0103/1461] Port fim92_bullet.json to 1.21.1 --- .../data/mcheli/recipe/fim92_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fim92_bullet.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fim92_bullet.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fim92_bullet.json new file mode 100644 index 00000000..29bc0018 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fim92_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:gunpowders"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "R ", + " I ", + " G" + ], + "result": { + "count": 2, + "id": "mcheli:fim92_bullet" + } +} From 1734d2a3ab9c07f921918f685ab89e550a7ffe12 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:57 +0800 Subject: [PATCH 0104/1461] Port fuel.json to 1.21.1 --- .../main/resources/data/mcheli/recipe/fuel.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fuel.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fuel.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fuel.json new file mode 100644 index 00000000..99e45469 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/fuel.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": {"item": "minecraft:coal"}, + "I": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "ICI", + "III" + ], + "result": { + "id": "mcheli:fuel" + } +} From cca222fc6483fda55f1af6e44f059baf4ba72119 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:58 +0800 Subject: [PATCH 0105/1461] Port gltd.json to 1.21.1 --- .../resources/data/mcheli/recipe/gltd.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/gltd.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/gltd.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/gltd.json new file mode 100644 index 00000000..b129a876 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/gltd.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": {"tag": "c:storage_blocks/iron"}, + "D": {"tag": "c:gems/diamond"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + " B ", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:gltd" + } +} From da7e1985d3f098c07138f8bab5ce3550730ebf52 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:58 +0800 Subject: [PATCH 0106/1461] Port m18_green.json to 1.21.1 --- .../data/mcheli/recipe/m18_green.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_green.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_green.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_green.json new file mode 100644 index 00000000..66bb96dd --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_green.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"item": "minecraft:green_dye"}, + "Y": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_green", + "minecraft:custom_model_data": 1, + "minecraft:max_stack_size": 1 + } + } +} From 6a997a9853d424df83e502548d37945757b8bad7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:59 +0800 Subject: [PATCH 0107/1461] Port m18_red.json to 1.21.1 --- .../resources/data/mcheli/recipe/m18_red.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_red.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_red.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_red.json new file mode 100644 index 00000000..dafb3688 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_red.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"item": "minecraft:red_dye"}, + "Y": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_red", + "minecraft:custom_model_data": 2, + "minecraft:max_stack_size": 1 + } + } +} From b9157ef7f02df7f3f1e29095541261c2a4ee5652 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:07:59 +0800 Subject: [PATCH 0108/1461] Port m18_violet.json to 1.21.1 --- .../data/mcheli/recipe/m18_violet.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_violet.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_violet.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_violet.json new file mode 100644 index 00000000..7944152c --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_violet.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"item": "minecraft:purple_dye"}, + "Y": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_violet", + "minecraft:custom_model_data": 3, + "minecraft:max_stack_size": 1 + } + } +} From c33c414360820174e8d332a304a82f7cc65b7db1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:00 +0800 Subject: [PATCH 0109/1461] Port m18_yellow.json to 1.21.1 --- .../data/mcheli/recipe/m18_yellow.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_yellow.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_yellow.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_yellow.json new file mode 100644 index 00000000..ca70c1ad --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/m18_yellow.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"item": "minecraft:yellow_dye"}, + "Y": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_yellow", + "minecraft:custom_model_data": 4, + "minecraft:max_stack_size": 1 + } + } +} From 0131203259683cb223cbdf334dd3f4e5d7db7f76 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:01 +0800 Subject: [PATCH 0110/1461] Port parachute.json to 1.21.1 --- .../resources/data/mcheli/recipe/parachute.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/parachute.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/parachute.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/parachute.json new file mode 100644 index 00000000..66118c43 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/parachute.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "S": {"tag": "c:strings"}, + "W": {"tag": "minecraft:wool"} + }, + "pattern": [ + "WWW", + "S S", + " W " + ], + "result": { + "id": "mcheli:parachute" + } +} From 6f56a965ad912881b4ec957b60a3f98caed0ff47 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:01 +0800 Subject: [PATCH 0111/1461] Port rangefinder.json to 1.21.1 --- .../data/mcheli/recipe/rangefinder.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/rangefinder.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/rangefinder.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/rangefinder.json new file mode 100644 index 00000000..eb87cd68 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/rangefinder.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:glass_blocks/colorless"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "III", + "RGR", + "III" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From af3f2fb0894f825fff1632bd717f351683f1fc93 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:02 +0800 Subject: [PATCH 0112/1461] Port rangefinder_recharge.json to 1.21.1 --- .../data/mcheli/recipe/rangefinder_recharge.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json new file mode 100644 index 00000000..23ac1b09 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "equipment", + "ingredients": [ + {"item": "mcheli:rangefinder"}, + {"tag": "c:dusts/redstone"} + ], + "result": { + "id": "mcheli:rangefinder" + } +} From f025169737103c01b4ebf43a5148a5b8dcc2653d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:02 +0800 Subject: [PATCH 0113/1461] Port uav_station.json to 1.21.1 --- .../data/mcheli/recipe/uav_station.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/uav_station.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/uav_station.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/uav_station.json new file mode 100644 index 00000000..47615ed5 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/uav_station.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"tag": "c:gems/diamond"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:storage_blocks/redstone"} + }, + "pattern": [ + "III", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station" + } +} From 66016338127aef4d649f039abdab89443f0505ec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:03 +0800 Subject: [PATCH 0114/1461] Port uav_station2.json to 1.21.1 --- .../data/mcheli/recipe/uav_station2.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/uav_station2.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/uav_station2.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/uav_station2.json new file mode 100644 index 00000000..2281cea7 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/uav_station2.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"tag": "c:gems/diamond"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station2" + } +} From 918b73f5ded8eae0d65836966fc84e219ecbc800 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:03 +0800 Subject: [PATCH 0115/1461] Port wrench.json to 1.21.1 --- .../main/resources/data/mcheli/recipe/wrench.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/recipe/wrench.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/wrench.json b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/wrench.json new file mode 100644 index 00000000..3b3ab88c --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/recipe/wrench.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": {"tag": "c:ingots/iron"} + }, + "pattern": [ + " I ", + " II", + "I " + ], + "result": { + "id": "mcheli:wrench" + } +} From 47a3dce1af4a3c2eab764bf384956bb75f4c62d8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:04 +0800 Subject: [PATCH 0116/1461] Add GameRendererInvoker.java on neoforge 1.21.1 --- .../mcheli/neoforge/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java diff --git a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..7a42c1a1 --- /dev/null +++ b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#loadEffect} private on 1.21.1. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("loadEffect") + void mcheli$loadEffect(ResourceLocation effect); +} From 55da0707b4cce5fcdfd7f042c401f8e2a0897f7e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:05 +0800 Subject: [PATCH 0117/1461] Wire McheliFabric.java into phases 8 to 12 on fabric 1.21.1 --- .../main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java | 1 + 1 file changed, 1 insertion(+) diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java index 382aaccb..16bc7b1d 100644 --- a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java @@ -34,6 +34,7 @@ public void onInitialize() { } CustomIngredientSerializer.register(FabricVehicleIngredient.SERIALIZER); registerPackets(); + CommonHooks.commonSetup(); ServerLifecycleEvents.SERVER_STARTING.register(CommonHooks::serverAboutToStart); ServerLifecycleEvents.SERVER_STOPPED.register(server -> CommonHooks.serverStopped()); From 44745c6fc596d7fe42c352c758abbba9d855bb4f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:05 +0800 Subject: [PATCH 0118/1461] Wire McheliFabricClient.java into phases 8 to 12 on fabric 1.21.1 --- .../jnhprojects/mcheli/fabric/McheliFabricClient.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java index 859952f8..b012a4bb 100644 --- a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java @@ -1,5 +1,10 @@ package com.jnhprojects.mcheli.fabric; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.Minecraft; +import com.jnhprojects.mcheli.fabric.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; import com.jnhprojects.mcheli.client.ClientHooks; import com.jnhprojects.mcheli.client.particle.GlowParticle; import com.jnhprojects.mcheli.network.ModNetwork; @@ -32,9 +37,12 @@ public final class McheliFabricClient implements ClientModInitializer { @Override public void onInitializeClient() { - ClientHooks.init(); + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$loadEffect(effect)); ClientHooks.registerEntityRenderers(EntityRendererRegistry::register); + ClientHooks.registerBlockEntityRenderers(BlockEntityRenderers::register); + ClientHooks.registerMenuScreens(MenuScreens::register); ParticleFactoryRegistry.getInstance().register(ModParticles.GLOW.get(), GlowParticle.Provider::new); + ParticleFactoryRegistry.getInstance().register(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); ClientHooks.keyMappings().forEach(KeyBindingHelper::registerKeyBinding); ResourceManagerHelper resources = ResourceManagerHelper.get(PackType.CLIENT_RESOURCES); ClientHooks.reloadListeners().forEach((id, listener) -> resources.registerReloadListener(new Identified(id, listener))); From 0f5e5a33787ac80b24a1c228e33cf52fda666cc7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:06 +0800 Subject: [PATCH 0119/1461] Wire GameRendererMixin.java into phases 8 to 12 on fabric 1.21.1 --- .../jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java index 09c2fcd5..89dafab3 100644 --- a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli.fabric.mixin; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import com.jnhprojects.mcheli.client.ClientHooks; import com.jnhprojects.mcheli.fabric.RollableCamera; import net.minecraft.client.Camera; @@ -35,6 +36,12 @@ abstract class GameRendererMixin { } } + /** Zoom through a sight, where Forge fires its field of view event. */ + @Inject(method = "getFov", at = @At("RETURN"), cancellable = true) + private void mcheli$getFov(Camera camera, float partialTicks, boolean useFovSetting, CallbackInfoReturnable cir) { + cir.setReturnValue((double) ClientHooks.fov((float) cir.getReturnValueD())); + } + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { ((RollableCamera) camera).mcheli$setRotation(yaw, pitch, roll); } From b11603e9987341674656c7fcdb459ce3e2231292 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:06 +0800 Subject: [PATCH 0120/1461] Wire mcheli.mixins.json into phases 8 to 12 on fabric 1.21.1 --- fabric-1.21.1/src/main/resources/mcheli.mixins.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fabric-1.21.1/src/main/resources/mcheli.mixins.json b/fabric-1.21.1/src/main/resources/mcheli.mixins.json index 1b14b27a..22cab45a 100644 --- a/fabric-1.21.1/src/main/resources/mcheli.mixins.json +++ b/fabric-1.21.1/src/main/resources/mcheli.mixins.json @@ -8,9 +8,11 @@ ], "client": [ "CameraMixin", + "GameRendererInvoker", "GameRendererMixin", "LivingEntityRendererMixin", "MinecraftMixin", + "ParticleEngineMixin", "RuntimeSoundInstanceMixin" ], "injectors": { From fb021f69c7be2f71b0a13dab6369922dbc3ac4e8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:07 +0800 Subject: [PATCH 0121/1461] Wire ForgeClient.java into phases 8 to 12 on forge 1.21.1 --- .../com/jnhprojects/mcheli/forge/ForgeClient.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java index eaf21a8a..fad19b67 100644 --- a/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java +++ b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -1,5 +1,9 @@ package com.jnhprojects.mcheli.forge; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.Minecraft; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; import com.jnhprojects.mcheli.client.CameraAngles; import com.jnhprojects.mcheli.client.ClientHooks; import com.jnhprojects.mcheli.client.particle.GlowParticle; @@ -27,9 +31,14 @@ private ForgeClient() { } static void init(IEventBus modBus) { - ClientHooks.init(); + ClientHooks.init(effect -> Minecraft.getInstance().gameRenderer.loadEffect(effect)); + modBus.addListener((FMLClientSetupEvent event) -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); + modBus.addListener((EntityRenderersEvent.RegisterRenderers event) -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); modBus.addListener((EntityRenderersEvent.RegisterRenderers event) -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); - modBus.addListener((RegisterParticleProvidersEvent event) -> event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new)); + modBus.addListener((RegisterParticleProvidersEvent event) -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); modBus.addListener((RegisterKeyMappingsEvent event) -> ClientHooks.keyMappings().forEach(event::register)); modBus.addListener((RegisterClientReloadListenersEvent event) -> ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); @@ -43,6 +52,7 @@ static void init(IEventBus modBus) { } }); forgeBus.addListener((ViewportEvent.ComputeCameraAngles event) -> ClientHooks.computeCameraAngles(new Angles(event))); + forgeBus.addListener((ViewportEvent.ComputeFov event) -> event.setFOV(ClientHooks.fov((float) event.getFOV()))); forgeBus.addListener((RenderHandEvent event) -> event.setCanceled(ClientHooks.hidesHand())); forgeBus.addListener(EventPriority.NORMAL, false, RenderLivingEvent.Pre.class, event -> ClientHooks.beforeRenderLiving(event.getEntity(), event.getPoseStack(), event.getPartialTick())); From cd765bb29ea2fe5e23c6867976f6796f24677b9c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:07 +0800 Subject: [PATCH 0122/1461] Wire McheliForge.java into phases 8 to 12 on forge 1.21.1 --- .../src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java index f072658a..c878e1b4 100644 --- a/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java +++ b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli.forge; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import com.jnhprojects.mcheli.CommonHooks; import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.item.VehicleIngredient; @@ -38,6 +39,7 @@ public McheliForge(FMLJavaModLoadingContext context) { helper -> helper.register(VehicleIngredient.ID, ForgeVehicleIngredient.SERIALIZER)); }); ForgeNetwork.register(); + modBus.addListener((FMLCommonSetupEvent event) -> event.enqueueWork(CommonHooks::commonSetup)); modBus.addListener((AddPackFindersEvent event) -> event.addRepositorySource(CommonHooks.packSource())); IEventBus forgeBus = MinecraftForge.EVENT_BUS; From 364ced2f50636b26a7663e2759002840597831dd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:08 +0800 Subject: [PATCH 0123/1461] Bring CommonHooks.java on 1.21.1 up to phase 12 --- .../com/jnhprojects/mcheli/CommonHooks.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java index d42f5228..c699c4e8 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java @@ -1,12 +1,16 @@ package com.jnhprojects.mcheli; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.item.ThrowableItem; import com.jnhprojects.mcheli.network.PackSync; +import com.jnhprojects.mcheli.registry.ModItems; import com.mojang.brigadier.CommandDispatcher; import net.minecraft.commands.CommandSourceStack; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.packs.repository.RepositorySource; import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.block.DispenserBlock; import net.minecraft.world.level.storage.LevelResource; import java.util.List; @@ -19,6 +23,11 @@ public final class CommonHooks { private CommonHooks() { } + /** Once the registries are frozen, on the main thread: what vanilla keeps in its own tables. */ + public static void commonSetup() { + DispenserBlock.registerBehavior(ModItems.THROWABLE.get(), new ThrowableItem.DispenseBehavior()); + } + /** * Adds the imported original content to the resource packs (client) and data packs (server). * Without the original zip there is nothing to add. @@ -52,12 +61,16 @@ public static void registerCommands(CommandDispatcher dispat } /** - * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack. The - * original listed them on its drafting table; until that is ported, every MCHeli recipe goes - * into a player's recipe book when they join, so they can be found. + * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack. Every + * MCHeli recipe goes into a player's recipe book when they join: the drafting table lists them + * from there, and they can be found in the crafting table's book too. */ public static void playerLoggedIn(ServerPlayer player) { PackSync.onLogin(player); + // A remote pilot whose drone did not come back with them is shown and made mortal again. + if (!(player.getVehicle() instanceof AircraftEntity uav && uav.isRemotePilot(player))) { + AircraftEntity.clearRemotePilot(player); + } List> ours = player.level().getServer().getRecipeManager().getRecipes().stream() .filter(holder -> holder.id().getNamespace().equals(MCHeli.MOD_ID)) .toList(); From bbd089fd7142b63cd9df5d333154408adf0af8a7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:08 +0800 Subject: [PATCH 0124/1461] Bring ContentPacks.java on 1.21.1 up to phase 12 --- .../com/jnhprojects/mcheli/ContentPacks.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java index c378489c..d3bfdee5 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java @@ -5,6 +5,7 @@ import com.jnhprojects.mcheli.content.pack.ContentSource; import com.jnhprojects.mcheli.content.pack.PackFolder; import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.content.pack.PendingPacks; import net.minecraft.network.chat.Component; import org.jspecify.annotations.Nullable; @@ -12,7 +13,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; /** @@ -30,15 +33,19 @@ */ public final class ContentPacks { private static final int PROBLEMS_IN_CHAT = 8; + private static final String SOUNDS = "assets/mcheli/sounds/"; private final Path folder; + private final PendingPacks pending; private final LegacyContent legacy; private volatile List active = List.of(); private volatile List problems = List.of(); + private volatile Set sounds = Set.of(); private List retired = List.of(); ContentPacks(Path gameDir, LegacyContent legacy) { this.folder = gameDir.resolve(MCHeli.MOD_ID).resolve("packs"); + this.pending = new PendingPacks(gameDir.resolve(MCHeli.MOD_ID).resolve("pending")); this.legacy = legacy; } @@ -106,13 +113,42 @@ public synchronized ContentRegistry load(boolean serverRules) { found.addAll(registry.problems()); close(retired); + PendingPacks.cleanReplaced(folder); retired = active; active = List.copyOf(accepted); problems = List.copyOf(found); + sounds = soundsOf(accepted); MCHeli.LOGGER.info("Content packs in use: {}", active.isEmpty() ? "none" : active); return registry; } + /** Sound names as {@code RuntimeAssets.soundFile} finds them: {@code heli.ogg} and {@code heli1.ogg} are both {@code heli}. */ + private static Set soundsOf(List packs) { + Set names = new HashSet<>(); + for (ContentPack pack : packs) { + for (String file : pack.files()) { + if (file.startsWith(SOUNDS) && file.endsWith(".ogg")) { + String name = file.substring(SOUNDS.length(), file.length() - ".ogg".length()); + names.add(name); + if (name.endsWith("1")) { + names.add(name.substring(0, name.length() - 1)); + } + } + } + } + return Set.copyOf(names); + } + + /** Whether a pack in use brought this sound, so that the sound manager has not heard of it. */ + public boolean hasSound(String name) { + return sounds.contains(name); + } + + /** What players have sent this server, waiting for an operator. */ + public PendingPacks pending() { + return pending; + } + private static @Nullable String refusal(ContentPack pack, long size, long total) { if (!ServerConfig.isPackAllowed(pack.id())) { return "left out, not on the server's allow list (packs.allowList in mcheli-server.toml)"; From 0bae32b5d1a1c056bc3f1ece8d099f00752b0f93 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:09 +0800 Subject: [PATCH 0125/1461] Bring MCHeli.java on 1.21.1 up to phase 12 --- .../src/main/java/com/jnhprojects/mcheli/MCHeli.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java index 21d4185b..1a890651 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java @@ -2,10 +2,13 @@ import com.jnhprojects.mcheli.content.ContentRegistry; import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModBlocks; import com.jnhprojects.mcheli.registry.ModCreativeTabs; import com.jnhprojects.mcheli.registry.ModDataComponents; import com.jnhprojects.mcheli.registry.ModEntities; import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.registry.ModMenus; import com.jnhprojects.mcheli.registry.ModParticles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,9 +40,12 @@ public static void init() { legacyContent = LegacyContent.prepare(platform.gameDir()); packs = new ContentPacks(platform.gameDir(), legacyContent); content = packs.load(false); + ModBlocks.init(); + ModBlockEntities.init(); ModDataComponents.init(); ModEntities.init(); ModParticles.init(); + ModMenus.init(); ModItems.init(content); ModCreativeTabs.init(); } From 0cce7afc7e6f740dcd35e406087ce789f27db503 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:10 +0800 Subject: [PATCH 0126/1461] Bring McheliCommand.java on 1.21.1 up to phase 12 --- .../com/jnhprojects/mcheli/McheliCommand.java | 131 +++++++++++++++++- 1 file changed, 129 insertions(+), 2 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java index f70a7470..1384fcc4 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java @@ -1,15 +1,21 @@ package com.jnhprojects.mcheli; import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.Definition; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.network.PackSubmissions; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.SharedSuggestionProvider; @@ -17,8 +23,11 @@ import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; +import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; /** @@ -28,15 +37,19 @@ * /mcheli content source and counts * /mcheli list [helicopter|plane|...] loaded vehicles * /mcheli packs content packs in use, and their problems -|plane|...] loaded vehicles * /mcheli give <players> <vehicle> [count] (operators) * /mcheli reload re-read the definitions and packs (operators) + * /mcheli pending packs players have sent, waiting (operators) + * /mcheli approve <pack> put a waiting pack in use (operators) + * /mcheli reject <pack> throw a waiting pack away (operators) * */ public final class McheliCommand { private static final List KINDS = List.of("helicopter", "plane", "tank", "vehicle"); private static final DynamicCommandExceptionType UNKNOWN_VEHICLE = new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.unknown_vehicle", name)); + private static final DynamicCommandExceptionType UNKNOWN_PENDING = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.pending.unknown", name)); private McheliCommand() { } @@ -62,7 +75,118 @@ public static void register(CommandDispatcher dispatcher) { .executes(ctx -> give(ctx, IntegerArgumentType.getInteger(ctx, "count"))))))) .then(Commands.literal("reload") .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) - .executes(McheliCommand::reload))); + .executes(McheliCommand::reload)) + .then(Commands.literal("pending") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::pending)) + .then(Commands.literal("approve") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::approve))) + .then(Commands.literal("reject") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::reject)))); + } + + private static CompletableFuture suggestPending(CommandContext ctx, SuggestionsBuilder builder) { + try { + return SharedSuggestionProvider.suggest(MCHeli.packs().pending().list().stream().map(PendingPacks.Submission::id), builder); + } catch (IOException e) { + return builder.buildFuture(); + } + } + + private static int pending(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List waiting; + try { + waiting = MCHeli.packs().pending().list(); + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + if (waiting.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.none"), false); + } + for (PendingPacks.Submission submission : waiting) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.entry", submission.id(), submission.manifest().version(), + submission.manifest().author(), submission.submitter(), "%.1f".formatted(submission.size() / 1048576.0), + String.join(", ", submission.definitions())), false); + List replaced = replaced(submission); + if (!replaced.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.replaces", String.join(", ", replaced)), false); + } + } + return waiting.size(); + } + + /** + * What going live would take the place of: a pack with the same id, and vehicles or weapons of + * another pack with the same file name. That is allowed, so it is the operator's call to make, + * but it should not come as a surprise. + */ + private static List replaced(PendingPacks.Submission submission) { + List names = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.id().equals(submission.id())) { + names.add("pack " + pack.id()); + } + } + ContentRegistry content = MCHeli.content(); + for (String path : submission.definitions()) { + String name = path.substring(path.lastIndexOf('/') + 1, path.length() - ".txt".length()); + Definition existing = path.startsWith("weapons/") ? content.weapon(name) : content.any(name); + if (existing != null && !existing.pack().equals(submission.id())) { + names.add(name + " (" + existing.pack() + ")"); + } + } + return names; + } + + private static PendingPacks.Submission waiting(CommandContext ctx) throws CommandSyntaxException { + String id = StringArgumentType.getString(ctx, "pack"); + PendingPacks.Submission submission; + try { + submission = MCHeli.packs().pending().find(id); + } catch (IOException e) { + submission = null; + } + if (submission == null) { + throw UNKNOWN_PENDING.create(id); + } + return submission; + } + + private static int approve(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + if (PackSubmissions.install(source.getServer(), submission)) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.approve.success", submission.id(), submission.submitter()), true); + return 1; + } + source.sendFailure(Component.translatable("commands.mcheli.approve.left_out", submission.id())); + MCHeli.packs().reportProblems(source::sendFailure); + } catch (IOException | PackException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.getMessage())); + } + return 0; + } + + private static int reject(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + PackSubmissions.reject(source.getServer(), submission); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reject.success", submission.id(), submission.submitter()), true); + return 1; + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } } private static int status(CommandContext ctx) { @@ -92,6 +216,9 @@ private static int packs(CommandContext ctx) { long weapons = content.weapons().stream().filter(def -> def.pack().equals(pack.id())).count(); source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.entry", pack.id(), pack.manifest().version(), pack.manifest().name(), vehicles, weapons, ContentPack.shortHash(pack.hash())), false); + if (!pack.manifest().author().isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.author", pack.manifest().author()), false); + } } MCHeli.packs().reportProblems(source::sendFailure); return packs.size(); From 38206c10d2be4c1830ecd4b882e1b1cc7264849a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:10 +0800 Subject: [PATCH 0127/1461] Bring ModSounds.java on 1.21.1 up to phase 12 --- .../com/jnhprojects/mcheli/ModSounds.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java index 5bbb3225..979dd53e 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java @@ -1,8 +1,11 @@ package com.jnhprojects.mcheli; import net.minecraft.core.Holder; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSoundPacket; import net.minecraft.network.protocol.game.ClientboundSoundPacket; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundSource; @@ -18,8 +21,14 @@ *

The events are not put in the registry. Content files name their sounds freely and a content * pack may bring its own, so they are created on demand as direct holders, which the sound packet * and the sound manager both accept. + * + *

A sound a pack brought is in no {@code sounds.json}, so the sound manager would drop it as + * unknown. For those the server sends our own packet, and the client streams the file. */ public final class ModSounds { + /** How far a sound carries at a volume of one, and per volume above that, as in vanilla. */ + private static final float RANGE = 16.0F; + private static final Map EVENTS = new ConcurrentHashMap<>(); private ModSounds() { @@ -31,17 +40,25 @@ public static SoundEvent get(String name) { /** Plays {@code name} at the entity for everyone nearby. Call on the server. */ public static void playAt(Entity entity, String name, float volume, float pitch) { - entity.level().playSound(null, entity.getX(), entity.getY(), entity.getZ(), get(name), SoundSource.NEUTRAL, volume, pitch); + playAt(entity.level(), entity.getX(), entity.getY(), entity.getZ(), name, volume, pitch); } /** Plays {@code name} at a position for everyone nearby. Call on the server. */ public static void playAt(Level level, double x, double y, double z, String name, float volume, float pitch) { - level.playSound(null, x, y, z, get(name), SoundSource.NEUTRAL, volume, pitch); + if (level instanceof ServerLevel server && MCHeli.packs().hasSound(name)) { + ModNetwork.sendNear(server, x, y, z, RANGE * Math.max(volume, 1.0F), new PackSoundPacket(name, x, y, z, volume, pitch)); + } else { + level.playSound(null, x, y, z, get(name), SoundSource.NEUTRAL, volume, pitch); + } } /** Plays {@code name} for one player only, such as a hit confirmation. */ public static void playTo(ServerPlayer player, String name, float volume, float pitch) { - player.connection.send(new ClientboundSoundPacket(Holder.direct(get(name)), SoundSource.PLAYERS, - player.getX(), player.getY(), player.getZ(), volume, pitch, player.getRandom().nextLong())); + if (MCHeli.packs().hasSound(name)) { + ModNetwork.sendToPlayer(player, new PackSoundPacket(name, player.getX(), player.getY(), player.getZ(), volume, pitch)); + } else { + player.connection.send(new ClientboundSoundPacket(Holder.direct(get(name)), SoundSource.PLAYERS, + player.getX(), player.getY(), player.getZ(), volume, pitch, player.getRandom().nextLong())); + } } } From d706506c5fa7670474fa9512b4de59dcab1e32ac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:11 +0800 Subject: [PATCH 0128/1461] Bring ServerConfig.java on 1.21.1 up to phase 12 --- .../com/jnhprojects/mcheli/ServerConfig.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java index eef7dc3e..9db31738 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java @@ -23,10 +23,13 @@ public final class ServerConfig { private static final ConfigSpec.BooleanValue EXPLOSIONS_START_FIRES; private static final ConfigSpec.DoubleValue VEHICLE_DAMAGE_FACTOR; private static final ConfigSpec.BooleanValue INFINITE_AMMO; + private static final ConfigSpec.BooleanValue INFINITE_FUEL; private static final ConfigSpec.StringListValue DISABLED_VEHICLES; private static final ConfigSpec.StringListValue PACK_ALLOW_LIST; private static final ConfigSpec.IntValue MAX_PACK_SIZE; private static final ConfigSpec.IntValue MAX_TOTAL_SIZE; + private static final ConfigSpec.BooleanValue ALLOW_SUBMISSIONS; + private static final ConfigSpec.BooleanValue REQUIRE_APPROVAL; static { ConfigSpec.Builder builder = ConfigSpec.builder(); @@ -49,6 +52,9 @@ public final class ServerConfig { .comment("Vehicles that cannot be placed, by definition name (the file name, such as \"ah-64\").", "Vehicles already in the world stay.") .defineList("disabled", List.of()); + INFINITE_FUEL = builder + .comment("Whether vehicles never run out of fuel. Original: InfinityFuel") + .define("infiniteFuel", false); builder.pop(); builder.push("weapons"); @@ -70,6 +76,15 @@ public final class ServerConfig { .comment("Most the server loads and sends in all, in megabytes. Packs are taken in id order until", "the next one would go over.") .defineInRange("maxTotalSize", 64, 1, 4096); + ALLOW_SUBMISSIONS = builder + .comment("Whether players may send this server packs they made in the workshop. They wait in mcheli/pending", + "until an operator runs /mcheli approve. Off, players can only get packs from the server's owner.") + .define("allowSubmissions", false); + REQUIRE_APPROVAL = builder + .comment("Whether a submitted pack waits for an operator. Off, any player can add vehicles and weapons for", + "everyone, and even replace existing ones, so only turn it off among people you trust. A pack taking", + "the id of a pack already in use always waits.") + .define("requireApproval", true); builder.pop(); SPEC = builder.build(); @@ -110,6 +125,10 @@ public static boolean infiniteAmmo() { return SPEC.isLoaded() ? INFINITE_AMMO.get() : INFINITE_AMMO.getDefault(); } + public static boolean infiniteFuel() { + return SPEC.isLoaded() ? INFINITE_FUEL.get() : INFINITE_FUEL.getDefault(); + } + /** Whether the allow list lets the server load this pack. An empty list allows every pack. */ public static boolean isPackAllowed(String id) { if (!SPEC.isLoaded() || PACK_ALLOW_LIST.get().isEmpty()) { @@ -131,6 +150,14 @@ public static long maxTotalPackBytes() { return (long) (SPEC.isLoaded() ? MAX_TOTAL_SIZE.get() : MAX_TOTAL_SIZE.getDefault()) << 20; } + public static boolean allowSubmissions() { + return SPEC.isLoaded() ? ALLOW_SUBMISSIONS.get() : ALLOW_SUBMISSIONS.getDefault(); + } + + public static boolean requireApproval() { + return SPEC.isLoaded() ? REQUIRE_APPROVAL.get() : REQUIRE_APPROVAL.getDefault(); + } + public static boolean isDisabled(String vehicle) { if (!SPEC.isLoaded()) { return false; From 123a879bf3308365f3db840ee1d2b3eb23849ae3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:11 +0800 Subject: [PATCH 0129/1461] Bring ClientHooks.java on 1.21.1 up to phase 12 --- .../mcheli/client/ClientHooks.java | 108 +++++++++++++++++- 1 file changed, 105 insertions(+), 3 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java index 6963150e..62612358 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -6,28 +6,54 @@ import com.jnhprojects.mcheli.client.hud.HudScripts; import com.jnhprojects.mcheli.client.render.A10Renderer; import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import com.jnhprojects.mcheli.client.render.ChainRenderer; +import com.jnhprojects.mcheli.client.render.DraftingTableRenderer; +import com.jnhprojects.mcheli.client.render.EquipmentRenderer; import com.jnhprojects.mcheli.client.render.FlareRenderer; +import com.jnhprojects.mcheli.client.render.GltdRenderer; import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.render.ParachuteRenderer; import com.jnhprojects.mcheli.client.render.ProjectileRenderer; import com.jnhprojects.mcheli.client.render.RiderPose; +import com.jnhprojects.mcheli.client.render.ThrowableRenderer; +import com.jnhprojects.mcheli.client.render.UavStationRenderer; +import com.jnhprojects.mcheli.client.screen.DraftingTableScreen; +import com.jnhprojects.mcheli.client.screen.TeamManagerScreen; +import com.jnhprojects.mcheli.client.screen.UavStationScreen; +import com.jnhprojects.mcheli.client.screen.VehicleScreen; import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; +import com.jnhprojects.mcheli.client.workshop.WorkshopClient; import com.jnhprojects.mcheli.entity.ClientAccess; import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.team.Markers; +import com.jnhprojects.mcheli.team.TeamManager; import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.Camera; import net.minecraft.client.DeltaTracker; import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.NoopRenderer; +import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; import java.util.List; import java.util.Map; @@ -44,8 +70,21 @@ public final class ClientHooks { private ClientHooks() { } + /** Loads a post effect into the game renderer, whose own method is private on some loaders. */ + public interface PostEffects { + void load(ResourceLocation effect); + } + + private static PostEffects postEffects = effect -> { + }; + + static PostEffects postEffects() { + return postEffects; + } + /** During mod construction, on a client only. */ - public static void init() { + public static void init(PostEffects effects) { + postEffects = effects; ClientConfig.load(Platform.get().configDir()); ClientAccess.install(() -> Minecraft.getInstance().player, ClientPacks.INSTANCE); } @@ -72,6 +111,35 @@ public static void registerEntityRenderers(EntityRenderers renderers) { renderers.register(ModEntities.DISPENSED_ITEM.get(), ProjectileRenderer::new); renderers.register(ModEntities.FLARE.get(), FlareRenderer::new); renderers.register(ModEntities.A10.get(), A10Renderer::new); + renderers.register(ModEntities.PARACHUTE.get(), ParachuteRenderer::new); + renderers.register(ModEntities.THROWABLE.get(), ThrowableRenderer::new); + renderers.register(ModEntities.GLTD.get(), GltdRenderer::new); + renderers.register(ModEntities.CHAIN.get(), ChainRenderer::new); + renderers.register(ModEntities.CONTAINER.get(), context -> new EquipmentRenderer<>(context, "container", 0.2F, 0xFFBFBFBF, 1.0F)); + renderers.register(ModEntities.UAV_STATION.get(), UavStationRenderer::new); + renderers.register(ModEntities.RAPPEL.get(), NoopRenderer::new); + } + + /** How a loader registers a block entity renderer. */ + public interface BlockEntityRenderers { + void register(BlockEntityType type, BlockEntityRendererProvider provider); + } + + public static void registerBlockEntityRenderers(BlockEntityRenderers renderers) { + renderers.register(ModBlockEntities.DRAFTING_TABLE.get(), DraftingTableRenderer::new); + } + + /** How a loader registers the screen of a menu: vanilla's own method is not public on all of them. */ + public interface MenuScreenRegistrar { + > void register(MenuType type, + MenuScreens.ScreenConstructor screen); + } + + /** On the main thread during client setup, once our menu types are registered. */ + public static void registerMenuScreens(MenuScreenRegistrar screens) { + screens.register(ModMenus.VEHICLE.get(), VehicleScreen::new); + screens.register(ModMenus.UAV_STATION.get(), UavStationScreen::new); + screens.register(ModMenus.DRAFTING_TABLE.get(), DraftingTableScreen::new); } public static List keyMappings() { @@ -86,14 +154,37 @@ public static Map reloadListeners() public static void clientTickEnd() { CrewController.onClientTick(); + GltdController.onClientTick(); + RangefinderController.onClientTick(); + LauncherController.onClientTick(); + VehicleCamera.tick(); WeaponController.onClientTick(); HudRenderer.tick(); EngineSoundPlayer.tick(); + TargetMarkers.tick(); + WorkshopClient.tick(); + while (KeyBindings.TEAM_MANAGER.consumeClick()) { + openTeamManager(); + } + } + + /** Operators get the team manager; anyone else is told it is theirs to open. */ + private static void openTeamManager() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.screen != null) { + return; + } + if (TeamManager.mayManage(mc.player)) { + mc.setScreen(new TeamManagerScreen()); + } else { + mc.player.displayClientMessage(Component.translatable("message.mcheli.team.operators_only"), true); + } } /** Tilts the view with the hull and moves the eye into the cockpit or onto a TV missile. */ public static void computeCameraAngles(CameraAngles angles) { CrewController.onCameraAngles(angles); + GltdController.onCameraAngles(angles); } /** Sets the camera's rotation, roll included, the way Forge's {@code Camera#setRotation} does. */ @@ -169,9 +260,14 @@ public void setRoll(float roll) { } } + /** The field of view for this frame, narrowed while the player zooms through a sight. */ + public static float fov(float fov) { + return LauncherController.fov(RangefinderController.fov(VehicleCamera.fov(fov))); + } + /** Whether to skip drawing the first person hand this frame. */ public static boolean hidesHand() { - return CrewController.hidesHand(); + return CrewController.hidesHand() || GltdController.hidesHand() || RangefinderController.isScoped() || LauncherController.isRaised(); } /** What happens to a press of the use key. */ @@ -185,7 +281,7 @@ public enum UseKey { } public static UseKey onUseKey(InteractionHand hand) { - if (WeaponController.onUseItem()) { + if (WeaponController.onUseItem() || GltdController.onUseItem()) { return UseKey.CONSUME; } return VehiclePicker.onUseItem(hand) ? UseKey.CONSUME_AND_SWING : UseKey.PASS; @@ -202,6 +298,10 @@ public static void beforeRenderLiving(LivingEntity entity, PoseStack pose, float public static void drawHud(GuiGraphics graphics, DeltaTracker delta) { WeaponHud.draw(graphics, delta); + GltdController.drawHud(graphics, delta); + RangefinderController.drawHud(graphics, delta); + LauncherController.drawHud(graphics, delta); + TargetMarkers.draw(graphics, delta); } /** Without the original zip there are no vehicles, so say so where the player will see it. */ @@ -211,5 +311,7 @@ public static void loggedIn(LocalPlayer player) { public static void loggedOut() { ClientPacks.INSTANCE.loggedOut(); + WorkshopClient.loggedOut(); + Markers.clear(); } } From 3c9d26077490b48378ba34d4934a597134fb342e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:12 +0800 Subject: [PATCH 0130/1461] Bring ClientPacks.java on 1.21.1 up to phase 12 --- .../jnhprojects/mcheli/client/ClientPacks.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java index 13f2bad9..d933e518 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java @@ -2,6 +2,7 @@ import com.jnhprojects.mcheli.ClientConfig; import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.sound.OneShotSound; import com.jnhprojects.mcheli.content.ContentRegistry; import com.jnhprojects.mcheli.content.pack.ContentPack; import com.jnhprojects.mcheli.content.pack.ContentSource; @@ -11,6 +12,7 @@ import com.jnhprojects.mcheli.network.PackDataPacket; import com.jnhprojects.mcheli.network.PackListPacket; import com.jnhprojects.mcheli.network.PackRequestPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; import net.minecraft.client.Minecraft; import net.minecraft.network.chat.Component; import com.jnhprojects.mcheli.platform.Platform; @@ -47,8 +49,8 @@ *

The cache is capped by {@code packs.cacheSize} in the client config: after a download, the * packs used longest ago go first. The current server's packs are always kept. */ -final class ClientPacks implements ClientAccess.PackReceiver { - static final ClientPacks INSTANCE = new ClientPacks(); +public final class ClientPacks implements ClientAccess.PackReceiver { + public static final ClientPacks INSTANCE = new ClientPacks(); private static final Pattern CACHED = Pattern.compile("[0-9a-f]{64}\\.zip"); @@ -123,6 +125,17 @@ public void onPackData(PackDataPacket packet) { } } + @Override + public void onPackSound(PackSoundPacket packet) { + Minecraft.getInstance().getSoundManager().play( + new OneShotSound(packet.sound(), packet.x(), packet.y(), packet.z(), packet.volume(), packet.pitch())); + } + + /** The packs the server sent or named, for the package browser. Empty when hosting the game. */ + public List serverPacks() { + return mounted; + } + /** Leaving a server: its packs are no longer needed. */ void loggedOut() { downloads.clear(); From 9481f7d1b3c558ac004b4849308739572d15caef Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:12 +0800 Subject: [PATCH 0131/1461] Bring CrewController.java on 1.21.1 up to phase 12 --- .../mcheli/client/CrewController.java | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java index 18c514c0..54382add 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java @@ -1,6 +1,7 @@ package com.jnhprojects.mcheli.client; import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; import com.jnhprojects.mcheli.client.hud.HudRenderer; import com.jnhprojects.mcheli.content.Vector3; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; @@ -22,6 +23,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.player.Input; import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; import net.minecraft.util.Mth; import net.minecraft.world.phys.Vec3; import org.joml.Vector3f; @@ -99,6 +101,10 @@ static void onClientTick() { tracking = false; viewVehicle = null; viewSeat = -1; + // The GLTD's sight shares the zoom and camera mode; only leaving it resets them. + if (GltdController.gltdOf(player) == null) { + VehicleCamera.reset(); + } return; } int seat = vehicle.seatOf(player); @@ -109,6 +115,7 @@ static void onClientTick() { cameraIndex = 0; gunnerSight = false; tracking = false; + VehicleCamera.reset(); } Input keys = player.input; @@ -155,6 +162,9 @@ static void onClientTick() { while (KeyBindings.SWITCH_SEAT.consumeClick()) { send(vehicle, CrewActionPacket.Action.SWITCH_SEAT); } + while (KeyBindings.DROP_CREW.consumeClick()) { + send(vehicle, pilot ? CrewActionPacket.Action.DROP_CREW : CrewActionPacket.Action.JUMP_OUT); + } while (KeyBindings.SWITCH_CAMERA.consumeClick()) { switchCamera(vehicle, seat); } @@ -164,10 +174,26 @@ static void onClientTick() { } } while (KeyBindings.FOLD.consumeClick()) { - if (pilot) { + if (looksThroughSight(vehicle, seat)) { + VehicleCamera.onZoomKey(player, vehicle); + } else if (pilot) { send(vehicle, CrewActionPacket.Action.FOLD); } } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + // As in the original, the key works the search light in vehicles that have one. + if (vehicle.canSwitchSearchLight(player)) { + send(vehicle, CrewActionPacket.Action.SEARCH_LIGHT); + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 1.0F); + } else { + VehicleCamera.onCameraModeKey(player, vehicle); + } + } + while (KeyBindings.VEHICLE_SCREEN.consumeClick()) { + if (!vehicle.isDestroyed()) { + send(vehicle, CrewActionPacket.Action.VEHICLE_SCREEN); + } + } while (KeyBindings.VTOL.consumeClick()) { if (pilot) { send(vehicle, CrewActionPacket.Action.VTOL); @@ -185,6 +211,18 @@ static void onClientTick() { } } + /** + * Whether this seat sees through a sight that can zoom: a camera position or gunner sight, the + * pilot in gunner mode, or the pilot of a UAV. The zoom key folds the wings everywhere else. + */ + private static boolean looksThroughSight(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount() || def.cameraZoom <= 1) { + return false; + } + return eyePosition(vehicle, def, seat) != null || vehicle.isGunnerMode() || def.uav; + } + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); } @@ -261,7 +299,7 @@ static void onCameraAngles(CameraAngles event) { TvMissileEntity missile = tvCamera(vehicle, seat); if (!(firstPerson && missile != null)) { // Every seat of every kind tilts with the hull, measured along the way the rider looks. - event.setRoll(AircraftRotation.cameraRoll(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), + event.setRoll(AircraftRotation.cameraRoll(vehicle.getViewYRot(partialTicks), vehicle.getViewXRot(partialTicks), vehicle.roll(partialTicks), event.yaw(), event.pitch())); } if (firstPerson && missile != null) { @@ -304,9 +342,9 @@ private static void steerWithMouse(AircraftEntity vehicle, LocalPlayer player, C tracking = false; } if (firstPerson && !freeLook) { - event.setYaw(vehicle.getYRot(partialTicks)); + event.setYaw(vehicle.getViewYRot(partialTicks)); if (!ownPitch) { - event.setPitch(vehicle.getXRot(partialTicks)); + event.setPitch(vehicle.getViewXRot(partialTicks)); } } } @@ -388,7 +426,7 @@ private static void moveCamera(Camera camera, AircraftEntity vehicle, AircraftDe } SeatDefinition s = def.seats.get(seat); Vector3 local = s.rotatingSeat() ? vehicle.onTurret(def, eye, partialTicks) : eye; - Vector3f offset = AircraftRotation.toWorld(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), vehicle.roll(partialTicks), + Vector3f offset = AircraftRotation.toWorld(vehicle.getViewYRot(partialTicks), vehicle.getViewXRot(partialTicks), vehicle.roll(partialTicks), local.x(), local.y(), local.z()); placeCamera(camera, new Vec3(Mth.lerp(partialTicks, vehicle.xo, vehicle.getX()) + offset.x, Mth.lerp(partialTicks, vehicle.yo, vehicle.getY()) + offset.y, @@ -409,7 +447,7 @@ private static void moveCamera(Camera camera, AircraftEntity vehicle, AircraftDe return missile; } - private static void placeCamera(Camera camera, Vec3 at) { + static void placeCamera(Camera camera, Vec3 at) { MethodHandle setter = cameraSetter(); if (setter == null) { return; From 310d773048875e82064b1279aebb904701961015 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:13 +0800 Subject: [PATCH 0132/1461] Bring KeyBindings.java on 1.21.1 up to phase 12 --- .../com/jnhprojects/mcheli/client/KeyBindings.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java index 863771b5..dc594e26 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java @@ -20,7 +20,7 @@ public final class KeyBindings { /** Cycles the pilot's camera positions, or toggles the gunner sight in seats that allow it. */ public static final KeyMapping SWITCH_CAMERA = new KeyMapping("key.mcheli.switch_camera", InputConstants.KEY_H, CATEGORY); public static final KeyMapping LANDING_GEAR = new KeyMapping("key.mcheli.landing_gear", InputConstants.KEY_B, CATEGORY); - /** Folds hatches and wings, sweeps variable geometry wings. */ + /** Zooms through a sight, as the original Z key did; otherwise folds hatches and wings, sweeps variable geometry wings. */ public static final KeyMapping FOLD = new KeyMapping("key.mcheli.fold", InputConstants.KEY_Z, CATEGORY); /** The original used F, which vanilla now uses to swap hands. */ public static final KeyMapping VTOL = new KeyMapping("key.mcheli.vtol", InputConstants.KEY_V, CATEGORY); @@ -32,11 +32,17 @@ public final class KeyBindings { public static final KeyMapping WEAPON_MODE = new KeyMapping("key.mcheli.weapon_mode", InputConstants.KEY_X, CATEGORY); /** The original used V, which VTOL took over. */ public static final KeyMapping FLARE = new KeyMapping("key.mcheli.flare", InputConstants.KEY_C, CATEGORY); - /** The original resupplied from the vehicle's inventory screen, which does not exist yet. */ - public static final KeyMapping RESUPPLY = new KeyMapping("key.mcheli.resupply", InputConstants.KEY_N, CATEGORY); + /** Fuel and parachute slots, reloading and the cargo chest. The original used R, which switches seats here. */ + public static final KeyMapping VEHICLE_SCREEN = new KeyMapping("key.mcheli.vehicle_screen", InputConstants.KEY_I, CATEGORY); + /** Night vision, thermal view and back in vehicles with {@code EnableNightVision}. The original used C, now the flare key. */ + public static final KeyMapping CAMERA_MODE = new KeyMapping("key.mcheli.camera_mode", InputConstants.KEY_K, CATEGORY); + /** The pilot lets the troops down; anyone else aboard slides down a rope or jumps. The original used Y for the pilot's part. */ + public static final KeyMapping DROP_CREW = new KeyMapping("key.mcheli.drop_crew", InputConstants.KEY_Y, CATEGORY); + /** Operators open the team manager, as the original's multiplayer manager key M did. */ + public static final KeyMapping TEAM_MANAGER = new KeyMapping("key.mcheli.team_manager", InputConstants.KEY_M, CATEGORY); public static final List ALL = List.of(FREE_LOOK, SWITCH_CAMERA, LANDING_GEAR, FOLD, VTOL, SWITCH_SEAT, RACK_LOAD, RACK_UNLOAD, - SWITCH_WEAPON, WEAPON_MODE, FLARE, RESUPPLY); + SWITCH_WEAPON, WEAPON_MODE, FLARE, VEHICLE_SCREEN, CAMERA_MODE, DROP_CREW, TEAM_MANAGER); private KeyBindings() { } From 42b0b281a25b5390f3b3d92cf7fb8ff6244fe72d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:14 +0800 Subject: [PATCH 0133/1461] Bring RuntimeAssets.java on 1.21.1 up to phase 12 --- .../mcheli/client/RuntimeAssets.java | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java index 43dcb6de..dfcd0db1 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java @@ -40,6 +40,8 @@ public final class RuntimeAssets { /** Searched in order, so the pack loaded last comes first. Replaced as a whole. */ private static volatile List roots = List.of(); + /** The pack open in the workshop, searched first whether the game has loaded it or not. */ + private static volatile @Nullable Path workshopRoot; /** Every vehicle texture asked for, and whether we uploaded it ourselves. */ private static final Map TEXTURES = new ConcurrentHashMap<>(); @@ -54,6 +56,11 @@ public static void setRoots(List folders) { roots = List.copyOf(folders); } + /** Sets the {@code assets/mcheli} folder of the pack open in the workshop, or {@code null} for none. */ + public static void setWorkshopRoot(@Nullable Path folder) { + workshopRoot = folder; + } + /** Opens an {@code mcheli} asset from the resource packs, or else from a runtime root. */ public static @Nullable InputStream open(ResourceLocation id) throws IOException { Optional resource = Minecraft.getInstance().getResourceManager().getResource(id); @@ -93,6 +100,11 @@ public static void refreshTextures() { } } + /** Uploads one texture again, read fresh from disk: the workshop just replaced its file. */ + public static void refreshTexture(ResourceLocation id) { + TEXTURES.put(id, upload(id)); + } + private static boolean upload(ResourceLocation id) { try (InputStream in = open(id)) { if (in == null) { @@ -130,12 +142,24 @@ public static void clearTextures() { } private static @Nullable Path find(String path) { + Path workshop = workshopRoot; + if (workshop != null) { + Path file = inRoot(workshop, path); + if (file != null) { + return file; + } + } for (Path root : roots) { - Path file = root.resolve(path).normalize(); - if (file.startsWith(root) && Files.isRegularFile(file)) { + Path file = inRoot(root, path); + if (file != null) { return file; } } return null; } + + private static @Nullable Path inRoot(Path root, String path) { + Path file = root.resolve(path).normalize(); + return file.startsWith(root) && Files.isRegularFile(file) ? file : null; + } } From bcfd733eee287f74047ce3d7db200b620ae19a2c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:14 +0800 Subject: [PATCH 0134/1461] Bring WeaponController.java on 1.21.1 up to phase 12 --- .../java/com/jnhprojects/mcheli/client/WeaponController.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java index f5e6111c..d46804e6 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java @@ -92,9 +92,6 @@ static void onClientTick() { click(); } } - while (KeyBindings.RESUPPLY.consumeClick()) { - send(s.vehicle, CrewActionPacket.Action.RESUPPLY); - } if (mc.screen == null && mc.options.keyUse.isDown() && !s.vehicle.isDestroyed()) { fire(s, player); From 02fe2745475f9b7a7b4e4b98dd59a7fc485da921 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:15 +0800 Subject: [PATCH 0135/1461] Bring WeaponHud.java on 1.21.1 up to phase 12 --- .../src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java index 5b9c40cb..347ac6f1 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java @@ -88,7 +88,7 @@ static void draw(GuiGraphics g, DeltaTracker delta) { } else if (set.isInPreparation()) { status = Component.translatable("hud.mcheli.preparing"); } else if (set.magazineSize() > 0 && set.ammo() == 0 && set.restAmmo() == 0) { - status = Component.translatable("hud.mcheli.resupply", KeyBindings.RESUPPLY.getTranslatedKeyMessage()); + status = Component.translatable("hud.mcheli.resupply", KeyBindings.VEHICLE_SCREEN.getTranslatedKeyMessage()); color = RED; } Guidance lock = weapons.guidance(weapons.selected(seat)); From a4018193b5a3f42e61cac71bd52f8030bb57a710 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:15 +0800 Subject: [PATCH 0136/1461] Bring HudState.java on 1.21.1 up to phase 12 --- .../mcheli/client/hud/HudState.java | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java index eb89586e..3f9db748 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli.client.hud; +import com.jnhprojects.mcheli.client.VehicleCamera; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; import com.jnhprojects.mcheli.entity.AircraftEntity; @@ -30,8 +31,7 @@ * the radar sweep, altitude and stick behave as in the original; {@link #update} then fills in the * per frame values before a script runs. * - *

Systems that arrive later in phase 4 read as their idle state for now: fuel is full, the - * camera is in normal view at zoom 1, the pilot gunner mode is off, the vehicle inventory is empty + *

Systems that arrive later read as their idle state for now: the vehicle inventory is empty * and the mortar distance is unknown. */ final class HudState { @@ -47,6 +47,8 @@ final class HudState { final List enemyBlips = new ArrayList<>(); private @Nullable AircraftEntity vehicle; + /** Where the selected mortar would land, or -1. */ + private float mortarDistance = -1.0F; private int radarRotate; private int radarRotateO; private int altitude; @@ -56,6 +58,7 @@ final class HudState { private final double[] stickXSamples = new double[STICK_SAMPLES]; private final double[] stickYSamples = new double[STICK_SAMPLES]; private int stickSample; + private int fuelWarnCount; /** Colour set by the last {@code Color} line. Like the original it carries over between frames. */ int color = 0xFF000000; @@ -174,8 +177,8 @@ void update(AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, v.set(HudVariable.TEST_MODE, 0.0); v.set(HudVariable.PLYR_YAW, Mth.wrapDegrees(player.getViewYRot(partialTicks))); v.set(HudVariable.PLYR_PITCH, player.getViewXRot(partialTicks)); - v.set(HudVariable.YAW, Mth.wrapDegrees(vehicle.getYRot(partialTicks))); - v.set(HudVariable.PITCH, vehicle.getXRot(partialTicks)); + v.set(HudVariable.YAW, Mth.wrapDegrees(vehicle.getViewYRot(partialTicks))); + v.set(HudVariable.PITCH, vehicle.getViewXRot(partialTicks)); v.set(HudVariable.ROLL, Mth.wrapDegrees(vehicle.roll(partialTicks))); v.set(HudVariable.ALTITUDE, altitude); v.set(HudVariable.SEA_ALT, Math.max(vehicle.getY() - vehicle.level().getSeaLevel(), 0.0)); @@ -196,15 +199,15 @@ void update(AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, v.set(HudVariable.MOTION_Y, my); v.set(HudVariable.MOTION_Z, mz); v.set(HudVariable.SPEED, Math.sqrt(mx * mx + my * my + mz * mz)); - v.set(HudVariable.FUEL, 1.0); - v.set(HudVariable.LOW_FUEL, 0.0); + v.set(HudVariable.FUEL, vehicle.fuelRatio()); + v.set(HudVariable.LOW_FUEL, lowFuel(vehicle)); updateStick(seat == 0 && vehicle.steersWithMouse()); updateWeapon(vehicle, seat); v.set(HudVariable.VTOL_STAT, vehicle.vtolMode()); v.set(HudVariable.FREE_LOOK, seat == 0 && vehicle.input().freeLook); v.set(HudVariable.GUNNER_MODE, vehicle.isGunnerMode(player)); - v.set(HudVariable.CAM_MODE, 0.0); - v.set(HudVariable.CAM_ZOOM, 1.0); + v.set(HudVariable.CAM_MODE, VehicleCamera.mode().ordinal()); + v.set(HudVariable.CAM_ZOOM, VehicleCamera.zoom()); v.set(HudVariable.AUTO_PILOT, vehicle instanceof PlaneEntity && vehicle.isGunnerMode(player)); VehicleWeapons weapons = vehicle.weapons(); boolean flares = weapons != null && weapons.flares.isFitted(); @@ -213,7 +216,21 @@ void update(AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, v.set(HudVariable.INVENTORY, 0.0); v.set(HudVariable.HOVERING, vehicle instanceof HelicopterEntity && vehicle.isHoveringMode()); v.set(HudVariable.IS_UAV, def.uav); - v.set(HudVariable.UAV_FS, 0.0); + double uavDistance = vehicle.stationDistance(); + // {@code getUAV_Fs}: signal strength, full at the station and gone 120 blocks out. + v.set(HudVariable.UAV_FS, uavDistance < 0.0 ? 0.0 : 1.0 - Math.min(uavDistance, 120.0) / 120.0); + } + + /** + * {@code isLowFuel}: below 10% the warning blinks, on for eight seconds out of every fourteen. + * Ticks with each update, as the original counted per frame. + */ + private boolean lowFuel(AircraftEntity vehicle) { + if (fuelWarnCount <= 0) { + fuelWarnCount = 280; + } + fuelWarnCount--; + return fuelWarnCount < 160 && vehicle.maxFuel() > 0 && vehicle.fuelRatio() < 0.1F; } /** The original averaged the last four stick positions, scaled to -1..1, with up positive. */ @@ -269,16 +286,19 @@ private void updateWeapon(AircraftEntity vehicle, int seat) { vars.set(HudVariable.IS_HEAT_WPN, def.maxHeatCount > 0); vars.set(HudVariable.RELOADING, set.isInPreparation()); vars.set(HudVariable.DSP_MT_DIST, def.displayMortarDistance); + // The original worked the arc out every fifth tick; it is cheap enough to follow every update. + mortarDistance = def.displayMortarDistance ? (float) set.landingDistance(vehicle) : -1.0F; } else { vars.set(HudVariable.IS_HEAT_WPN, 0.0); vars.set(HudVariable.RELOADING, 0.0); vars.set(HudVariable.DSP_MT_DIST, 0.0); + mortarDistance = -1.0F; } vars.set(HudVariable.RELOAD_TIME, reloadTime); vars.set(HudVariable.WPN_HEAT, heat); vars.set(HudVariable.SIGHT_TYPE, sightType); vars.set(HudVariable.LOCK, lock); - vars.set(HudVariable.MT_DIST, -1.0); + vars.set(HudVariable.MT_DIST, mortarDistance); } boolean hasWeapon() { @@ -317,7 +337,7 @@ boolean hasMagazine() { case WPN_RM_AMMO -> weaponRestAmmo; case RELOAD_PER -> reloadPercent; case RELOAD_SEC -> reloadSeconds; - case MORTAR_DIST -> -1.0F; + case MORTAR_DIST -> mortarDistance; case MC_VER -> "26.3"; case MOD_VER -> Platform.get().modVersion(); case MOD_NAME -> "MCHeli Rework"; @@ -327,8 +347,8 @@ boolean hasMagazine() { case PLYR_YAW -> rotate360(player.getYRot() + 180.0); case PLYR_PITCH -> -player.getXRot(); case TVM_POS_X, TVM_POS_Y, TVM_POS_Z, TVM_DIFF -> tvMissile(arg, vehicle); - case CAM_ZOOM -> 1.0F; - case UAV_DIST -> 0.0; + case CAM_ZOOM -> VehicleCamera.zoom(); + case UAV_DIST -> Math.max(vehicle.stationDistance(), 0.0); case THROTTLE -> vehicle.visibleThrottle() * 100.0; }; } From 00cab373ba50c34c69bb143177470f2bba9e6e70 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:16 +0800 Subject: [PATCH 0137/1461] Bring A10Renderer.java on 1.21.1 up to phase 12 --- .../com/jnhprojects/mcheli/client/render/A10Renderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java index a2126506..5535e184 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java @@ -26,8 +26,8 @@ public void render(A10Entity entity, float entityYaw, float partialTicks, PoseSt Mesh mesh = entity.isVisible() ? MeshCache.INSTANCE.get("", "a-10") : null; if (mesh != null) { pose.pushPose(); - pose.mulPose(Axis.YP.rotationDegrees(-entity.getYRot(partialTicks))); - pose.mulPose(Axis.XP.rotationDegrees(-entity.getXRot(partialTicks))); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + pose.mulPose(Axis.XP.rotationDegrees(-entity.getViewXRot(partialTicks))); MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(TEXTURE)), light, OverlayTexture.NO_OVERLAY); pose.popPose(); } From 56acc585bdae8f6064504b92069b44a28be45803 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:16 +0800 Subject: [PATCH 0138/1461] Bring AircraftRenderState.java on 1.21.1 up to phase 12 --- .../mcheli/client/render/AircraftRenderState.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java index 626907fe..2fcf06d6 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java @@ -18,12 +18,21 @@ public class AircraftRenderState { public float roll; /** Helicopter main rotor or plane propeller angle. */ public float rotorAngle; + /** Each helicopter blade's own angle, per rotor, so folding blades can leave the rotor's spin. */ + public float[][] bladeAngles = new float[0][]; public float throttle; // Part states, 0 to 90 degrees public float canopy; public float landingGear; public float hatch; + /** Rope length below the hooks while troops go down, 0 with the ropes in. */ + public float ropeLength; + /** The beams of the search lights, or false with them off. */ + public boolean searchLight; + /** Where the crew aim a turning search light, in world degrees. */ + public float searchLightYaw; + public float searchLightPitch; public float nozzle; public float wing; @@ -43,6 +52,8 @@ public class AircraftRenderState { public boolean localFirstPerson; /** Shade for the unlit glass, from the light at the vehicle. */ public int glassColor = 0xFFFFFFFF; + /** Drawn inside a screen, as the workshop does, rather than in the world. */ + public boolean inGui; /** Per {@code AddPartWeapon} line, the state of the weapon it belongs to; see {@link WeaponPartState}. */ public WeaponPartState[] weaponParts = new WeaponPartState[0]; From 93556ac6e4644be5c556ed7e6c34601c60dbda4d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:17 +0800 Subject: [PATCH 0139/1461] Bring AircraftRenderer.java on 1.21.1 up to phase 12 --- .../client/render/AircraftRenderer.java | 135 +++++++++++++++++- 1 file changed, 129 insertions(+), 6 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java index 29086b0f..93e7b126 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java @@ -1,20 +1,27 @@ package com.jnhprojects.mcheli.client.render; +import net.minecraft.client.renderer.RenderType; import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.client.RuntimeAssets; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RepellingHook; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.SearchLight; import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; -import com.jnhprojects.mcheli.weapon.VehicleWeapons; -import com.jnhprojects.mcheli.weapon.WeaponSet; import com.jnhprojects.mcheli.entity.AircraftEntity; import com.jnhprojects.mcheli.entity.AnimationState; import com.jnhprojects.mcheli.entity.HelicopterEntity; import com.jnhprojects.mcheli.entity.PartState; import com.jnhprojects.mcheli.entity.PlaneEntity; import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.math.Axis; import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.culling.Frustum; @@ -23,14 +30,23 @@ import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; import net.minecraft.world.phys.AABB; import org.jspecify.annotations.Nullable; +import java.util.List; + /** * Draws any MCHeli vehicle from its MQO/OBJ model, following {@code MCH_RenderAircraft}: the hull, * the kind's own moving parts (rotors, wings, gun mounts) and then the parts every kind can have. */ public class AircraftRenderer extends EntityRenderer { + private static final float[] HULL_COLOR = {0.0F, 0.88F, 1.0F}; + private static final float[] GENERATED_COLOR = {1.0F, 0.75F, 0.0F}; + /** Ropes take the colour of brown wool. */ + private static final ResourceLocation ROPE = ResourceLocation.withDefaultNamespace("textures/block/brown_wool.png"); + private static final float ROPE_WIDTH = 0.03F; + /** Reused every frame; rendering is single threaded. */ private final AircraftRenderState state = new AircraftRenderState(); @@ -45,13 +61,12 @@ private void extractRenderState(T entity, AircraftRenderState state, float parti AircraftDefinition def = entity.definition(); state.definition = def; state.texture = entity.texture(); - state.yaw = entity.getYRot(partialTicks); - state.pitch = entity.getXRot(partialTicks); + state.yaw = entity.getViewYRot(partialTicks); + state.pitch = entity.getViewXRot(partialTicks); state.roll = entity.roll(partialTicks); if (def == null) { return; } - state.rotorAngle = switch (entity) { case HelicopterEntity heli -> heli.rotorAngle(partialTicks); case PlaneEntity plane -> plane.rotorAngle(partialTicks); @@ -60,6 +75,16 @@ private void extractRenderState(T entity, AircraftRenderState state, float parti state.canopy = angle(entity.canopy(), partialTicks); state.landingGear = angle(entity.landingGear(), partialTicks); state.hatch = angle(entity.hatch(), partialTicks); + if (entity instanceof HelicopterEntity heli && def instanceof HelicopterDefinition heliDef) { + bladeAngles(heli, heliDef, state, partialTicks); + } + state.ropeLength = entity.isRepelling() ? entity.ropeLength() : 0.0F; + state.searchLight = entity.isSearchLightOn(); + if (state.searchLight) { + Entity aimer = entity.searchLightAimer(); + state.searchLightYaw = aimer != null ? aimer.getViewYRot(partialTicks) : state.yaw; + state.searchLightPitch = aimer != null ? aimer.getViewXRot(partialTicks) : state.pitch; + } if (entity instanceof PlaneEntity plane) { state.nozzle = angle(plane.nozzle(), partialTicks); state.wing = angle(plane.wing(), partialTicks); @@ -199,6 +224,15 @@ private static AABB cullingBox(AircraftEntity entity) { @Override public void render(T entity, float entityYaw, float partialTicks, PoseStack poseStack, MultiBufferSource buffers, int light) { extractRenderState(entity, state, partialTicks, light); + if (state.definition != null && entityRenderDispatcher.shouldRenderHitBoxes()) { + showHull(entity, state.definition, poseStack, buffers); + } + draw(state, poseStack, buffers); + super.render(entity, entityYaw, partialTicks, poseStack, buffers, light); + } + + /** The vehicle and what hangs off it, from a state alone; the workshop draws a model with no entity this way. */ + public static void draw(AircraftRenderState state, PoseStack poseStack, MultiBufferSource buffers) { AircraftDefinition def = state.definition; Mesh mesh = def != null ? MeshCache.INSTANCE.get(def.folder(), def.name()) : null; if (mesh != null) { @@ -210,9 +244,98 @@ public void render(T entity, float entityYaw, float partialTicks, PoseStack pose poseStack.mulPose(Axis.ZP.rotationDegrees(state.roll)); new PartRenderer(def, mesh, state, poseStack, buffers, texture(def.folder(), state.texture), state.lightCoords, OverlayTexture.NO_OVERLAY, state.glassColor).submitAll(); + if (state.ropeLength < 0.0F) { + ropes(def, state, poseStack, buffers); + } + if (state.searchLight) { + searchLights(def, state, poseStack, buffers); + } poseStack.popPose(); } - super.render(entity, entityYaw, partialTicks, poseStack, buffers, light); + } + + /** + * With F3+B on, outlines the hull boxes next to vanilla's main box: what projectiles hit and + * what other vehicles collide with. The definition's own boxes are cyan, the ones generated + * from the model amber. + */ + private static void showHull(AircraftEntity entity, AircraftDefinition def, PoseStack pose, MultiBufferSource buffers) { + List hull = entity.hullBoxes(); + if (hull.size() < 2) { + return; + } + VertexConsumer lines = buffers.getBuffer(RenderType.lines()); + int listed = def.boundingBoxes.size(); + // The first is the main box, which vanilla draws itself; then the listed boxes, then the generated ones. + for (int i = 1; i < hull.size(); i++) { + float[] c = i <= listed ? HULL_COLOR : GENERATED_COLOR; + LevelRenderer.renderLineBox(pose, lines, hull.get(i).move(-entity.getX(), -entity.getY(), -entity.getZ()), c[0], c[1], c[2], 1.0F); + } + } + + /** + * {@code renderLight}: each lamp's cone. A turning light follows the crew's aim; a fixed one + * points along the vehicle, and a steering one also turns with the front wheels. + */ + private static void searchLights(AircraftDefinition def, AircraftRenderState state, PoseStack pose, MultiBufferSource buffers) { + for (SearchLight light : def.searchLights) { + pose.pushPose(); + pose.translate(light.position().x(), light.position().y(), light.position().z()); + if (!light.fixed()) { + pose.mulPose(Axis.YN.rotationDegrees(state.searchLightYaw - state.yaw + light.yaw())); + pose.mulPose(Axis.XP.rotationDegrees(state.searchLightPitch + 90.0F - state.pitch + light.pitch())); + } else { + float steer = light.steering() ? -state.steer * light.steeringRotation() : 0.0F; + pose.mulPose(Axis.YN.rotationDegrees(light.yaw() + steer)); + pose.mulPose(Axis.XP.rotationDegrees(90.0F + light.pitch())); + } + Glow.cone(pose, buffers, light.height(), light.width() / 2.0F, light.colorStart(), light.colorEnd()); + pose.popPose(); + } + } + + private static void bladeAngles(HelicopterEntity heli, HelicopterDefinition def, AircraftRenderState state, float partialTicks) { + if (state.bladeAngles.length != def.rotors.size()) { + state.bladeAngles = new float[def.rotors.size()][]; + } + for (int r = 0; r < def.rotors.size(); r++) { + RotorDefinition rotor = def.rotors.get(r); + int blades = Math.max(rotor.blades(), 1); + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + if (state.bladeAngles[r] == null || state.bladeAngles[r].length != blades) { + state.bladeAngles[r] = new float[blades]; + } + for (int b = 0; b < blades; b++) { + state.bladeAngles[r][b] = heli.bladeAngle(r, b, step, partialTicks); + } + } + } + + /** {@code renderRope}: a dark line straight down from each hook, in the vehicle's own frame. */ + private static void ropes(AircraftDefinition def, AircraftRenderState state, PoseStack pose, MultiBufferSource buffers) { + int light = state.lightCoords; + float length = state.ropeLength; + VertexConsumer buffer = buffers.getBuffer(RenderType.entityCutout(ROPE)); + PoseStack.Pose p = pose.last(); + for (RepellingHook hook : def.repellingHooks) { + float x = (float) hook.position().x(); + float y = (float) hook.position().y(); + float z = (float) hook.position().z(); + // Two thin crossed strips, so the rope shows from every side. + ropeStrip(p, buffer, x - ROPE_WIDTH, z, x + ROPE_WIDTH, z, y, length, light); + ropeStrip(p, buffer, x, z - ROPE_WIDTH, x, z + ROPE_WIDTH, y, length, light); + } + } + + private static void ropeStrip(PoseStack.Pose p, VertexConsumer buffer, float x0, float z0, float x1, float z1, float y, float length, int light) { + ropeVertex(p, buffer, x0, y, z0, 0.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y, z1, 1.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y + length, z1, 1.0F, 1.0F, light); + ropeVertex(p, buffer, x0, y + length, z0, 0.0F, 1.0F, light); + } + + private static void ropeVertex(PoseStack.Pose p, VertexConsumer buffer, float x, float y, float z, float u, float v, int light) { + buffer.addVertex(p, x, y, z).setColor(0xFFFFFFFF).setUv(u, v).setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(p, 0.0F, 1.0F, 0.0F); } /** Vehicles bind their own textures per part; this is only for vanilla's bookkeeping. */ From 8a52fe3416c1d922b0ab7e2d8407487f7b61e28f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:18 +0800 Subject: [PATCH 0140/1461] Bring GlassSplit.java on 1.21.1 up to phase 12 --- .../mcheli/client/render/GlassSplit.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java index f80e3aeb..5be980cd 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java @@ -2,6 +2,7 @@ import com.jnhprojects.mcheli.client.RuntimeAssets; import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; import com.mojang.blaze3d.platform.NativeImage; import net.minecraft.resources.ResourceLocation; import org.slf4j.Logger; @@ -45,6 +46,7 @@ int at(float u, float v) { private final Map> textures = new HashMap<>(); private final Map, Map> splits = new IdentityHashMap<>(); + private final Map, Map, List>> buckets = new IdentityHashMap<>(); private GlassSplit() { } @@ -57,9 +59,20 @@ Split split(List groups, ResourceLocation texture) { return splits.computeIfAbsent(groups, g -> new HashMap<>()).computeIfAbsent(texture, t -> compute(groups, t)); } + /** + * A part's objects by the texture each draws with, for a vehicle with {@code PartTexture} lines. + * The lists stay the same objects from frame to frame, so {@link #split} can remember them too. + * Called from the render thread only. + */ + List buckets(List groups, Map overrides) { + return buckets.computeIfAbsent(groups, g -> new HashMap<>()) + .computeIfAbsent(Map.copyOf(overrides), o -> PartTextures.assign(groups, o)); + } + void clear() { textures.clear(); splits.clear(); + buckets.clear(); } private Split compute(List groups, ResourceLocation texture) { From a67292937d65c36dd1008e0efc7e2757b458e247 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:18 +0800 Subject: [PATCH 0141/1461] Bring Glow.java on 1.21.1 up to phase 12 --- .../mcheli/client/render/Glow.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java index 305050d0..baed9729 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java @@ -22,6 +22,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; import net.minecraft.world.level.Level; import net.minecraft.world.level.LightLayer; import net.minecraft.world.phys.Vec3; @@ -55,6 +56,27 @@ public String toString() { private static final RenderType HALO = Types.halo(); + /** + * The particle render type for {@code mcheli:smoke}: vanilla's translucent particles with depth + * writes off. A puff's clear edge would otherwise hide the puffs drawn after it, and a smoke + * cloud is mostly overlapping puffs. + */ + public static final ParticleRenderType SMOKE_LAYER = new ParticleRenderType() { + @Override + public BufferBuilder begin(Tesselator tesselator, TextureManager textures) { + RenderSystem.depthMask(false); + RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_PARTICLES); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + return tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE); + } + + @Override + public String toString() { + return "MCHELI_SMOKE"; + } + }; + private static final int FULL_BRIGHT = LightTexture.FULL_BRIGHT; private Glow() { @@ -137,6 +159,25 @@ public static void streak(PoseStack pose, MultiBufferSource buffers, Vec3 toCame vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); } + /** + * A search light's beam, {@code renderLight}: a cone {@code height} long along +Y to a disc + * {@code radius} across, fading from {@code start} at the lamp to {@code end} at the rim (ARGB). + */ + public static void cone(PoseStack pose, MultiBufferSource buffers, float height, float radius, int start, int end) { + VertexConsumer buffer = buffers.getBuffer(HALO); + PoseStack.Pose p = pose.last(); + int segments = 24; + for (int i = 0; i < segments; i++) { + float a0 = i * Mth.TWO_PI / segments; + float a1 = (i + 1) * Mth.TWO_PI / segments; + // A quad with the apex twice is the fan's triangle; the texture's bright middle colours it. + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, Mth.sin(a0) * radius, height, Mth.cos(a0) * radius, 0.5F, 0.5F, end); + vertex(buffer, p, Mth.sin(a1) * radius, height, Mth.cos(a1) * radius, 0.5F, 0.5F, end); + } + } + private static int alpha(float strength) { return Math.clamp(Math.round(strength * 255.0F), 0, 255); } From 3bda4cce596d822dc0a965724a1e899791a8d7fc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:19 +0800 Subject: [PATCH 0142/1461] Bring PartRenderer.java on 1.21.1 up to phase 12 --- .../mcheli/client/render/PartRenderer.java | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java index c367447b..09c4faf3 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java @@ -1,5 +1,7 @@ package com.jnhprojects.mcheli.client.render; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; import com.jnhprojects.mcheli.content.Vector3; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; @@ -22,7 +24,9 @@ import com.jnhprojects.mcheli.content.aircraft.part.WingPart; import com.jnhprojects.mcheli.model.Mesh; import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.math.Axis; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; @@ -82,19 +86,19 @@ void submitAll() { nozzles(plane); wings(plane); propellers(plane); - submit(mesh.body()); + hull(); } case HelicopterDefinition heli -> { - submit(mesh.body()); + hull(); rotors(heli); } case VehicleDefinition vehicle -> { - submit(mesh.body()); + hull(); for (VehiclePart part : vehicle.parts) { vehiclePart(vehicle, part); } } - default -> submit(mesh.body()); + default -> hull(); } weapons(); rotatingParts(); @@ -110,10 +114,23 @@ void submitAll() { movingParts(def.canopies, state.canopy); } + /** + * The body and, as a {@code $} object of its own so it can have its own {@code PartTexture}, + * the cockpit. A model without {@code $body} draws as one piece, cockpit included, and no original + * model has a {@code $cockpit}. + */ + private void hull() { + submit(mesh.body()); + if (mesh.contains("$body")) { + submit(mesh.part("$cockpit")); + } + } + // Kind specific parts private void rotors(HelicopterDefinition heli) { - for (RotorDefinition rotor : heli.rotors) { + for (int r = 0; r < heli.rotors.size(); r++) { + RotorDefinition rotor = heli.rotors.get(r); List blade = part(rotor.part()); if (blade.isEmpty() || isZero(rotor.axis())) { continue; @@ -124,7 +141,8 @@ private void rotors(HelicopterDefinition heli) { pose.pushPose(); Vector3 p = rotor.position(); translate(p, 1.0F); - rotate(state.rotorAngle + i * step, rotor.axis()); + float[] angles = r < state.bladeAngles.length ? state.bladeAngles[r] : null; + rotate(angles != null && i < angles.length ? angles[i] : state.rotorAngle + i * step, rotor.axis()); if (!rotor.legacyPivot()) { translate(p, -1.0F); } @@ -480,12 +498,34 @@ private void submit(List groups) { if (groups.isEmpty()) { return; } + if (def.partTextures.isEmpty()) { + submit(groups, texture, opaqueType, glassType); + return; + } + for (PartTextures.Bucket bucket : GlassSplit.INSTANCE.buckets(groups, def.partTextures)) { + if (bucket.texture() == null) { + submit(bucket.groups(), texture, opaqueType, glassType); + } else { + ResourceLocation own = RuntimeAssets.texture(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, + "textures/" + def.folder() + "/" + bucket.texture() + ".png")); + submit(bucket.groups(), own, RenderType.entityCutout(own), RenderType.entityTranslucentEmissive(own)); + } + } + } + + private void submit(List groups, ResourceLocation texture, RenderType opaqueType, RenderType glassType) { GlassSplit.Split split = GlassSplit.INSTANCE.split(groups, texture); if (!split.opaque().isEmpty()) { MeshRenderer.emit(split.opaque(), pose.last(), buffers.getBuffer(opaqueType), light, overlay); } if (!split.glass().isEmpty()) { - MeshRenderer.emitFrontFaces(split.glass(), pose.last(), buffers.getBuffer(glassType), light, overlay, glassColor); + VertexConsumer buffer = buffers.getBuffer(glassType); + // Culling glass relies on the camera sitting at the origin, which is not so in a GUI. + if (state.inGui) { + MeshRenderer.emit(split.glass(), pose.last(), buffer, light, overlay, glassColor); + } else { + MeshRenderer.emitFrontFaces(split.glass(), pose.last(), buffer, light, overlay, glassColor); + } } } From c7c479df389ec2a7f8dee9c9059582c1141fd0d3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:19 +0800 Subject: [PATCH 0143/1461] Bring ProjectileRenderer.java on 1.21.1 up to phase 12 --- .../mcheli/client/render/ProjectileRenderer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java index 5a34b6ee..f4c2abb7 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java @@ -82,15 +82,15 @@ private static void extractRenderState(ProjectileEntity entity, State state, flo state.y = Mth.lerp(partialTicks, entity.yo, entity.getY()); state.z = Mth.lerp(partialTicks, entity.zo, entity.getZ()); state.model = entity.model(); - state.yaw = entity.getYRot(partialTicks); - state.pitch = entity.getXRot(partialTicks); - state.visible = !(entity instanceof BombEntity bomb) || bomb.isVisible(); + state.yaw = entity.getViewYRot(partialTicks); + state.pitch = entity.getViewXRot(partialTicks); + state.visible = !entity.isSpent() && (!(entity instanceof BombEntity bomb) || bomb.isVisible()); WeaponDefinition w = entity.weapon(); boolean inWater = inWater(entity.level(), entity); WeaponColor c = w != null ? (inWater ? w.colorInWater : w.color) : WeaponColor.WHITE; state.color = c.argb(); - state.glowKind = inWater ? GlowKind.NONE : glowKind(entity); + state.glowKind = inWater || entity.isSpent() ? GlowKind.NONE : glowKind(entity); if (state.glowKind != GlowKind.NONE) { state.glow = Glow.strength(entity.level(), entity.blockPosition()); state.travel = new Vec3(entity.getX() - entity.xo, entity.getY() - entity.yo, entity.getZ() - entity.zo); From 7a5be7a9ec077b8e6e4a9e20e90ece0d9dc9b64e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:20 +0800 Subject: [PATCH 0144/1461] Bring RiderPose.java on 1.21.1 up to phase 12 --- .../java/com/jnhprojects/mcheli/client/render/RiderPose.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java index 7b2b5872..98ff23fd 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java @@ -27,8 +27,8 @@ public static void beforeRender(LivingEntity rider, PoseStack pose, float partia if (!(rider instanceof Player) || !(rider.getVehicle() instanceof AircraftEntity vehicle)) { return; } - float yaw = vehicle.getYRot(partialTicks); - float pitch = vehicle.getXRot(partialTicks); + float yaw = vehicle.getViewYRot(partialTicks); + float pitch = vehicle.getViewXRot(partialTicks); float roll = vehicle.roll(partialTicks); if (pitch == 0.0F && roll == 0.0F) { return; From 40f572c6a7d6c266a9665be8dffa14af5890bdd5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:20 +0800 Subject: [PATCH 0145/1461] Bring EngineSound.java on 1.21.1 up to phase 12 --- .../mcheli/client/sound/EngineSound.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java index 8f91e640..b32040f5 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java @@ -2,6 +2,7 @@ import com.jnhprojects.mcheli.ModSounds; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; import com.jnhprojects.mcheli.entity.AircraftEntity; import com.jnhprojects.mcheli.entity.EngineSoundSource; import net.minecraft.client.Minecraft; @@ -10,25 +11,29 @@ import net.minecraft.util.RandomSource; /** - * The looping engine sound of one vehicle. Ported from {@code MCH_SoundUpdater}. + * A looping sound of one vehicle: its engine, or the rotor loop some vehicles have alongside it. + * Ported from {@code MCH_SoundUpdater}. * - *

Volume and pitch follow the vehicle's throttle every tick. For listeners outside the vehicle - * the sound is placed between them and the vehicle, so that the 16 block range of a normal sound - * stretches to the definition's {@code SoundRange}, and the pitch bends slightly as the vehicle - * approaches or leaves, which is what the original did. + *

Volume and pitch follow the vehicle's throttle every tick, the pitch along the definition's + * curve if it has one and otherwise the way the kind of vehicle always has. For listeners outside + * the vehicle the sound is placed between them and the vehicle, so that the 16 block range of a + * normal sound stretches to the definition's {@code SoundRange}, and the pitch bends slightly as the + * vehicle approaches or leaves, which is what the original did. */ final class EngineSound extends RuntimeSoundInstance { private static final float MAX_DOPPLER = 0.2F; private final AircraftEntity vehicle; private final EngineSoundSource source; + private final boolean rotor; private float doppler; private double lastDistance = -1.0; - EngineSound(AircraftEntity vehicle, EngineSoundSource source, String sound) { + EngineSound(AircraftEntity vehicle, EngineSoundSource source, String sound, boolean rotor) { super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); this.vehicle = vehicle; this.source = source; + this.rotor = rotor; this.looping = true; this.delay = 0; update(); @@ -53,8 +58,9 @@ private void update() { volume = 0.0F; return; } - volume = def.soundVolume * source.engineVolume(); - float pitch = def.soundPitch * source.enginePitch(); + volume = (rotor ? def.rotorSoundVolume : def.soundVolume) * source.engineVolume(); + PitchCurve curve = rotor ? def.rotorSoundPitchCurve : def.soundPitchCurve; + float pitch = def.soundPitch * (curve != null ? curve.at(vehicle.visibleThrottle()) : source.enginePitch()); boolean aboard = player.getRootVehicle() == vehicle.getRootVehicle(); if (aboard) { x = vehicle.getX(); From 545aedc2d04598a97be4fdc7afe25743bba23c9e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:21 +0800 Subject: [PATCH 0146/1461] Bring EngineSoundPlayer.java on 1.21.1 up to phase 12 --- .../client/sound/EngineSoundPlayer.java | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java index b0d46e30..62e52284 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java @@ -12,15 +12,16 @@ import java.util.Map; /** - * Starts engine loops for vehicles near the player and restarts them after they fall silent. - * The sounds themselves track their vehicle until it stops or disappears. + * Starts engine and rotor loops for vehicles near the player and restarts them after they fall + * silent. The sounds themselves track their vehicle until it stops or disappears. */ public final class EngineSoundPlayer { /** Ticks to wait before restarting a loop, as the original did, so a stuttering engine does not spam. */ private static final int RESTART_DELAY = 20; - private static final Map PLAYING = new HashMap<>(); - private static final Map COOLDOWN = new HashMap<>(); + /** By {@link #key}, since a vehicle can have two loops. */ + private static final Map PLAYING = new HashMap<>(); + private static final Map COOLDOWN = new HashMap<>(); private EngineSoundPlayer() { } @@ -44,22 +45,31 @@ public static void tick() { if (!(entity instanceof AircraftEntity vehicle) || !(entity instanceof EngineSoundSource source)) { continue; } - int id = vehicle.getId(); AircraftDefinition def = vehicle.definition(); - if (def == null || PLAYING.containsKey(id) || COOLDOWN.containsKey(id)) { + if (def == null) { continue; } - String sound = def.soundOrDefault(); double range = def.soundRange * 1.2; - if (sound.isEmpty() || vehicle.distanceToSqr(player) > range * range) { + if (vehicle.distanceToSqr(player) > range * range) { continue; } - if (def.soundVolume * source.engineVolume() > 0.01F) { - EngineSound loop = new EngineSound(vehicle, source, sound); - mc.getSoundManager().play(loop); - PLAYING.put(id, loop); - COOLDOWN.put(id, RESTART_DELAY); - } + start(mc, vehicle, source, def.soundOrDefault(), def.soundVolume, false); + start(mc, vehicle, source, def.rotorSound, def.rotorSoundVolume, true); } } + + private static void start(Minecraft mc, AircraftEntity vehicle, EngineSoundSource source, String sound, float volume, boolean rotor) { + long key = key(vehicle, rotor); + if (sound.isEmpty() || PLAYING.containsKey(key) || COOLDOWN.containsKey(key) || volume * source.engineVolume() <= 0.01F) { + return; + } + EngineSound loop = new EngineSound(vehicle, source, sound, rotor); + mc.getSoundManager().play(loop); + PLAYING.put(key, loop); + COOLDOWN.put(key, RESTART_DELAY); + } + + private static long key(AircraftEntity vehicle, boolean rotor) { + return (long) vehicle.getId() << 1 | (rotor ? 1L : 0L); + } } From 29e30b4efc06de91886f0293ea9cba3c1e167894 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:22 +0800 Subject: [PATCH 0147/1461] Bring AircraftEntity.java on 1.21.1 up to phase 12 --- .../mcheli/entity/AircraftEntity.java | 1061 ++++++++++++++++- 1 file changed, 1017 insertions(+), 44 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java index bd9c1c80..f628a2cf 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java @@ -12,9 +12,17 @@ import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.WrenchItem; +import com.jnhprojects.mcheli.menu.VehicleMenu; import com.jnhprojects.mcheli.particle.GlowParticleOptions; import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.jnhprojects.mcheli.physics.CrashSpin; import com.jnhprojects.mcheli.physics.GroundImpact; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.supply.SupplyRules; import com.jnhprojects.mcheli.weapon.Explosions; import com.jnhprojects.mcheli.weapon.VehicleWeapons; import com.jnhprojects.mcheli.weapon.WeaponSet; @@ -24,6 +32,7 @@ import net.minecraft.nbt.ListTag; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; @@ -36,17 +45,20 @@ import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleMenuProvider; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.damagesource.DamageTypes; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.MoverType; import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; @@ -98,6 +110,14 @@ public abstract class AircraftEntity extends Entity { private static final EntityDataAccessor DATA_TV_MISSILE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); private static final EntityDataAccessor DATA_FLARE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor DATA_SINKING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_FUEL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + /** The UAV station a drone is flown from, by entity id, or 0. */ + private static final EntityDataAccessor DATA_STATION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_REPELLING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SEARCH_LIGHT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + + /** Marks a player flying a drone from a station, so a stale hide can be undone at login. */ + public static final String REMOTE_PILOT_TAG = "mcheli_remote_pilot"; // Bits of DATA_PARTS, the same assignment the original used for its part status word. protected static final int PART_CANOPY = 0; @@ -140,6 +160,20 @@ public abstract class AircraftEntity extends Entity { private int pendingSeat = EMPTY; private final Map savedSeats = new HashMap<>(); private final Map lastSeats = new HashMap<>(); + /** The station a drone is flown from, and where its pilot goes back to. Server only. */ + private @Nullable UUID station; + private @Nullable Vec3 stationReturn; + /** Remote pilots leaving this tick, by entity id, with where they come back down. */ + private final Map remoteReturns = new HashMap<>(); + /** Where a workshop test flight started, or {@code null} for an ordinary vehicle. Server only. */ + private @Nullable Vec3 testFlightStart; + private boolean testFlightOver; + /** How far the ropes hang below their hooks, as a negative number; both sides let them out. */ + private float ropeLength; + private int repellingTicks; + private int lastRope; + /** Passengers are leaving one by one under parachutes. Server only. */ + private boolean droppingTroops; private @Nullable VehicleWeapons weapons; private int @Nullable [] savedAmmo; @@ -149,6 +183,8 @@ public abstract class AircraftEntity extends Entity { /** Damage factor of the extra hit box the last projectile ray struck, {@code lastBBDamageFactor}. */ private float hitBoxFactor = 1.0F; private int timeSinceHit; + /** The fuel load from which an aircraft counts as heavy: the H8K flying boat and up. */ + private static final int HEAVY_FUEL = 3000; /** How long a wreck stays, burning, before it disappears: 30 seconds. */ private static final int WRECK_TICKS = 600; /** Ticks a server position update is eased in over. */ @@ -165,9 +201,8 @@ public abstract class AircraftEntity extends Entity { private double lerpXRot; /** Ticks the client has seen this vehicle as a wreck, for the fire to die down on time. */ private int wreckTicks; - private float destroyedYaw; - private float destroyedPitch; - private float destroyedRoll; + /** Ticks since this aircraft was shot down, for the crash spin. Counted on both sides. */ + private int spinTicks; private boolean exploded; /** Blasts still to come while a wrecked boat cooks off, and ticks until the next one. */ private int cookOffBlasts; @@ -175,9 +210,23 @@ public abstract class AircraftEntity extends Entity { private @Nullable Entity cookOffCause; private int alertCount; + /** Fuel burnt but not yet taken from the tank, which only holds whole units. Server only. */ + private double fuelBurnt; + /** Ticks a fuel truck nearby keeps this vehicle from burning fuel. Server only. */ + private int fuelSuppliedCount; + /** One bit per seat whose occupant has night vision on. Server only, not saved. */ + private int nightVisionSeats; + /** Fuel and parachute slots and the cargo chest. Only the server's copy holds anything. */ + private final VehicleInventory inventory = new VehicleInventory(this); + // Crashes, measured from position so they work whichever side simulates the vehicle. /** Blocks per tick into terrain, sideways, that count as a crash: about 25 m/s. */ private static final double TERRAIN_CRASH_SPEED = 1.25; + /** + * How far a hull reaches past its main box, at most, when looking for neighbours: other + * vehicles are found by their main box, and the longest hulls stretch about this far from it. + */ + private static final double HULL_REACH = 24.0; /** Closing speed between two vehicles that counts as a crash: about 10 m/s. */ private static final double VEHICLE_CRASH_SPEED = 0.5; /** Highest point since a ground vehicle left the ground, or NaN while it is on it. */ @@ -210,6 +259,10 @@ protected void defineSynchedData(SynchedEntityData.Builder builder) { builder.define(DATA_TV_MISSILE, 0); builder.define(DATA_FLARE, false); builder.define(DATA_SINKING, false); + builder.define(DATA_FUEL, 0); + builder.define(DATA_STATION, 0); + builder.define(DATA_REPELLING, false); + builder.define(DATA_SEARCH_LIGHT, false); } // Definition @@ -274,6 +327,7 @@ public void setType(AircraftDefinition definition) { entityData.set(DATA_TYPE, definition.name()); entityData.set(DATA_TEXTURE, definition.textures().getFirst()); entityData.set(DATA_HP, definition.maxHp); + entityData.set(DATA_FUEL, definition.maxFuel); refreshDimensions(); definition(); } @@ -291,6 +345,261 @@ public int hp() { return entityData.get(DATA_HP); } + /** + * {@code repair}: adds hit points up to the maximum. A wreck stays a wreck. Server only. + * + * @return whether anything was repaired + */ + public boolean repair(int amount) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || hp() >= def.maxHp) { + return false; + } + entityData.set(DATA_HP, Math.min(hp() + amount, def.maxHp)); + return true; + } + + /** + * {@code switchNextTextureName}: the next paint scheme the definition lists. Server only. + * + * @return false when the vehicle has only one + */ + public boolean nextTexture() { + AircraftDefinition def = definition(); + if (def == null || def.textures().size() < 2) { + return false; + } + List textures = def.textures(); + int next = (textures.indexOf(texture()) + 1) % textures.size(); + entityData.set(DATA_TEXTURE, textures.get(next)); + return true; + } + + // Fuel + + public int fuel() { + return entityData.get(DATA_FUEL); + } + + public int maxFuel() { + AircraftDefinition def = definition(); + return def != null ? def.maxFuel : 0; + } + + /** {@code getFuelP}: how full the tank is, 0 to 1. Vehicles without a tank read as full. */ + public float fuelRatio() { + int max = maxFuel(); + return max <= 0 ? 1.0F : (float) fuel() / max; + } + + /** {@code canUseFuel}: a vehicle without a tank never runs dry. */ + public boolean hasFuel() { + return maxFuel() <= 0 || fuel() > 1; + } + + /** Sets the tank outright; a drone leaves its station with 5%. Server only. */ + public void setFuelLevel(int fuel) { + entityData.set(DATA_FUEL, Math.clamp(fuel, 0, Math.max(maxFuel(), 0))); + } + + /** + * Pours up to {@code amount} into the tank. Server only. + * + * @return how much went in + */ + public int refuel(int amount) { + int added = Math.clamp(maxFuel() - fuel(), 0, Math.max(amount, 0)); + if (added > 0) { + entityData.set(DATA_FUEL, fuel() + added); + } + return added; + } + + // Night vision + + /** A crew member switched night vision on or off. Server only. */ + public void setNightVision(Player player, boolean on) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (seat < 0 || seat >= 31) { + return; + } + if (on && def != null && def.nightVision && !isDestroyed()) { + nightVisionSeats |= 1 << seat; + } else if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(player); + } + } + + /** Keeps night vision on for everyone in the crew who switched it on, see {@link NightVision}. */ + private void tickNightVision() { + if (nightVisionSeats == 0) { + return; + } + if (isDestroyed()) { + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null) { + NightVision.end(crew); + } + } + nightVisionSeats = 0; + return; + } + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (hasNightVision(seat) && crew != null) { + NightVision.topUp(crew); + } + } + } + + /** Only the first 31 seats can switch it on; racks and the like come after them anyway. */ + private boolean hasNightVision(int seat) { + return seat >= 0 && seat < 31 && (nightVisionSeats & 1 << seat) != 0; + } + + /** + * {@code updateFuel}: once a second the engine burns its {@code FuelConsumption}, scaled by the + * throttle and topping out at 70% throttle. Nothing burns next to a fuel truck or with the + * server's infinite fuel on. + */ + private void burnFuel(AircraftDefinition def) { + if (def.maxFuel <= 0 || isDestroyed()) { + return; + } + if (fuelSuppliedCount > 0) { + fuelSuppliedCount--; + return; + } + if (ticksAlive % 20 != 0 || fuel() <= 1 || throttle <= 0.0 || ServerConfig.infiniteFuel()) { + return; + } + fuelBurnt += Math.min(throttle * 1.4, 1.0) * def.fuelConsumption; + if (fuelBurnt > 1.0) { + int whole = (int) fuelBurnt; + fuelBurnt -= whole; + entityData.set(DATA_FUEL, Math.max(fuel() - whole, 0)); + } + } + + /** + * {@code supplyFuel}: a fuel truck ({@code FuelSupplyRange}) tops up every vehicle within range, + * 30 units every half second. While it is on the ground it only reaches vehicles that are on + * the ground too. + */ + private void supplyFuel(AircraftDefinition def) { + float range = def.fuelSupplyRange; + if (range <= 0.0F || ticksAlive % 10 != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + if (other == this) { + continue; + } + if (!onGround() || other.onGround()) { + other.refuel(30); + } + other.fuelSuppliedCount = 40; + } + } + + // Inventory and supply + + public VehicleInventory inventory() { + return inventory; + } + + /** + * {@code canSupply}: fuel cans and resupply only work while the vehicle stands on something, + * within three blocks; a vehicle that cannot float must not be in the water either. + */ + public boolean canSupply() { + AircraftDefinition def = definition(); + return def != null && hasBlockBelow(1, 3, false) && (def.floats || !isInWater()); + } + + /** {@code openGui}: shows a crew member the vehicle screen. Server only. */ + public void openScreen(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || player.getVehicle() != this) { + return; + } + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new VehicleMenu(id, playerInventory, this), displayName(def))); + } + + /** + * {@code openInventory}: shows the cargo chest of a vehicle with an {@code InventorySize}. + * Server only. + * + * @return false when the vehicle has none + */ + public boolean openCargo(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || def.cargoRows() <= 0 || isDestroyed() || !inventory.stillValid(player)) { + return false; + } + int rows = Math.min(def.cargoRows(), 6); + MenuType type = switch (rows) { + case 1 -> MenuType.GENERIC_9x1; + case 2 -> MenuType.GENERIC_9x2; + case 3 -> MenuType.GENERIC_9x3; + case 4 -> MenuType.GENERIC_9x4; + case 5 -> MenuType.GENERIC_9x5; + default -> MenuType.GENERIC_9x6; + }; + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new ChestMenu(type, id, playerInventory, inventory.cargo(), rows), + displayName(def))); + return true; + } + + /** The vehicle's name as its item shows it. */ + private static Component displayName(AircraftDefinition def) { + return Component.translatableWithFallback("item.mcheli." + def.name(), def.displayName); + } + + /** + * {@code supplyAmmoToOtherAircraft}: an ammunition truck ({@code AmmoSupplyRange}) tops up the + * weapons of every vehicle within range that stands on something, a tenth of each weapon's + * maximum every two seconds. + */ + private void supplyAmmo(AircraftDefinition def) { + float range = def.ammoSupplyRange; + if (range <= 0.0F || ticksAlive % SupplyRules.AMMO_TRUCK_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + VehicleWeapons theirs = other.weapons(); + if (other == this || theirs == null || other.isDestroyed() || !other.canSupply()) { + continue; + } + for (WeaponSet set : theirs.sets()) { + int rounds = SupplyRules.roundsFromTruck(set.ammo() + set.restAmmo(), set.maxAmmo()); + if (rounds > 0) { + set.setRestAmmo(set.restAmmo() + rounds); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + } + } + } + } + + /** {@code repairOtherAircraft}: a repair vehicle ({@code RepairOtherVehicles}) mends those in range. */ + private void repairOthers(AircraftDefinition def) { + float range = def.repairOtherVehiclesRange; + if (range <= 0.0F || ticksAlive % SupplyRules.REPAIR_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + AircraftDefinition theirs = other.definition(); + if (other != this && theirs != null) { + other.repair(SupplyRules.repair(other.hp(), theirs.maxHp, def.repairOtherVehiclesValue)); + } + } + } + /** Whether the flight model holds a hover: in hover mode, or while the pilot mans the guns. */ public boolean isHovering() { return isHoveringMode() || isGunnerMode(); @@ -410,8 +719,25 @@ public float reverse() { // Tick + /** + * Makes this a workshop test flight: it takes no damage, is not saved with the world, and goes + * away once its pilot gets out, putting them back where they started. + */ + public void startTestFlight(Vec3 start) { + testFlightStart = start; + } + + @Override + public boolean shouldBeSaved() { + return testFlightStart == null && super.shouldBeSaved(); + } + @Override public void tick() { + if (testFlightOver) { + discard(); + return; + } rollO = roll(); if (getHurtTime() > 0) { setHurtTime(getHurtTime() - 1); @@ -422,6 +748,7 @@ public void tick() { } ticksAlive++; lastSeats.clear(); + remoteReturns.clear(); if (modeSwitchCooldown > 0) { modeSwitchCooldown--; } @@ -442,10 +769,34 @@ public void tick() { double fallBefore = getDeltaMovement().y; tickDestroyed(); + if (!level().isClientSide()) { + supplyFuel(def); + supplyAmmo(def); + repairOthers(def); + burnFuel(def); + if (def.maxFuel > 0 && fuel() < def.maxFuel && !isDestroyed()) { + inventory.pourFuelCans(ticksAlive); + } + tickNightVision(); + tickStation(); + } + tickRopes(def); + if (isSimulatingHere()) { if (!(getControllingPassenger() instanceof Player) || isDestroyed()) { input.clear(); } + if (isRepelling()) { + // {@code MCH_AircraftClientTickHandler}: the pilot holds a hover while the ropes are out. + input.throttleUp = false; + input.throttleDown = false; + input.left = false; + input.right = false; + } + if (!hasFuel()) { + // An empty tank: the throttle runs down by itself, as with the pilot's hand off it. + input.throttleUp = false; + } if (entityData.get(DATA_SINKING)) { sink(); } else { @@ -486,6 +837,7 @@ public void tick() { } if (level().isClientSide()) { damageSmoke(def); + VehicleParticles.tick(this, def); } else { tickImpacts(def, groundedBefore, fallBefore); } @@ -542,8 +894,14 @@ private void tickImpacts(AircraftDefinition def, boolean groundedBefore, double hurt(damageSources().flyIntoWall(), crashDamage(previous.horizontalDistance())); } if (speed > VEHICLE_CRASH_SPEED && collisionCooldown == 0) { - for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(0.5), - o -> o != this && !o.isRemoved() && o.getVehicle() != this && getVehicle() != o)) { + // A blocked move can leave the hulls up to a tick of travel apart; when the vehicle + // stopped short this tick, look that far for what stopped it. + boolean stopped = velocity.length() < previous.length() * 0.5; + double reach = stopped ? 0.5 + previous.length() : 0.5; + for (AircraftEntity other : solidNeighbours(hullBounds().inflate(reach))) { + if (!hullsTouch(other, reach)) { + continue; + } double closing = velocity.subtract(other.lastVelocity).length(); closing = Math.max(closing, previous.subtract(other.lastVelocity).length()); if (closing > VEHICLE_CRASH_SPEED && other.collisionCooldown == 0) { @@ -866,7 +1224,8 @@ private void decodeSeats(String encoded) { public void switchSeat(Entity entity) { AircraftDefinition def = definition(); int current = seatOf(entity); - if (def == null || current < 0 || current >= def.seatCount()) { + if (def == null || current < 0 || current >= def.seatCount() || current == 0 && station != null) { + // A remote pilot stays at the controls; they are not really aboard. return; } int count = def.seatCount(); @@ -887,6 +1246,120 @@ public void switchSeat(Entity entity) { assignSeat(entity, current); } + // UAV stations + + /** Whether a player at a station could take this drone's controls now. */ + public boolean canTakeRemotePilot() { + AircraftDefinition def = definition(); + return def != null && def.uav && !def.targetDrone && !isDestroyed() && isSeatFree(0); + } + + /** + * Seats {@code player}, who sits at {@code from}, at this drone's controls. The pilot is hidden + * and takes no harm while flying, since in the original they never left the station. Server only. + */ + public boolean takeRemotePilot(ServerPlayer player, UavStationEntity from) { + if (!canTakeRemotePilot()) { + return false; + } + pendingSeat = 0; + boolean seated = player.startRiding(this, true); + pendingSeat = EMPTY; + if (!seated) { + return false; + } + station = from.getUUID(); + stationReturn = from.returnPosition(); + entityData.set(DATA_STATION, from.getId()); + player.addTag(REMOTE_PILOT_TAG); + player.setInvisible(true); + player.setInvulnerable(true); + return true; + } + + /** Sends the remote pilot back to the station, if one is flying. Server only. */ + public void releaseRemotePilot() { + Entity pilot = occupant(0); + if (station != null && pilot != null) { + pilot.stopRiding(); + } + } + + /** The station this drone is flown from, as the client sees it. */ + public @Nullable UavStationEntity stationEntity() { + return level().getEntity(entityData.get(DATA_STATION)) instanceof UavStationEntity s ? s : null; + } + + /** Flat distance to the station this drone is flown from, or -1 without one. */ + public double stationDistance() { + UavStationEntity from = stationEntity(); + if (from == null) { + return -1.0; + } + double dx = getX() - from.getX(); + double dz = getZ() - from.getZ(); + return Math.sqrt(dx * dx + dz * dz); + } + + /** Whether {@code entity} flies this drone from a station rather than sitting in it. */ + public boolean isRemotePilot(Entity entity) { + return (station != null || entityData.get(DATA_STATION) != 0) && occupant(0) == entity; + } + + private void endRemoteControl(Entity pilot) { + UavStationEntity from = level() instanceof ServerLevel server && station != null + && server.getEntity(station) instanceof UavStationEntity s ? s : null; + Vec3 home = from != null ? from.returnPosition() : stationReturn; + if (home != null) { + remoteReturns.put(pilot.getId(), home); + } + clearRemotePilot(pilot); + if (from != null) { + from.setLinked(false); + } + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + } + + /** Undoes what {@link #takeRemotePilot} did to the player. */ + public static void clearRemotePilot(Entity pilot) { + if (pilot.removeTag(REMOTE_PILOT_TAG)) { + pilot.setInvisible(false); + pilot.setInvulnerable(false); + } + } + + /** + * {@code updateUAV}: the link breaks when the station is gone, and flying out of its range + * loses the drone, as in the original. + */ + private void tickStation() { + if (station == null || !(level() instanceof ServerLevel server)) { + return; + } + Entity pilot = occupant(0); + if (pilot == null) { + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + return; + } + UavStationEntity from = server.getEntity(station) instanceof UavStationEntity s && s.isAlive() ? s : null; + if (from == null) { + pilot.stopRiding(); + return; + } + entityData.set(DATA_STATION, from.getId()); + if (!from.inRange(this) && !isDestroyed()) { + if (pilot instanceof Player player) { + player.displayClientMessage(Component.translatable("message.mcheli.uav.lost"), true); + } + pilot.stopRiding(); + hurt(damageSources().fellOutOfWorld(), hp() + 10.0F); + } + } + @Override public @Nullable LivingEntity getControllingPassenger() { // A wreck is nobody's to fly; the server lets it fall, so its crash is seen the same way by everyone. @@ -902,8 +1375,13 @@ protected boolean canAddPassenger(Entity passenger) { if (pendingSeat >= 0) { return isSeatFree(pendingSeat); } + // Coming back aboard after a reload, a remote pilot included. + Integer saved = savedSeats.get(passenger.getUUID()); + if (saved != null && isSeatFree(saved)) { + return true; + } // Cargo only boards through a rack, never into a crew seat. - return !(passenger instanceof AircraftEntity) && firstFreeCrewSeat() >= 0; + return !(passenger instanceof AircraftEntity || passenger instanceof ContainerEntity) && firstFreeCrewSeat() >= 0; } @Override @@ -929,6 +1407,10 @@ protected void removePassenger(Entity passenger) { } int seat = seatOf(passenger); if (seat >= 0) { + if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(passenger); + } lastSeats.put(passenger.getId(), seat); seatIds[seat] = EMPTY; entityData.set(DATA_SEATS, encodeSeats()); @@ -938,6 +1420,13 @@ protected void removePassenger(Entity passenger) { } if (seat == 0) { input.clear(); + if (station != null) { + endRemoteControl(passenger); + } + if (testFlightStart != null) { + remoteReturns.put(passenger.getId(), testFlightStart); + testFlightOver = true; + } } } @@ -998,11 +1487,19 @@ protected void positionRider(Entity passenger, MoveFunction moveFunction) { cargo.setXRot(getXRot() + pitch); cargo.roll = roll(); cargo.setDeltaMovement(Vec3.ZERO); + } else if (def != null && passenger instanceof ContainerEntity container) { + RackDefinition rack = def.rack(seatOf(passenger)); + container.setYRot(getYRot() + (rack != null ? rack.yaw() : 0.0F)); + container.setDeltaMovement(Vec3.ZERO); } } @Override public Vec3 getDismountLocationForPassenger(LivingEntity passenger) { + Vec3 home = remoteReturns.remove(passenger.getId()); + if (home != null) { + return home; + } AircraftDefinition def = definition(); Integer seat = lastSeats.get(passenger.getId()); if (def != null) { @@ -1029,11 +1526,29 @@ public Vec3 getDismountLocationForPassenger(LivingEntity passenger) { */ @Override public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + ItemStack held = player.getItemInHand(hand); + if (held.getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (definition() != null && (held.getItem() instanceof FuelItem || held.getItem() instanceof WrenchItem)) { + if (level().isClientSide()) { + return InteractionResult.SUCCESS; + } + return held.getItem() instanceof FuelItem ? FuelItem.pour(held, this, player) : WrenchItem.useOn(held, this, player, hand); + } InteractionResult result = super.interact(player, hand); + AircraftDefinition def = definition(); + if (result == InteractionResult.PASS && player.isSecondaryUseActive() && def != null && def.cargoRows() > 0 + && !isDestroyed() && player.getVehicle() != this) { + // Sneaking opens the cargo chest from outside, as in the original. + if (player instanceof ServerPlayer server) { + openCargo(server); + } + return InteractionResult.SUCCESS; + } if (result != InteractionResult.PASS || player.isSecondaryUseActive()) { return result; } - AircraftDefinition def = definition(); if (def == null || !def.canRide || player.getVehicle() == this) { return InteractionResult.PASS; } @@ -1054,6 +1569,7 @@ public InteractionResult interactAt(Player player, Vec3 location, InteractionHan } if (seatOf(player) == 0) { closeCanopy(); + boardNearbyMobs(); } } return InteractionResult.SUCCESS; @@ -1062,7 +1578,7 @@ public InteractionResult interactAt(Player player, Vec3 location, InteractionHan // Racks /** Loads {@code cargo} into the given rack seat. Server only. */ - private boolean loadCargo(AircraftEntity cargo, int seat) { + private boolean loadCargo(Entity cargo, int seat) { if (!isSeatFree(seat) || cargo.isPassenger()) { return false; } @@ -1100,13 +1616,21 @@ public boolean acceptsCargo(AircraftEntity cargo, int seat) { return false; } + /** Whether rack {@code seat} carries containers, {@code AddRack} naming {@code container}. */ + public boolean acceptsContainer(ContainerEntity container, int seat) { + AircraftDefinition def = definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + return rack != null && !isPassenger() && !container.isPassenger() + && rack.accepts(ContainerEntity.CARGO_NAME, ContainerEntity.CARGO_NAME); + } + private boolean isNearRackEntry(AircraftEntity cargo, RackDefinition rack) { Vec3 entry = toWorld(rack.entry()); float r = rack.range(); return Math.abs(cargo.getX() - entry.x) <= r && Math.abs(cargo.getY() - entry.y) <= r && Math.abs(cargo.getZ() - entry.z) <= r; } - /** Carrier pilot loads every nearby vehicle that fits a free rack. Server only. */ + /** Carrier pilot loads every nearby vehicle or container that fits a free rack. Server only. */ public void loadNearbyCargo() { AircraftDefinition def = definition(); if (def == null || def.racks.isEmpty() || throttle > 0.3 || !hasBlockBelow(1, 3, true)) { @@ -1119,11 +1643,20 @@ public void loadNearbyCargo() { RackDefinition rack = def.rack(seat); Vec3 entry = toWorld(rack.entry()); AABB area = new AABB(entry, entry).inflate(rack.range()); + boolean loaded = false; for (AircraftEntity cargo : level().getEntitiesOfClass(AircraftEntity.class, area)) { if (acceptsCargo(cargo, seat) && loadCargo(cargo, seat)) { + loaded = true; break; } } + if (!loaded) { + for (ContainerEntity container : level().getEntitiesOfClass(ContainerEntity.class, area)) { + if (acceptsContainer(container, seat) && loadCargo(container, seat)) { + break; + } + } + } } } @@ -1147,6 +1680,10 @@ public void unloadCargo() { Vec3 at = toWorld(drop); cargo.stopRiding(); cargo.moveTo(at.x, at.y, at.z, getYRot() + rack.yaw(), 0.0F); + // As in the original, with no ground in the 20 blocks below it goes down under a canopy. + if (!(cargo instanceof AircraftEntity) && !hasBlockBelow(3, 20, false)) { + ParachuteEntity.open(level(), cargo, ParachuteEntity.Kind.DROP); + } return; } } @@ -1210,8 +1747,10 @@ public boolean canEject(Entity entity) { } /** - * Throws the occupant clear of the aircraft. Without the parachute item (a later phase) the - * crew member gets a few seconds of slow falling instead. Server only. + * {@code ejectSeat}: throws the occupant clear of the aircraft in an ejection seat, whose + * parachute opens once it starts to fall. Each ejection takes a parachute from the parachute + * slots of the vehicle screen; without one the seat stays put. Creative players need none. + * Server only. */ public void eject(Entity entity) { AircraftDefinition def = definition(); @@ -1219,6 +1758,14 @@ public void eject(Entity entity) { if (def == null || !canEject(entity)) { return; } + boolean creative = entity instanceof Player player && player.getAbilities().instabuild; + if (!creative && !inventory.takeParachute()) { + if (entity instanceof ServerPlayer player) { + ModSounds.playTo(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.no_parachute"), true); + } + return; + } Vector3 seatPos = def.seats.get(seat).position(); Vec3 out = toWorld(seatPos.add(0.0, 2.0, 0.0)); Vector3f push = toWorldOffset(new Vector3(0.0, 2.0, 0.0)); @@ -1227,9 +1774,7 @@ public void eject(Entity entity) { entity.setDeltaMovement(getDeltaMovement().add(push.x + (random.nextFloat() - 0.5) * 0.1, push.y, push.z + (random.nextFloat() - 0.5) * 0.1)); entity.hurtMarked = true; - if (entity instanceof LivingEntity living) { - living.addEffect(new MobEffectInstance(MobEffects.SLOW_FALLING, 400)); - } + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.EJECTION_SEAT); if (canopy != null && !canopy.status()) { canopy.set(true, false); modeSwitchCooldown = 40; @@ -1237,6 +1782,257 @@ public void eject(Entity entity) { ModSounds.playAt(entity, "eject_seat", 5.0F, 1.0F); } + // Search lights + + public boolean isSearchLightOn() { + AircraftDefinition def = definition(); + return def != null && !def.searchLights.isEmpty() && entityData.get(DATA_SEARCH_LIGHT); + } + + /** {@code canSwitchSearchLight}: the pilot or the first crew seat, in vehicles that have one. */ + public boolean canSwitchSearchLight(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && !def.searchLights.isEmpty() && seat >= 0 && seat <= 1; + } + + /** Server only. */ + public void toggleSearchLight(Entity entity) { + if (canSwitchSearchLight(entity)) { + entityData.set(DATA_SEARCH_LIGHT, !entityData.get(DATA_SEARCH_LIGHT)); + } + } + + /** + * Where a turning search light points: the first crew seat's view, else the pilot's, as + * {@code renderLight} took them. Null with nobody aboard to aim it. + */ + public @Nullable Entity searchLightAimer() { + Entity crew = occupant(1); + return crew != null ? crew : occupant(0); + } + + // Troops: parachute drops and ropes + + public boolean isRepelling() { + return entityData.get(DATA_REPELLING); + } + + /** How far the ropes hang below their hooks, as a negative number. */ + public float ropeLength() { + return ropeLength; + } + + /** Where rope {@code index} hangs from, in the world. */ + public Vec3 ropePosition(int index) { + AircraftDefinition def = definition(); + return def != null && index >= 0 && index < def.repellingHooks.size() ? toWorld(def.repellingHooks.get(index).position()) : position(); + } + + /** + * {@code onUpdate_Repelling} and {@code onUpdate_UnmountCrew}: the ropes run out towards the + * ground, then the mobs aboard slide down them one per hook interval; a parachute drop lets one + * passenger out per {@code MobDropOption} interval while the hatch is open. + */ + private void tickRopes(AircraftDefinition def) { + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + int clear = airBelow(50); + if (ropeLength > -50.0F && ropeLength > -clear) { + ropeLength -= level().isClientSide() ? 0.3F : 0.25F; + } + } else { + ropeLength = 0.0F; + } + } + if (level().isClientSide()) { + return; + } + if (isRepelling() && isDestroyed()) { + entityData.set(DATA_REPELLING, false); + } + if (!isRepelling()) { + repellingTicks = 0; + } else if (repellingTicks < 60) { + repellingTicks++; + } else { + for (int rope = 0; rope < def.repellingHooks.size(); rope++) { + if (ticksAlive % def.repellingHooks.get(rope).interval() != 0) { + continue; + } + for (int seat = 1; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + rappel(mob, rope); + break; + } + } + } + } + if (droppingTroops) { + if (hasBlockBelow(3, 10, false) || isDestroyed()) { + droppingTroops = false; + } else if ((hatch == null || hatch.isOn()) && ticksAlive % def.dropInterval == 0 && !dropNextTroop(def)) { + droppingTroops = false; + } + } + } + + /** Air blocks straight below, up to {@code max}. */ + private int airBelow(int max) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + int i = 0; + while (i < max && pos.getY() - i > level().getMinBuildHeight() && level().getBlockState(pos.setY(blockPosition().getY() - i - 1)).isAir()) { + i++; + } + return i; + } + + /** + * {@code canStartRepelling}: a helicopter in hover mode, level and holding still. The original + * compared against where it was a few ticks before. + */ + private boolean canStartRepelling() { + return this instanceof HelicopterEntity && isHoveringMode() && Math.abs(getXRot()) < 3.0F && Math.abs(roll()) < 3.0F + && lastTickPos != null && position().distanceTo(lastTickPos) < 0.05; + } + + /** + * The pilot's drop key, {@code unmountCrew}: with ropes, lets them down from a steady hover + * or pulls them up again; with {@code EnableParachuting}, starts or stops dropping the mobs + * aboard under parachutes; otherwise, or close to the ground, simply lets the mobs out. + * Server only. + * + * @return false if nothing could be done, so the pilot hears it + */ + public boolean dropCrew() { + AircraftDefinition def = definition(); + if (def == null || isDestroyed()) { + return false; + } + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + entityData.set(DATA_REPELLING, false); + return true; + } + if (hasBlockBelow(3, 4, false)) { + return unloadMobs(); + } + if (!canStartRepelling()) { + return false; + } + entityData.set(DATA_REPELLING, true); + repellingTicks = 0; + return true; + } + if (droppingTroops) { + droppingTroops = false; + return true; + } + if (def.parachuting && !hasBlockBelow(3, 10, false)) { + droppingTroops = true; + if (hatch != null && hatch.isOff()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } + return true; + } + return unloadMobs(); + } + + /** Lets every mob in a crew seat out, as in {@code unmountCrew(false)}. */ + private boolean unloadMobs() { + AircraftDefinition def = definition(); + boolean any = false; + for (int seat = 1; def != null && seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + mob.stopRiding(); + any = true; + } + } + return any; + } + + /** The next mob behind the second seat leaves at the drop point under a parachute. */ + private boolean dropNextTroop(AircraftDefinition def) { + for (int seat = 2; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + parachuteOut(mob, def); + return true; + } + } + return false; + } + + private void parachuteOut(Entity entity, AircraftDefinition def) { + Vec3 out = toWorld(def.dropPosition); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement()); + entity.hurtMarked = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.DROP); + } + + /** + * A passenger's own drop key, {@code MCH_ClientSeatTickHandler}: slide down a rope once the + * ropes are out, or jump with a parachute from behind the second seat of a vehicle with + * {@code EnableParachuting} and room below. Server only. + * + * @return false if neither is possible right now + */ + public boolean jumpOut(Player player) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (def == null || seat <= 0 || seat >= def.seatCount() || isDestroyed()) { + return false; + } + if (isRepelling() && repellingTicks > 50 && !def.repellingHooks.isEmpty()) { + lastRope = (lastRope + 1) % def.repellingHooks.size(); + rappel(player, lastRope); + return true; + } + if (def.parachuting && seat >= 2 && !hasBlockBelow(3, 13, false)) { + parachuteOut(player, def); + return true; + } + return false; + } + + /** {@code unmountEntityRepelling}: two blocks under the hook, onto a rope rider that lets them down. */ + private void rappel(Entity entity, int rope) { + Vec3 at = ropePosition(rope).add(0.0, -2.0, 0.0); + entity.stopRiding(); + entity.teleportTo(at.x, at.y, at.z); + entity.setDeltaMovement(Vec3.ZERO); + entity.resetFallDistance(); + RappelEntity rider = new RappelEntity(ModEntities.RAPPEL.get(), level()); + rider.attach(this, rope); + rider.setPos(at); + level().addFreshEntity(rider); + entity.startRiding(rider, true); + lastRope = rope; + } + + /** + * {@code mountMobToSeats}: when the pilot climbs in, mobs standing close by take the free crew + * seats, so villagers and the like can be flown somewhere. Server only. + */ + private void boardNearbyMobs() { + AircraftDefinition def = definition(); + if (def == null || def.uav) { + return; + } + for (Mob mob : level().getEntitiesOfClass(Mob.class, getBoundingBox().inflate(3.0, 2.0, 3.0), m -> !m.isPassenger() && !m.isVehicle())) { + for (int seat = 1; seat < def.seatCount(); seat++) { + if (isSeatFree(seat)) { + pendingSeat = seat; + mob.startRiding(this, true); + pendingSeat = EMPTY; + break; + } + } + } + } + // Terrain helpers /** True if any solid block is within {@code depth} blocks below the vehicle. */ @@ -1459,11 +2255,8 @@ public Optional clipHitBoxes(Vec3 from, Vec3 to, double inflate) { double bestDistance = best.map(from::distanceToSqr).orElse(Double.MAX_VALUE); AircraftDefinition def = definition(); if (def != null) { - for (BoundingBoxDefinition box : def.boundingBoxes) { - Vec3 c = toWorld(box.offset()); - double hw = box.width() / 2.0 + inflate; - double hh = box.height() / 2.0 + inflate; - Optional hit = clip(new AABB(c.x - hw, c.y - hh, c.z - hw, c.x + hw, c.y + hh, c.z + hw), from, to); + for (BoundingBoxDefinition box : def.hull()) { + Optional hit = clip(hullBox(box).inflate(inflate), from, to); if (hit.isPresent() && from.distanceToSqr(hit.get()) < bestDistance) { best = hit; bestDistance = from.distanceToSqr(hit.get()); @@ -1474,6 +2267,136 @@ public Optional clipHitBoxes(Vec3 from, Vec3 to, double inflate) { return best; } + /** + * One {@code BoundingBox} in world space. Like the original's, it stays axis aligned: the + * centre turns with the hull, the box itself does not. + */ + private AABB hullBox(BoundingBoxDefinition box) { + Vec3 c = toWorld(box.offset()); + double hw = box.width() / 2.0; + double hh = box.height() / 2.0; + return new AABB(c.x - hw, c.y - hh, c.z - hw, c.x + hw, c.y + hh, c.z + hw); + } + + /** The whole hull: the main hit box, every {@code BoundingBox} and the generated boxes, in world space. */ + public List hullBoxes() { + AircraftDefinition def = definition(); + if (def == null || def.hull().isEmpty()) { + return List.of(getBoundingBox()); + } + List boxes = new ArrayList<>(def.hull().size() + 1); + boxes.add(getBoundingBox()); + for (BoundingBoxDefinition box : def.hull()) { + boxes.add(hullBox(box)); + } + return boxes; + } + + /** The smallest box around the whole hull. */ + private AABB hullBounds() { + AABB bounds = getBoundingBox(); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + bounds = bounds.minmax(hullBox(box)); + } + } + return bounds; + } + + /** Whether any part of this hull touches any part of {@code other}'s, grown by {@code inflate}. */ + private boolean hullsTouch(AircraftEntity other, double inflate) { + // Hulls generated from big models run to a thousand boxes, so skip ours that are nowhere near. + AABB around = other.hullBounds().inflate(inflate); + List theirs = null; + for (AABB mine : hullBoxes()) { + AABB grown = mine.inflate(inflate); + if (!grown.intersects(around)) { + continue; + } + if (theirs == null) { + theirs = other.hullBoxes(); + } + for (AABB box : theirs) { + if (grown.intersects(box)) { + return true; + } + } + } + return false; + } + + /** + * Vehicles whose hulls block this one: everything nearby except its own carrier or cargo, and + * vehicles either of the two could load onto a rack, which have to drive into each other. + */ + private List solidNeighbours(AABB area) { + return level().getEntitiesOfClass(AircraftEntity.class, area.inflate(HULL_REACH), other -> other != this && !other.isRemoved() + && other.definition() != null && !other.isPassenger() && !isPassengerOfSameVehicle(other) + && other.getVehicle() != this && getVehicle() != other && !canCarry(other) && !other.canCarry(this)); + } + + /** Whether a free rack of this vehicle would take {@code cargo}. */ + private boolean canCarry(AircraftEntity cargo) { + AircraftDefinition def = definition(); + if (def == null) { + return false; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (acceptsCargo(cargo, seat)) { + return true; + } + } + return false; + } + + /** + * Not in the original, where only the main box at a vehicle's feet collided: the whole hull + * blocks other vehicles. A move that would push this hull into another vehicle's is undone, + * sideways first and then completely, so a helicopter can also set down on a parked truck. + * Hulls that already overlapped, say after two vehicles were placed into each other, may move + * apart. + */ + @Override + public void move(MoverType type, Vec3 delta) { + if (type != MoverType.SELF || isPassenger() || definition() == null) { + super.move(type, delta); + return; + } + List near = solidNeighbours(hullBounds().expandTowards(delta)); + if (near.isEmpty()) { + super.move(type, delta); + return; + } + List overlapping = new ArrayList<>(); + for (AircraftEntity other : near) { + if (hullsTouch(other, 0.0)) { + overlapping.add(other); + } + } + Vec3 before = position(); + super.move(type, delta); + if (!entersHull(near, overlapping)) { + return; + } + Vec3 m = getDeltaMovement(); + setPos(before.x, getY(), before.z); + setDeltaMovement(0.0, m.y, 0.0); + if (entersHull(near, overlapping)) { + setPos(before); + setDeltaMovement(Vec3.ZERO); + } + } + + private boolean entersHull(List near, List overlapping) { + for (AircraftEntity other : near) { + if (!overlapping.contains(other) && hullsTouch(other, 0.0)) { + return true; + } + } + return false; + } + /** A ray that starts inside a box hits it where it starts, as it does for vanilla entity picking. */ private static Optional clip(AABB box, Vec3 from, Vec3 to) { return box.contains(from) ? Optional.of(from) : box.clip(from, to); @@ -1503,7 +2426,7 @@ public boolean hurt(DamageSource source, float damage) { return false; } float boxFactor = consumeHitBoxFactor(); - if (isRemoved() || isInvulnerableTo(source) || timeSinceHit > 0) { + if (testFlightStart != null || isRemoved() || isInvulnerableTo(source) || timeSinceHit > 0) { return false; } if (source.is(DamageTypes.IN_FIRE) || source.is(DamageTypes.CACTUS)) { @@ -1567,7 +2490,7 @@ public boolean hurt(DamageSource source, float damage) { entityData.set(DATA_HP, 0); destroyVehicle(); timeSinceHit = 20; - if (isAfloat()) { + if (isAfloat() || isHeavy() && isNearGround(3)) { startCookOff(source.getEntity()); } else if (source.is(DamageTypes.IN_WALL) || source.is(DamageTypes.FLY_INTO_WALL) || source.is(DamageTypes.FALL)) { crashExplosion(); @@ -1602,9 +2525,6 @@ private boolean hasPlayerPassenger() { private void destroyVehicle() { AircraftDefinition def = definition(); despawnCount = def != null && def.targetDrone ? 50 : WRECK_TICKS; - destroyedPitch = random.nextFloat() - 0.5F; - destroyedRoll = (random.nextFloat() - 0.5F) * 0.5F; - destroyedYaw = 0.0F; setHovering(false); if (def != null && def.uav) { Entity remote = occupant(0); @@ -1625,16 +2545,30 @@ private void crashExplosion() { if (!(level() instanceof ServerLevel level)) { return; } + if (isHeavy()) { + startCookOff(null); + return; + } exploded = true; AircraftDefinition def = definition(); float size = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); - Explosions.explode(level, getX(), getY(), getZ(), + Vec3 at = def != null && def.fuelTankPosition != null ? toWorld(def.fuelTankPosition) : position(); + Explosions.explode(level, at.x, at.y, at.z, new Explosions.Params(null, null, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); if (!isInWater()) { - Explosions.crashFireball(level, getX(), getY(), getZ(), size); + Explosions.crashFireball(level, at.x, at.y, at.z, size); } } + /** + * Airliners, bombers and transports ({@code MaxFuel} of {@value HEAVY_FUEL} or more): like a + * boat, they go up in three blasts rather than one fireball. + */ + private boolean isHeavy() { + AircraftDefinition def = definition(); + return def != null && def.maxFuel >= HEAVY_FUEL; + } + /** A floating vehicle sitting on the water, such as a patrol boat or a float plane at rest. */ private boolean isAfloat() { AircraftDefinition def = definition(); @@ -1642,9 +2576,9 @@ private boolean isAfloat() { } /** - * A boat destroyed on the water does not go up in one crash fireball; its fuel and magazine - * cook off in three blasts over about five seconds, then the hull sinks. Not in the original, - * which only had the crash explosion. + * A boat destroyed on the water, or a heavy aircraft wrecked on the ground, does not go up in + * one crash fireball; its fuel and magazine cook off in three blasts over about five seconds, + * then a hull in the water sinks. Not in the original, which only had the crash explosion. */ private void startCookOff(@Nullable Entity cause) { exploded = true; @@ -1676,7 +2610,10 @@ private void tickCookOff(ServerLevel level) { // Three blasts inside five seconds: about 1.5 to 2.5 seconds apart. cookOffDelay = 30 + random.nextInt(20); } else { - entityData.set(DATA_SINKING, true); + // A boat or a flying boat goes under; a heavy aircraft on land stays where it burns. + if (isInWater() || waterDepth() > 0.0) { + entityData.set(DATA_SINKING, true); + } cookOffCause = null; } } @@ -1733,16 +2670,7 @@ private void tickDestroyed() { } input.clear(); throttle *= 0.98; - if (this instanceof HelicopterEntity) { - destroyedYaw = Math.min(destroyedYaw + 0.3F, 15.0F); - setYRot(getYRot() + destroyedYaw * (float) throttle); - if (!isNearGround(3)) { - if (Math.abs(getXRot()) < 10.0F) { - setXRot(getXRot() + destroyedPitch); - } - roll += destroyedRoll; - } - } + crashSpin(); if (!(level() instanceof ServerLevel level)) { return; } @@ -1762,6 +2690,27 @@ private void tickDestroyed() { } } + /** + * Not in the original: a shot down plane rolls into a nose-down spiral and a helicopter spins + * with some wobble, until it is close to the ground. Tanks and cars just stop. + */ + private void crashSpin() { + if (!(this instanceof HelicopterEntity) && !(this instanceof PlaneEntity) || isNearGround(3) || isInWater()) { + return; + } + int direction = CrashSpin.direction(getId()); + int ticks = spinTicks++; + if (this instanceof HelicopterEntity) { + setYRot(getYRot() + CrashSpin.helicopterYawRate(ticks, direction)); + setXRot(Mth.approach(getXRot(), CrashSpin.helicopterWobblePitch(ticks, getId()), 2.0F)); + roll = Mth.approach(roll, CrashSpin.helicopterWobbleRoll(ticks, getId()), 3.0F); + } else { + setYRot(getYRot() + CrashSpin.planeYawRate(ticks, direction)); + setXRot(CrashSpin.planePitch(getXRot())); + roll = AircraftRotation.wrap(roll + CrashSpin.planeRollRate(ticks, direction)); + } + } + /** {@code onUpdate_Particle2}: smoke from a damaged hull, thicker the lower its hit points. */ private void damageSmoke(AircraftDefinition def) { float ratio = (float) hp() / def.maxHp; @@ -1795,7 +2744,7 @@ private void wreckFire(AircraftDefinition def) { return; } float left = Math.clamp((float) (WRECK_TICKS - wreckTicks) / WRECK_FADE_TICKS, 0.15F, 1.0F); - Vec3 centre = position().add(0.0, def.bodyHeight / 2.0, 0.0); + Vec3 centre = def.enginePosition != null ? toWorld(def.enginePosition) : position().add(0.0, def.bodyHeight / 2.0, 0.0); if (wreckTicks % 8 == 0) { // Each glow outlives the gap to the next, so the fire never goes dark in between. float size = Math.clamp(def.bodyWidth * 0.8F, 2.0F, 8.0F) * left; @@ -1891,11 +2840,21 @@ private void tickLerp() { } } + /** A vehicle that is destroyed, packed up or killed spills what it carried; one that unloads keeps it. */ + @Override + public void remove(RemovalReason reason) { + if (reason.shouldDestroy() && !level().isClientSide() && !isRemoved()) { + inventory.dropAll(); + } + super.remove(reason); + } + @Override protected void addAdditionalSaveData(CompoundTag tag) { tag.putString("Type", typeName()); tag.putString("Texture", texture()); tag.putInt("Hp", hp()); + tag.putInt("Fuel", fuel()); tag.putFloat("Roll", roll); tag.putBoolean("Hovering", isHoveringMode()); tag.putBoolean("GunnerMode", isGunnerMode()); @@ -1903,9 +2862,17 @@ protected void addAdditionalSaveData(CompoundTag tag) { tag.putInt("DespawnCount", despawnCount); tag.putInt("CookOff", cookOffBlasts); tag.putBoolean("Sinking", entityData.get(DATA_SINKING)); + tag.putBoolean("SearchLight", entityData.get(DATA_SEARCH_LIGHT)); if (weapons != null) { tag.putIntArray("WeaponsAmmo", weapons.saveAmmo()); } + inventory.save(tag); + if (station != null && stationReturn != null) { + tag.putUUID("Station", station); + tag.putDouble("StationReturnX", stationReturn.x); + tag.putDouble("StationReturnY", stationReturn.y); + tag.putDouble("StationReturnZ", stationReturn.z); + } ListTag seats = new ListTag(); for (int i = 0; i < seatIds.length; i++) { Entity e = occupant(i); @@ -1921,6 +2888,8 @@ protected void readAdditionalSaveData(CompoundTag tag) { entityData.set(DATA_PARTS, tag.getInt("Parts")); AircraftDefinition def = definition(); entityData.set(DATA_HP, tag.contains("Hp") ? tag.getInt("Hp") : def != null ? def.maxHp : 1); + // 1.0.0 and the earlier betas had no fuel; their vehicles come back with a full tank. + entityData.set(DATA_FUEL, tag.contains("Fuel") ? tag.getInt("Fuel") : def != null ? def.maxFuel : 0); roll = tag.getFloat("Roll"); setHovering(tag.getBoolean("Hovering")); entityData.set(DATA_GUNNER_MODE, tag.getBoolean("GunnerMode")); @@ -1931,6 +2900,7 @@ protected void readAdditionalSaveData(CompoundTag tag) { cookOffBlasts = tag.getInt("CookOff"); cookOffDelay = cookOffBlasts > 0 ? 20 : 0; entityData.set(DATA_SINKING, tag.getBoolean("Sinking")); + entityData.set(DATA_SEARCH_LIGHT, tag.getBoolean("SearchLight")); exploded |= cookOffBlasts > 0 || entityData.get(DATA_SINKING); if (tag.contains("WeaponsAmmo", Tag.TAG_INT_ARRAY)) { int[] ammo = tag.getIntArray("WeaponsAmmo"); @@ -1940,6 +2910,9 @@ protected void readAdditionalSaveData(CompoundTag tag) { savedAmmo = ammo; } } + inventory.load(tag); + station = tag.hasUUID("Station") ? tag.getUUID("Station") : null; + stationReturn = station != null ? new Vec3(tag.getDouble("StationReturnX"), tag.getDouble("StationReturnY"), tag.getDouble("StationReturnZ")) : null; savedSeats.clear(); ListTag seats = tag.getList("Seats", Tag.TAG_STRING); for (int i = 0; i < seats.size(); i++) { From a75d9c986034ed392fae713d33322621d4630d86 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:22 +0800 Subject: [PATCH 0148/1461] Bring ClientAccess.java on 1.21.1 up to phase 12 --- .../java/com/jnhprojects/mcheli/entity/ClientAccess.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java index bf89ea5a..28f14851 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java @@ -2,6 +2,7 @@ import com.jnhprojects.mcheli.network.PackDataPacket; import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; import net.minecraft.world.entity.Entity; import org.jspecify.annotations.Nullable; @@ -22,6 +23,10 @@ public void onPackList(PackListPacket packet) { @Override public void onPackData(PackDataPacket packet) { } + + @Override + public void onPackSound(PackSoundPacket packet) { + } }; private ClientAccess() { @@ -46,5 +51,7 @@ public interface PackReceiver { void onPackList(PackListPacket packet); void onPackData(PackDataPacket packet); + + void onPackSound(PackSoundPacket packet); } } From d6ee26fb8ae306433bb44435266dad6e4059574d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:23 +0800 Subject: [PATCH 0149/1461] Bring HelicopterEntity.java on 1.21.1 up to phase 12 --- .../mcheli/entity/HelicopterEntity.java | 244 +++++++++++++++++- 1 file changed, 239 insertions(+), 5 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java index b140713a..96758a2c 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java @@ -2,11 +2,16 @@ import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.util.Mth; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.MoverType; import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; @@ -21,7 +26,8 @@ *

    *
  • The original ran roll damping once per rendered frame, so it depended on frame rate. It * now runs once per tick with constants tuned to match the feel at 60 FPS.
  • - *
  • Blade folding, weapons, fuel and damage effects are not ported yet.
  • + *
  • Blades fold and unfold one after another at the same pace on both sides, from the + * synced fold state, instead of each client stepping its own copy.
  • *
*/ public class HelicopterEntity extends AircraftEntity implements EngineSoundSource { @@ -37,9 +43,25 @@ public class HelicopterEntity extends AircraftEntity implements EngineSoundSourc private static final float ROLL_RATE = 1.8F; private static final double MIN_SPEED = 0.07; + private static final EntityDataAccessor DATA_FOLD = SynchedEntityData.defineId(HelicopterEntity.class, EntityDataSerializers.BYTE); + // DATA_FOLD values, as the original numbered them. + private static final byte FOLDED = 0; + private static final byte FOLDING = 1; + private static final byte UNFOLDED = 2; + private static final byte UNFOLDING = 3; + /** {@code MCH_Blade}: degrees a blade swings per tick while folding. */ + private static final float FOLD_SPEED = 2.0F; + private static final int FOLD_COOLDOWN = 40; + private double currentSpeed = MIN_SPEED; private float rotorAngle; public float rotorAngleO; + /** Per rotor, per blade: where each blade points while they fold, and whether that rotor is folded. */ + private float[][] blades = new float[0][]; + private float[][] bladesO = new float[0][]; + private boolean[] rotorFolded = new boolean[0]; + private int foldCooldown; + private byte lastFold = -1; public HelicopterEntity(EntityType type, Level level) { super(type, level); @@ -92,11 +114,210 @@ public float rotorAngle(float partialTicks) { return prev + (rotorAngle - prev) * partialTicks; } + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_FOLD, UNFOLDED); + } + + // Blade folding + + private byte foldState() { + return entityData.get(DATA_FOLD); + } + + private boolean hasFoldingRotors() { + HelicopterDefinition def = definition(); + return def != null && def.foldableBlades && !def.rotors.isEmpty(); + } + + /** {@code isFoldBlades}: folded, or on the way. */ + public boolean isFoldedBlades() { + return hasFoldingRotors() && foldState() == FOLDED; + } + + /** {@code canUseBlades}: the blades are out and none is still swinging into place. */ + public boolean canUseBlades() { + if (foldState() != UNFOLDED) { + return false; + } + for (int r = 0; r < rotorFolded.length; r++) { + if (rotorFolded[r]) { + return false; + } + } + return true; + } + + /** {@code canSwitchFoldBlades}: with the engine stopped and the last switch finished. */ + private boolean canSwitchFoldBlades() { + byte state = foldState(); + return hasFoldingRotors() && visibleThrottle() <= 0.01F && foldCooldown == 0 && (state == UNFOLDED || state == FOLDED); + } + + /** The fold key folds the blades of helicopters that can, and works the hatches otherwise. */ + @Override + public void toggleFold() { + if (canSwitchFoldBlades()) { + entityData.set(DATA_FOLD, foldState() == FOLDED ? UNFOLDING : FOLDING); + foldCooldown = FOLD_COOLDOWN; + return; + } + super.toggleFold(); + } + + /** + * {@code onUpdate_Rotor} with {@code MCH_Rotor.update} and {@code MCH_Blade}: spinning blades + * follow the rotor; folding ones swing one by one onto the tail, each a few degrees further + * than the one before, and swing out again until they meet the spinning rotor's angle. + */ + private void tickBlades(HelicopterDefinition def) { + int rotors = def.rotors.size(); + if (blades.length != rotors) { + blades = new float[rotors][]; + bladesO = new float[rotors][]; + rotorFolded = new boolean[rotors]; + for (int r = 0; r < rotors; r++) { + int count = Math.max(def.rotors.get(r).blades(), 1); + blades[r] = new float[count]; + bladesO[r] = new float[count]; + } + if (foldState() == FOLDED || foldState() == FOLDING) { + forceFold(def); + } + } + byte state = foldState(); + if (state != lastFold) { + lastFold = state; + foldCooldown = FOLD_COOLDOWN; + } else if (foldCooldown > 0) { + foldCooldown--; + } + boolean target = state == FOLDING || state == FOLDED; + boolean settled = true; + for (int r = 0; r < rotors; r++) { + RotorDefinition rotor = def.rotors.get(r); + boolean wanted = target && rotor.foldable(); + boolean done = true; + for (int b = 0; b < blades[r].length; b++) { + float base = b * Math.max(rotor.bladeStep(), 1); + float fold = 5.0F + b * 3.0F; + bladesO[r][b] = blades[r][b]; + if (!rotorFolded[r]) { + if (!wanted) { + blades[r][b] = wrap(rotorAngle + base); + done = false; + } else if (!foldBlade(r, b, fold)) { + done = false; + } + } else if (wanted) { + done = false; + } else if (!unfoldBlade(r, b, base)) { + done = false; + } + } + if (done) { + rotorFolded[r] = wanted; + } + if (rotorFolded[r] != wanted) { + settled = false; + } + } + if (settled && !level().isClientSide()) { + if (state == FOLDING) { + entityData.set(DATA_FOLD, FOLDED); + } else if (state == UNFOLDING) { + entityData.set(DATA_FOLD, UNFOLDED); + } + } + } + + /** {@code MCH_Blade.folding}: towards the nearer side of the tail. */ + private boolean foldBlade(int r, int b, float fold) { + float angle = blades[r][b]; + if (angle > fold && angle < 360.0F - fold) { + blades[r][b] = wrap(angle < 180.0F ? angle - FOLD_SPEED : angle + FOLD_SPEED); + return false; + } + return true; + } + + /** {@code MCH_Blade.unfolding}: back round until the blade meets its place on the turning rotor. */ + private boolean unfoldBlade(int r, int b, float base) { + float target = wrap(rotorAngle + base); + float before = blades[r][b]; + if (target <= 180.0F) { + blades[r][b] = wrap(before + FOLD_SPEED); + if (blades[r][b] >= target && before <= target) { + blades[r][b] = target; + return true; + } + } else { + blades[r][b] = wrap(before - FOLD_SPEED); + if (blades[r][b] <= target && before >= target) { + blades[r][b] = target; + return true; + } + } + return false; + } + + /** {@code forceFold}: a helicopter loaded with its blades folded starts with them folded. */ + private void forceFold(HelicopterDefinition def) { + for (int r = 0; r < blades.length; r++) { + if (!def.rotors.get(r).foldable()) { + continue; + } + for (int b = 0; b < blades[r].length; b++) { + float fold = 5.0F + b * 3.0F; + float angle = wrap(rotorAngle + b * Math.max(def.rotors.get(r).bladeStep(), 1)); + if (angle > fold && angle < 360.0F - fold) { + angle = angle < 180.0F ? fold : 360.0F - fold; + } + blades[r][b] = angle; + bladesO[r][b] = angle; + } + rotorFolded[r] = true; + } + } + + private static float wrap(float degrees) { + float d = degrees % 360.0F; + return d < 0.0F ? d + 360.0F : d; + } + + /** Where blade {@code blade} of rotor {@code rotor} points, for drawing. */ + public float bladeAngle(int rotor, int blade, int step, float partialTicks) { + if (rotor >= blades.length || blade >= blades[rotor].length) { + return rotorAngle(partialTicks) + blade * step; + } + float prev = bladesO[rotor][blade]; + float now = blades[rotor][blade]; + if (now - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - now < -180.0F) { + prev += 360.0F; + } + return prev + (now - prev) * partialTicks; + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + super.addAdditionalSaveData(output); + output.putBoolean("FoldBlade", foldState() == FOLDED || foldState() == FOLDING); + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + super.readAdditionalSaveData(input); + entityData.set(DATA_FOLD, (input.contains("FoldBlade") ? input.getBoolean("FoldBlade") : false) ? FOLDED : UNFOLDED); + } + /** Pilot toggles hover mode. Only allowed while fairly level and not in gunner mode, as in the original. */ public void toggleHovering() { if (isHoveringMode()) { setHovering(false); - } else if (!isGunnerMode() && isLevelEnough()) { + } else if (!isGunnerMode() && isLevelEnough() && canUseBlades()) { setHovering(true); } } @@ -105,10 +326,9 @@ private boolean isLevelEnough() { return Math.abs(Mth.wrapDegrees(roll())) < 40.0F && Math.abs(Mth.wrapDegrees(getXRot())) < 40.0F; } - /** The original also needed the blades unfolded; folding is not ported yet. */ @Override public boolean canSwitchGunnerMode() { - return super.canSwitchGunnerMode() && isLevelEnough(); + return super.canSwitchGunnerMode() && isLevelEnough() && canUseBlades(); } @Override @@ -126,7 +346,8 @@ public void applyPilotLook(float dYaw, float dPitch) { if (!isNearGround(2)) { yaw = clamp(x, 40.0F * def.mobilityYaw) * LOOK_SCALE; } - if (!isHovering()) { + // Standing on its skids or wheels a helicopter stays level; the mouse only tilts it in the air. + if (!isHovering() && !onGround()) { pitch = clamp(y, 40.0F * def.mobilityPitch) * LOOK_SCALE; rollDelta = clamp(x, 40.0F * def.mobilityRoll) * LOOK_SCALE; } @@ -165,6 +386,18 @@ protected void simulate() { private void updateThrottle(HelicopterDefinition def, boolean piloted) { if (piloted) { Vec3 motion = getDeltaMovement(); + if (!canUseBlades()) { + // Folded blades give no lift; on the ground A/D taxi the helicopter round slowly. + throttle = Math.max(0.0, throttle - 0.02); + if (onGround() && isFoldedBlades() && !isDestroyed()) { + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + } + return; + } if (!isHovering()) { if (input.throttleUp) { throttle = Math.min(1.0, throttle + 0.02 * def.throttleUpDown); @@ -314,5 +547,6 @@ protected void tickAnimation() { rotorAngleO = rotorAngle; float spool = 1.0F - visibleThrottle(); rotorAngle = (rotorAngle + (1.0F - spool * spool * spool) * def.rotorSpeed) % 360.0F; + tickBlades(def); } } From 659910ce3b20c51627a872eed1a65993399bef9f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:23 +0800 Subject: [PATCH 0150/1461] Bring ProjectileEntity.java on 1.21.1 up to phase 12 --- .../entity/projectile/ProjectileEntity.java | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java index 3ffc508a..a572358b 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java @@ -48,6 +48,9 @@ public abstract class ProjectileEntity extends Entity { private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.STRING); private static final EntityDataAccessor DATA_TARGET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); private static final EntityDataAccessor DATA_BOMBLET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.BOOLEAN); + /** Who fired, for clients: their rounds must not stop on their own vehicle. */ + private static final EntityDataAccessor DATA_SHOOTER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_USER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); /** Beyond 5820 blocks from whoever fired it, a projectile is dropped. */ private static final double MAX_RANGE_SQ = 3.38724E7; @@ -66,6 +69,8 @@ public abstract class ProjectileEntity extends Entity { public double acceleration = 1.0; public double accelerationFactor = 1.0; private int age; + /** Client only: the round hit something here and waits, hidden, for the server to remove it. */ + private boolean spent; public Vec3 prevMotion = Vec3.ZERO; /** Position two ticks ago, used to spread trail particles over the distance covered. */ @@ -83,6 +88,8 @@ protected void defineSynchedData(SynchedEntityData.Builder builder) { builder.define(DATA_WEAPON, ""); builder.define(DATA_TARGET, 0); builder.define(DATA_BOMBLET, false); + builder.define(DATA_SHOOTER, 0); + builder.define(DATA_USER, 0); } /** @@ -153,8 +160,7 @@ public void setParameters(WeaponDefinition def, @Nullable Entity shooter, @Nulla explosionPowerInWater = def.explosionInWater; power = def.power; piercing = def.piercing; - this.shooter = shooter; - this.user = user; + setOwners(shooter, user); } /** The same, inherited from a parent projectile, for bomblets. */ @@ -163,13 +169,14 @@ public void setParameters(ProjectileEntity parent) { explosionPowerInWater = parent.explosionPowerInWater; power = parent.power; piercing = parent.piercing; - shooter = parent.shooter; - user = parent.user; + setOwners(parent.shooter, parent.user); } public void setOwners(@Nullable Entity shooter, @Nullable Entity user) { this.shooter = shooter; this.user = user; + entityData.set(DATA_SHOOTER, shooter != null ? shooter.getId() : 0); + entityData.set(DATA_USER, user != null ? user.getId() : 0); } public @Nullable Entity user() { @@ -222,6 +229,9 @@ public float gravityInWater() { @Override public void tick() { + if (spent) { + return; + } if (level().isClientSide() && age == 0) { int targetId = entityData.get(DATA_TARGET); if (targetId > 0) { @@ -535,8 +545,21 @@ protected void checkCollisions() { return best != null ? new Impact(bestAt, best, null, Direction.UP) : null; } + /** Clients only learn who fired through synched data; look them up once it has arrived. */ + private void resolveOwners() { + if (shooter == null && entityData.get(DATA_SHOOTER) > 0) { + shooter = level().getEntity(entityData.get(DATA_SHOOTER)); + } + if (user == null && entityData.get(DATA_USER) > 0) { + user = level().getEntity(entityData.get(DATA_USER)); + } + } + /** {@code canBeCollidedEntity}: never the shooter, its crew, or rounds fired by the same side. */ public boolean canHit(Entity entity) { + if (level().isClientSide()) { + resolveOwners(); + } if (!entity.isPickable() || entity == user || entity == shooter) { return false; } @@ -603,6 +626,13 @@ protected void impact(Impact hit, float damageFactor) { if (weapon != null && (weapon.explosion == 0 || weapon.modeNum >= 2) && hit.block() != null) { debris(hit); } + if (piercing <= 0) { + // The server decides what the hit does; until its removal arrives the round stays + // where it struck instead of flying on through the hull. + spent = true; + setPos(hit.location()); + setDeltaMovement(Vec3.ZERO); + } return; } if (weapon == null) { @@ -726,6 +756,11 @@ public boolean hurt(DamageSource source, float damage) { return false; } + /** Client only: whether the round already hit something and should no longer be drawn. */ + public boolean isSpent() { + return spent; + } + @Override public boolean isPickable() { return true; From 83c9674985bdd86cb887c6e7b611b9d0548482c9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:24 +0800 Subject: [PATCH 0151/1461] Bring AircraftItem.java on 1.21.1 up to phase 12 --- .../jnhprojects/mcheli/item/AircraftItem.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java index f7258aa3..7eacf467 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java @@ -4,10 +4,12 @@ import com.jnhprojects.mcheli.ServerConfig; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackManifest; import com.jnhprojects.mcheli.entity.AircraftEntity; import com.jnhprojects.mcheli.registry.ModDataComponents; import com.jnhprojects.mcheli.registry.ModEntities; import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.ChatFormatting; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; import net.minecraft.stats.Stats; @@ -16,6 +18,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.item.component.CustomModelData; import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.Level; @@ -25,6 +28,8 @@ import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; +import java.util.List; + /** * Places a vehicle, the same way a boat is placed. * @@ -83,6 +88,21 @@ public Component getName(ItemStack stack) { return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); } + /** Who made a vehicle from a content pack. The import's vehicles have their credits in the docs. */ + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, List out, TooltipFlag flag) { + super.appendHoverText(stack, context, out, flag); + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + PackManifest credit = def != null ? def.credit() : null; + if (credit != null) { + out.add((credit.author().isEmpty() + ? Component.translatable("item.mcheli.vehicle.from_pack", credit.name()) + : Component.translatable("item.mcheli.vehicle.from_pack_by", credit.name(), credit.author())) + .withStyle(ChatFormatting.GRAY)); + } + } + @Override public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { ItemStack stack = player.getItemInHand(hand); From abe251b079674c3b8d02775fa374d0d9e8a9691a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:24 +0800 Subject: [PATCH 0152/1461] Bring CrewActionPacket.java on 1.21.1 up to phase 12 --- .../mcheli/network/CrewActionPacket.java | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java index 8e7136fa..1b565e21 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java @@ -1,8 +1,8 @@ package com.jnhprojects.mcheli.network; +import com.jnhprojects.mcheli.ModSounds; import com.jnhprojects.mcheli.entity.AircraftEntity; import com.jnhprojects.mcheli.entity.PlaneEntity; -import com.jnhprojects.mcheli.weapon.Resupply; import com.jnhprojects.mcheli.weapon.VehicleWeapons; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -44,10 +44,20 @@ public enum Action { WEAPON_MODE, /** Release flares. Pilot and first crew seat. */ FLARE, - /** Pay the selected weapon's {@code Item} cost from the inventory for one resupply. */ - RESUPPLY, + /** Open the vehicle screen: fuel and parachute slots, reloading, the way to the cargo chest. */ + VEHICLE_SCREEN, /** Hand the controls to the autopilot and man the weapons, or take them back. Pilot only. */ - GUNNER_MODE + GUNNER_MODE, + /** Turn night vision on for the sender, in vehicles with {@code EnableNightVision}. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF, + /** Let the troops down: ropes from a hover, a parachute drop, or out on the ground. Pilot only. */ + DROP_CREW, + /** Slide down a rope or jump with a parachute. Crew other than the pilot. */ + JUMP_OUT, + /** Turn the search light on or off. Pilot and first crew seat. */ + SEARCH_LIGHT } public static void encode(CrewActionPacket msg, FriendlyByteBuf buf) { @@ -131,7 +141,19 @@ public static void handle(CrewActionPacket msg, ServerPlayer sender) { weapons.flares.use(); } } - case RESUPPLY -> Resupply.fromInventory(sender, vehicle); + case VEHICLE_SCREEN -> vehicle.openScreen(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> vehicle.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + case DROP_CREW -> { + if (pilot && !vehicle.dropCrew()) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + case SEARCH_LIGHT -> vehicle.toggleSearchLight(sender); + case JUMP_OUT -> { + if (!pilot && !vehicle.jumpOut(sender)) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } } } From f3920df33b66395ab8411a7587d9bb033eb825a7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:25 +0800 Subject: [PATCH 0153/1461] Bring ModNetwork.java on 1.21.1 up to phase 12 --- .../jnhprojects/mcheli/network/ModNetwork.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java index de59563d..74ee6247 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java @@ -21,23 +21,32 @@ */ public final class ModNetwork { /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ - public static final int PROTOCOL_VERSION = 6; + public static final int PROTOCOL_VERSION = 11; /** Client to server. The handler gets the sending player. */ public static final List> SERVERBOUND = List.of( new Serverbound<>(PilotStatePacket.TYPE, PilotStatePacket.STREAM_CODEC, PilotStatePacket::handle), new Serverbound<>(CrewActionPacket.TYPE, CrewActionPacket.STREAM_CODEC, CrewActionPacket::handle), + new Serverbound<>(EquipmentActionPacket.TYPE, EquipmentActionPacket.STREAM_CODEC, EquipmentActionPacket::handle), + new Serverbound<>(SpotPacket.TYPE, SpotPacket.STREAM_CODEC, SpotPacket::handle), + new Serverbound<>(LauncherPacket.TYPE, LauncherPacket.STREAM_CODEC, LauncherPacket::handle), + new Serverbound<>(DraftingTablePacket.TYPE, DraftingTablePacket.STREAM_CODEC, DraftingTablePacket::handle), + new Serverbound<>(TeamPacket.TYPE, TeamPacket.STREAM_CODEC, TeamPacket::handle), + new Serverbound<>(TestFlightPacket.TYPE, TestFlightPacket.STREAM_CODEC, TestFlightPacket::handle), new Serverbound<>(FireWeaponPacket.TYPE, FireWeaponPacket.STREAM_CODEC, FireWeaponPacket::handle), new Serverbound<>(BoardVehiclePacket.TYPE, BoardVehiclePacket.STREAM_CODEC, BoardVehiclePacket::handle), new Serverbound<>(LockWarningPacket.TYPE, LockWarningPacket.STREAM_CODEC, LockWarningPacket::handle), - new Serverbound<>(PackRequestPacket.TYPE, PackRequestPacket.STREAM_CODEC, PackRequestPacket::handle)); + new Serverbound<>(PackRequestPacket.TYPE, PackRequestPacket.STREAM_CODEC, PackRequestPacket::handle), + new Serverbound<>(PackSubmitPacket.TYPE, PackSubmitPacket.STREAM_CODEC, PackSubmitPacket::handle)); /** Server to client. Handlers touch client classes, so they only ever run on a client. */ public static final List> CLIENTBOUND = List.of( new Clientbound<>(WeaponStatePacket.TYPE, WeaponStatePacket.STREAM_CODEC, WeaponStatePacket::handle), new Clientbound<>(ExplosionEffectPacket.TYPE, ExplosionEffectPacket.STREAM_CODEC, ExplosionEffectPacket::handle), new Clientbound<>(PackListPacket.TYPE, PackListPacket.STREAM_CODEC, PackListPacket::handle), - new Clientbound<>(PackDataPacket.TYPE, PackDataPacket.STREAM_CODEC, PackDataPacket::handle)); + new Clientbound<>(PackDataPacket.TYPE, PackDataPacket.STREAM_CODEC, PackDataPacket::handle), + new Clientbound<>(PackSoundPacket.TYPE, PackSoundPacket.STREAM_CODEC, PackSoundPacket::handle), + new Clientbound<>(MarkerPacket.TYPE, MarkerPacket.STREAM_CODEC, MarkerPacket::handle)); private ModNetwork() { } From d4c06053ec199917181fd1832c10b72f75e03769 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:25 +0800 Subject: [PATCH 0154/1461] Bring PackSync.java on 1.21.1 up to phase 12 --- .../src/main/java/com/jnhprojects/mcheli/network/PackSync.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java index 2a12ac25..e522f525 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java @@ -43,10 +43,12 @@ public static void onLogin(ServerPlayer player) { if (seesProblems(player)) { MCHeli.packs().reportProblems(player::sendSystemMessage); } + PackSubmissions.onLogin(player); } public static void onLogout(ServerPlayer player) { TRANSFERS.remove(player.getUUID()); + PackSubmissions.onLogout(player); } /** The packs the server uses now, as the client should load them. */ From 521c47e6f8b674b72d78dcdb190bcbfe987be979 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:26 +0800 Subject: [PATCH 0155/1461] Bring ModCreativeTabs.java on 1.21.1 up to phase 12 --- .../mcheli/registry/ModCreativeTabs.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java index 8b24af05..870aa8b8 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java @@ -3,6 +3,7 @@ import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ThrowableItem; import com.jnhprojects.mcheli.platform.Platform; import com.jnhprojects.mcheli.platform.Registered; import com.jnhprojects.mcheli.platform.Registration; @@ -16,8 +17,9 @@ import java.util.List; /** - * One tab per vehicle kind, listing a stack for each loaded definition of that kind. The list is - * read from the content registry whenever a tab is built, so reloaded content shows up. + * One tab per vehicle kind, listing a stack for each loaded definition of that kind, and one for + * the equipment and throwables. Those lists are read from the content registry whenever a tab is + * built, so reloaded content shows up. */ public final class ModCreativeTabs { public static final Registered HELICOPTERS = register("helicopters", "helicopter", "ah-64", @@ -25,6 +27,15 @@ public final class ModCreativeTabs { public static final Registered PLANES = register("planes", "plane", "f-35b", key(HELICOPTERS)); public static final Registered TANKS = register("tanks", "tank", "m1a2", key(PLANES)); public static final Registered VEHICLES = register("vehicles", "vehicle", "mk15", key(TANKS)); + public static final Registered EQUIPMENT = Registration.register(Registries.CREATIVE_MODE_TAB, "equipment", + key -> Platform.get().creativeTab(key(VEHICLES)) + .title(Component.translatable("itemGroup.mcheli.equipment")) + .icon(() -> new ItemStack(ModItems.WRENCH.get())) + .displayItems((params, output) -> { + ModItems.EQUIPMENT.forEach(item -> output.accept(item.get())); + MCHeli.content().throwables().forEach(def -> output.accept(ThrowableItem.stackOf(def))); + }) + .build()); private ModCreativeTabs() { } @@ -52,7 +63,7 @@ private static Registered register(String name, String kind, St /** Our tabs, for rebuilding their contents after a content reload. */ public static List all() { - return List.of(HELICOPTERS.get(), PLANES.get(), TANKS.get(), VEHICLES.get()); + return List.of(HELICOPTERS.get(), PLANES.get(), TANKS.get(), VEHICLES.get(), EQUIPMENT.get()); } /** Declares the entries; the loader module registers them. */ From 9d1416a2cb783c075e294d3d66f48ef842adec6c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:27 +0800 Subject: [PATCH 0156/1461] Bring ModDataComponents.java on 1.21.1 up to phase 12 --- .../com/jnhprojects/mcheli/registry/ModDataComponents.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java index 08b564eb..377a5783 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java @@ -18,6 +18,13 @@ public final class ModDataComponents { .networkSynchronized(ByteBufCodecs.STRING_UTF8) .build()); + /** The definition a throwable item throws, such as {@code m18_red}. */ + public static final Registered> THROWABLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "throwable", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + private ModDataComponents() { } From 2ee76849237cce0d58a1ed3686f73cd33af8987f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:27 +0800 Subject: [PATCH 0157/1461] Bring ModEntities.java on 1.21.1 up to phase 12 --- .../mcheli/registry/ModEntities.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java index 19441e8c..bc6526db 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java @@ -5,10 +5,16 @@ import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; import com.jnhprojects.mcheli.content.aircraft.TankDefinition; import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.entity.ContainerEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; import com.jnhprojects.mcheli.entity.GroundVehicleEntity; import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.ParachuteEntity; import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.RappelEntity; import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.UavStationEntity; import com.jnhprojects.mcheli.entity.projectile.A10Entity; import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; @@ -21,6 +27,7 @@ import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; import com.jnhprojects.mcheli.entity.projectile.RocketEntity; import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; import com.jnhprojects.mcheli.platform.Registered; import com.jnhprojects.mcheli.platform.Registration; @@ -65,6 +72,61 @@ public final class ModEntities { .noSave() .build(key.location().toString())); + public static final Registered> PARACHUTE = Registration.register(Registries.ENTITY_TYPE, "parachute", key -> EntityType.Builder + .of(ParachuteEntity::new, MobCategory.MISC) + .sized(1.5F, 0.6F) + .clientTrackingRange(10) + .updateInterval(1) + .build(key.location().toString())); + + public static final Registered> THROWABLE = Registration.register(Registries.ENTITY_TYPE, "throwable", key -> EntityType.Builder + .of(ThrowableEntity::new, MobCategory.MISC) + .sized(0.25F, 0.25F) + .clientTrackingRange(8) + .updateInterval(2) + .noSave() + .build(key.location().toString())); + + public static final Registered> GLTD = Registration.register(Registries.ENTITY_TYPE, "gltd", key -> EntityType.Builder + .of(GltdEntity::new, MobCategory.MISC) + .sized(0.5F, 0.5F) + .clientTrackingRange(10) + .updateInterval(3) + .build(key.location().toString())); + + public static final Registered> CONTAINER = Registration.register(Registries.ENTITY_TYPE, "container", key -> EntityType.Builder + .of(ContainerEntity::new, MobCategory.MISC) + .sized(2.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key.location().toString())); + + public static final Registered> CHAIN = Registration.register(Registries.ENTITY_TYPE, "chain", key -> EntityType.Builder + .of(ChainEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(2) + .fireImmune() + .build(key.location().toString())); + + public static final Registered> RAPPEL = Registration.register(Registries.ENTITY_TYPE, "rappel", key -> EntityType.Builder + .of(RappelEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key.location().toString())); + + public static final Registered> UAV_STATION = Registration.register(Registries.ENTITY_TYPE, "uav_station", key -> EntityType.Builder + .of(UavStationEntity::new, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key.location().toString())); + private ModEntities() { } From 40a3562526d406ad6d14d85488408208a20497f3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:28 +0800 Subject: [PATCH 0158/1461] Bring ModItems.java on 1.21.1 up to phase 12 --- .../jnhprojects/mcheli/registry/ModItems.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java index 7eec8784..ca2691f2 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java @@ -3,18 +3,31 @@ import com.jnhprojects.mcheli.content.ContentRegistry; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.entity.UavStationEntity; import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.LauncherItem; import com.jnhprojects.mcheli.item.LegacyAircraftItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.item.PlacedEntityItem; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.item.UavStationItem; +import com.jnhprojects.mcheli.item.WrenchItem; import com.jnhprojects.mcheli.platform.Registered; import com.jnhprojects.mcheli.platform.Registration; import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; +import java.util.List; import java.util.Map; /** * One item per vehicle kind; the stack says which vehicle (see {@link AircraftItem}). The 1.0.0 - * items, one per vehicle, are kept for the content present at startup so old worlds keep them. + * items, one per vehicle, are kept for the content present at startup so old worlds keep them. The + * equipment (fuel can, wrench, parachute and so on) has one item each, and the throwables share one. */ public final class ModItems { public static final Registered HELICOPTER = kind("helicopter"); @@ -22,6 +35,34 @@ public final class ModItems { public static final Registered TANK = kind("tank"); public static final Registered VEHICLE = kind("vehicle"); + public static final Registered FUEL = Registration.register(Registries.ITEM, "fuel", key -> new FuelItem(new Item.Properties())); + public static final Registered WRENCH = Registration.register(Registries.ITEM, "wrench", key -> new WrenchItem(new Item.Properties())); + public static final Registered PARACHUTE = Registration.register(Registries.ITEM, "parachute", key -> new ParachuteItem(new Item.Properties())); + public static final Registered GLTD = Registration.register(Registries.ITEM, "gltd", + key -> new PlacedEntityItem(ModEntities.GLTD, new Item.Properties().stacksTo(1))); + public static final Registered RANGEFINDER = Registration.register(Registries.ITEM, "rangefinder", key -> new RangefinderItem(new Item.Properties())); + public static final Registered CONTAINER = Registration.register(Registries.ITEM, "container", + key -> new PlacedEntityItem(ModEntities.CONTAINER, new Item.Properties())); + public static final Registered CHAIN = Registration.register(Registries.ITEM, "chain", key -> new ChainItem(new Item.Properties())); + public static final Registered STINGER_MISSILE = Registration.register(Registries.ITEM, "fim92_bullet", key -> new Item(new Item.Properties().stacksTo(16))); + public static final Registered STINGER = Registration.register(Registries.ITEM, "fim92", + key -> new LauncherItem("fim92", () -> ModItems.STINGER_MISSILE.get(), new Item.Properties())); + public static final Registered JAVELIN_MISSILE = Registration.register(Registries.ITEM, "fgm148_bullet", key -> new Item(new Item.Properties().stacksTo(16))); + public static final Registered JAVELIN = Registration.register(Registries.ITEM, "fgm148", + key -> new LauncherItem("fgm148", () -> ModItems.JAVELIN_MISSILE.get(), new Item.Properties())); + public static final Registered UAV_STATION = Registration.register(Registries.ITEM, "uav_station", + key -> new UavStationItem(UavStationEntity.STATION, new Item.Properties().stacksTo(1))); + public static final Registered UAV_CONTROLLER = Registration.register(Registries.ITEM, "uav_station2", + key -> new UavStationItem(UavStationEntity.PORTABLE, new Item.Properties().stacksTo(1))); + public static final Registered DRAFTING_TABLE = Registration.register(Registries.ITEM, "drafting_table", + key -> new BlockItem(ModBlocks.DRAFTING_TABLE.get(), new Item.Properties())); + /** Every throwable; the stack says which (see {@link ThrowableItem}). */ + public static final Registered THROWABLE = Registration.register(Registries.ITEM, "throwable", key -> new ThrowableItem(new Item.Properties())); + + /** The equipment tab, in the order it lists them. */ + public static final List> EQUIPMENT = List.of(FUEL, WRENCH, PARACHUTE, GLTD, RANGEFINDER, CONTAINER, CHAIN, + STINGER, STINGER_MISSILE, JAVELIN, JAVELIN_MISSILE, UAV_STATION, UAV_CONTROLLER, DRAFTING_TABLE); + private static final Map> BY_KIND = Map.of("helicopter", HELICOPTER, "plane", PLANE, "tank", TANK, "vehicle", VEHICLE); @@ -40,7 +81,7 @@ public static void init(ContentRegistry content) { for (AircraftDefinition def : content.all()) { String name = def.name(); // Only the imported content existed in 1.0.0; pack vehicles never had their own item. - if (!def.pack().equals(ContentSource.ORIGINAL) || BY_KIND.containsKey(name)) { + if (!def.pack().equals(ContentSource.ORIGINAL) || BY_KIND.containsKey(name) || EQUIPMENT.stream().anyMatch(e -> e.id().getPath().equals(name))) { // 1.0.0 could not have had such an item either: it would have clashed the same way. continue; } From f208a05d39f9aeac4e1b05b5aa9ec9c895d7e4fb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:28 +0800 Subject: [PATCH 0159/1461] Bring ModParticles.java on 1.21.1 up to phase 12 --- .../jnhprojects/mcheli/registry/ModParticles.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java index ce9a1da4..4ed53acd 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java @@ -1,6 +1,7 @@ package com.jnhprojects.mcheli.registry; import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; import com.jnhprojects.mcheli.platform.Registered; import com.jnhprojects.mcheli.platform.Registration; import com.mojang.serialization.MapCodec; @@ -24,6 +25,20 @@ public StreamCodec streamC } }); + /** Smoke grenades: a marker that vanishes under the particle limit is no marker. */ + public static final Registered> SMOKE = Registration.register(Registries.PARTICLE_TYPE, "smoke", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return SmokeParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return SmokeParticleOptions.STREAM_CODEC; + } + }); + private ModParticles() { } From 5b37f0e78659663877257a2bfcb5996552d74c62 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:29 +0800 Subject: [PATCH 0160/1461] Bring Resupply.java on 1.21.1 up to phase 12 --- .../jnhprojects/mcheli/weapon/Resupply.java | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java index 44df1ac2..533f5fba 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java @@ -10,30 +10,35 @@ import net.minecraft.world.item.Items; /** - * Resupplying a weapon from the crew member's inventory, the {@code supplyAmmo} path of the - * original. There the vehicle's inventory screen did it; until that screen exists a key does. - * One supply costs the weapon's {@code Item} lines and adds {@code SuppliedNum} rounds. + * Resupplying a weapon from a crew member's own items, the {@code supplyAmmo} path of the + * original's vehicle screen. One supply costs the weapon's {@code Item} lines and adds + * {@code SuppliedNum} rounds; it only works while the vehicle stands on something. */ public final class Resupply { private Resupply() { } - public static boolean fromInventory(ServerPlayer player, AircraftEntity vehicle) { + /** {@code canPlayerSupplyAmmo}: whether {@link #supply} would do anything right now. */ + public static boolean canSupply(ServerPlayer player, AircraftEntity vehicle, int weapon) { VehicleWeapons weapons = vehicle.weapons(); - int seat = vehicle.seatOf(player); - WeaponSet set = weapons != null ? weapons.current(seat) : null; - if (set == null || vehicle.isDestroyed()) { + WeaponSet set = weapons != null ? weapons.set(weapon) : null; + if (set == null || vehicle.isDestroyed() || player.getVehicle() != vehicle || !vehicle.canSupply()) { return false; } - WeaponDefinition w = set.definition(); if (set.ammo() + set.restAmmo() >= set.maxAmmo()) { return false; } + return player.getAbilities().instabuild || canPay(player.getInventory(), set.definition()); + } + + /** Pays for and adds one supply of {@code weapon}. Server only. */ + public static boolean supply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + if (!canSupply(player, vehicle, weapon)) { + return false; + } + WeaponSet set = vehicle.weapons().set(weapon); if (!player.getAbilities().instabuild) { - if (!canPay(player.getInventory(), w)) { - return false; - } - pay(player.getInventory(), w); + pay(player.getInventory(), set.definition()); } set.supply(); if (set.ammo() <= 0) { From 30de8c13d8a83bcb92a75d4be682d37b76ec1aea Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:29 +0800 Subject: [PATCH 0161/1461] Bring ServerFiring.java on 1.21.1 up to phase 12 --- .../mcheli/weapon/ServerFiring.java | 87 +++++-------------- 1 file changed, 24 insertions(+), 63 deletions(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java index 5a536bab..f7b6e969 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java @@ -8,7 +8,6 @@ import com.jnhprojects.mcheli.entity.HelicopterEntity; import com.jnhprojects.mcheli.entity.PlaneEntity; import com.jnhprojects.mcheli.entity.TankEntity; -import com.jnhprojects.mcheli.entity.projectile.A10Entity; import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; @@ -26,10 +25,7 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.tags.FluidTags; -import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; @@ -41,6 +37,9 @@ import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; + /** * What each weapon type does when the server fires it: the {@code shot} methods of the * {@code MCH_Weapon*} classes. Every method returns whether a round was used. @@ -93,14 +92,19 @@ private static Vec3 forward(float yaw, float pitch) { } private static void sound(WeaponSet set, Entity at) { - sound(set, at, set.definition().sound); + sound(set, at, soundName(set)); + } + + private static String soundName(WeaponSet set) { + return set.ownSound != null ? set.ownSound.sound() : set.definition().sound; } private static void sound(WeaponSet set, Entity at, String name) { if (set.soundAllowed) { WeaponDefinition w = set.definition(); float rnd = w.soundPitchRandom; - ModSounds.playAt(at, name, w.soundVolume, w.soundPitch * (1.0F - rnd) + RANDOM.nextFloat() * rnd); + float pitch = set.ownSound != null && set.ownSound.pitch() != null ? set.ownSound.pitch() : w.soundPitch; + ModSounds.playAt(at, name, w.soundVolume, pitch * (1.0F - rnd) + RANDOM.nextFloat() * rnd); } } @@ -335,20 +339,24 @@ private static boolean targetingPod(ServerLevel level, WeaponSet set, Shot shot) WeaponDefinition w = set.definition(); Entity aimer = shot.aimer(); if ((w.target & WeaponDefinition.TARGET_BLOCK) != 0) { - sound(set, aimer, "ng"); + // {@code MCH_Multiplay.markPoint}: the block under the crew's view, 300 blocks out, goes to the team. + boolean marked = aimer instanceof ServerPlayer player && Spotting.markPoint(player, new Vec3(shot.x, shot.y, shot.z), 300.0); + sound(set, aimer, marked ? soundName(set) : "ng"); return true; } - boolean spotted = false; + List spotted = new ArrayList<>(); double range = Math.max(w.length, 1.0F); for (LivingEntity entity : level.getEntitiesOfClass(LivingEntity.class, aimer.getBoundingBox().inflate(range))) { if (entity == aimer || shot.shooter.hasPassenger(entity) || aimer.distanceToSqr(entity) > range * range || !matchesTarget(entity, w.target) || !Guidance.inCone(aimer, aimer.getYRot(), aimer.getXRot(), entity, w.angle)) { continue; } - entity.addEffect(new MobEffectInstance(MobEffects.GLOWING, Math.max(w.markTime, 20))); - spotted = true; + spotted.add(entity.getId()); + } + if (aimer instanceof ServerPlayer player) { + Spotting.tellTeam(player, spotted, Math.max(w.markTime, 20)); } - sound(set, shot.shooter, spotted ? w.sound : "ng"); + sound(set, shot.shooter, !spotted.isEmpty() ? soundName(set) : "ng"); return true; } @@ -383,12 +391,10 @@ private static boolean cas(ServerLevel level, WeaponSet set, Shot shot) { set.casX = at.x; set.casY = at.y; set.casZ = at.z; - int direction = (int) Mth.positiveModulo(shot.aimer().getYRot() + 45.0F, 360.0F) / 90; - direction += RANDOM.nextBoolean() ? -1 : 1; - set.casDirection = Math.floorMod(direction, 4); + set.casDirection = Math.floorMod(CasStrike.approach(shot.aimer().getYRot()), 4); set.casUser = shot.user; set.casShooter = shot.shooter; - set.casStartTick = 50; + set.casStartTick = CasStrike.FIRST_PASS; set.casCount = 0; if (shot.user instanceof ServerPlayer player) { ModSounds.playTo(player, set.definition().sound, 1.0F, 1.0F); @@ -402,55 +408,10 @@ static void tick(AircraftEntity vehicle, WeaponSet set) { || !(vehicle.level() instanceof ServerLevel level)) { return; } - double x = 0.0; - double z = 0.0; - if (set.casCount >= 1) { - double sign = set.casCount == 1 ? 1.0 : -1.0; - if (set.casDirection % 2 == 0) { - x = RANDOM.nextDouble() * 10.0 * sign; - z = (RANDOM.nextDouble() - 0.5) * 10.0; - } else { - z = RANDOM.nextDouble() * 10.0 * sign; - x = (RANDOM.nextDouble() - 0.5) * 10.0; - } - } - spawnA10(level, set, set.casX + x, set.casY + 20.0, set.casZ + z); + Vec3 offset = CasStrike.passOffset(set.casCount, set.casDirection); + CasStrike.spawnA10(level, set.name(), set.definition(), set.casDirection, set.casX + offset.x, set.casY + 20.0, + set.casZ + offset.z, set.casShooter, set.casUser); set.casStartTick = set.ticksSinceShot(0) + 45; set.casCount++; } - - private static void spawnA10(ServerLevel level, WeaponSet set, double x, double y, double z) { - double mx = 0.0; - double mz = 0.0; - switch (set.casDirection) { - case 0 -> { - mz = 3.0; - z -= 90.0; - } - case 1 -> { - mx = -3.0; - x += 90.0; - } - case 2 -> { - mz = -3.0; - z += 90.0; - } - default -> { - mx = 3.0; - x -= 90.0; - } - } - A10Entity a10 = ModEntities.A10.get().create(level); - if (a10 == null) { - return; - } - a10.setPos(x, y, z); - WeaponDefinition w = set.definition(); - a10.setup(set.name(), set.casDirection, mx, mz, set.casShooter, set.casUser); - a10.power = w.power; - a10.explosionPower = w.explosion; - a10.acceleration = w.acceleration; - level.addFreshEntity(a10); - ModSounds.playAt(level, x, y, z, "a-10_snd", 150.0F, 1.0F); - } } From d921b6a8da20004929034db0316c5112f554aef9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:30 +0800 Subject: [PATCH 0162/1461] Bring VehicleWeapons.java on 1.21.1 up to phase 12 --- .../java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java index a5dc19b2..02f4c39b 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java @@ -70,7 +70,9 @@ public VehicleWeapons(AircraftEntity vehicle, AircraftDefinition def) { for (WeaponGroup group : def.weaponGroups) { WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); if (weapon != null && !group.mounts().isEmpty()) { - sets.add(new WeaponSet(weapon, group.mounts(), clientSide)); + WeaponSet set = new WeaponSet(weapon, group.mounts(), clientSide); + set.ownSound = def.weaponSounds.get(group.weaponType()); + sets.add(set); } } selected = new int[def.seatCount()]; From 211d65a6491722944274143a934ce08c23000e65 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:30 +0800 Subject: [PATCH 0163/1461] Bring WeaponSet.java on 1.21.1 up to phase 12 --- .../jnhprojects/mcheli/weapon/WeaponSet.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java index b358a4c9..3f539030 100644 --- a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java @@ -1,11 +1,17 @@ package com.jnhprojects.mcheli.weapon; import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; import com.jnhprojects.mcheli.content.weapon.WeaponType; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; import java.util.List; @@ -51,6 +57,8 @@ public interface Trigger { public int soundWait; /** Whether the shot being fired may play its sound, see {@code SoundDelay}. */ public boolean soundAllowed = true; + /** The vehicle's own sound for this weapon, from {@code WeaponSound}. */ + public @Nullable WeaponSound ownSound; /** Aim relative to the hull, following the crew member who operates the weapon. */ public float rotationYaw; @@ -276,6 +284,57 @@ public void waitForGroup(boolean reload) { } } + /** + * {@code getLandInDistance}: how far out, measured flat, a shell fired now would come down, + * stepping its arc a tick at a time as the projectile does; -1 if it would not come down in + * range. Mortars show it on the HUD. + */ + public double landingDistance(Entity shooter) { + if (def.gravity >= 0.0F || mounts.isEmpty()) { + return -1.0; + } + WeaponMount mount = mounts.get(currentMount); + float yaw = Mth.wrapDegrees(shooter.getYRot() + rotationYaw + mount.yaw()); + float pitch = Mth.wrapDegrees(shooter.getXRot() + rotationPitch + mount.pitch()); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + double acc = Math.min(def.acceleration, 4.0F); + double my = dir.y * def.acceleration; + if (my <= 0.0 || acc <= 0.0) { + return -1.0; + } + double mx = dir.x * def.acceleration; + double mz = dir.z * def.acceleration; + double gravity = def.gravity * (def.acceleration / acc); + double steps = my / def.gravity; + if (steps < -12.0) { + double f = steps / -12.0; + mx *= f; + my *= f; + mz *= f; + gravity *= f * f * 0.95; + } + Level level = shooter.level(); + double x = shooter.getX(); + double y = shooter.getY() + 3.0; + double z = shooter.getZ(); + for (int i = 0; i < 50; i++) { + Vec3 from = new Vec3(x, y, z); + Vec3 to = new Vec3(x + mx, y + my, z + mz); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shooter)); + if (hit.getType() == HitResult.Type.BLOCK) { + return Math.hypot(hit.getBlockPos().getX() - shooter.getX(), hit.getBlockPos().getZ() - shooter.getZ()); + } + my += gravity; + x += mx; + y += my; + z += mz; + if (y < shooter.getY()) { + return Math.hypot(x - shooter.getX(), z - shooter.getZ()); + } + } + return -1.0; + } + // Firing /** From 5cf02def4dea636f804633731cd4cdb86efefff4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:31 +0800 Subject: [PATCH 0164/1461] Wire McheliNeoForge.java into phases 8 to 12 on neoforge 1.21.1 --- .../java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java index 734b0ed6..a1a02c70 100644 --- a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java +++ b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli.neoforge; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; import com.jnhprojects.mcheli.CommonHooks; import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.item.VehicleIngredient; @@ -40,6 +41,7 @@ public McheliNeoForge(IEventBus modBus) { helper -> helper.register(VehicleIngredient.ID, NeoForgeVehicleIngredient.TYPE)); }); modBus.addListener(RegisterPayloadHandlersEvent.class, McheliNeoForge::registerPackets); + modBus.addListener(FMLCommonSetupEvent.class, event -> event.enqueueWork(CommonHooks::commonSetup)); modBus.addListener(AddPackFindersEvent.class, event -> event.addRepositorySource(CommonHooks.packSource())); NeoForge.EVENT_BUS.addListener(ServerAboutToStartEvent.class, event -> CommonHooks.serverAboutToStart(event.getServer())); From ba8c406953a4aabd8cb8868e05676d1df449e86e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:32 +0800 Subject: [PATCH 0165/1461] Wire NeoForgeClient.java into phases 8 to 12 on neoforge 1.21.1 --- .../mcheli/neoforge/NeoForgeClient.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java index 31b238aa..73c951b7 100644 --- a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java +++ b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -1,5 +1,10 @@ package com.jnhprojects.mcheli.neoforge; +import net.neoforged.neoforge.client.event.ViewportEvent; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; +import net.minecraft.client.Minecraft; +import com.jnhprojects.mcheli.neoforge.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; import com.jnhprojects.mcheli.client.ClientHooks; import com.jnhprojects.mcheli.client.particle.GlowParticle; import com.jnhprojects.mcheli.registry.ModParticles; @@ -27,11 +32,16 @@ private NeoForgeClient() { } static void init(IEventBus modBus) { - ClientHooks.init(); + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$loadEffect(effect)); modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); - modBus.addListener(RegisterParticleProvidersEvent.class, - event -> event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + modBus.addListener(RegisterMenuScreensEvent.class, event -> ClientHooks.registerMenuScreens(event::register)); + modBus.addListener(RegisterParticleProvidersEvent.class, event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); modBus.addListener(RegisterKeyMappingsEvent.class, event -> ClientHooks.keyMappings().forEach(event::register)); modBus.addListener(RegisterClientReloadListenersEvent.class, event -> ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); @@ -39,6 +49,7 @@ static void init(IEventBus modBus) { event -> event.registerAbove(VanillaGuiLayers.SLEEP_OVERLAY, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post.class, event -> ClientHooks.clientTickEnd()); + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeFov.class, event -> event.setFOV(ClientHooks.fov((float) event.getFOV()))); NeoForge.EVENT_BUS.addListener(RenderHandEvent.class, event -> event.setCanceled(ClientHooks.hidesHand())); NeoForge.EVENT_BUS.addListener(RenderLivingEvent.Pre.class, event -> ClientHooks.beforeRenderLiving(event.getEntity(), event.getPoseStack(), event.getPartialTick())); From 7dc3e174d226a5d6e8fe373c1a49c8ead05f5f58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:32 +0800 Subject: [PATCH 0166/1461] Wire mcheli.mixins.json into phases 8 to 12 on neoforge 1.21.1 --- neoforge-1.21.1/src/main/resources/mcheli.mixins.json | 1 + 1 file changed, 1 insertion(+) diff --git a/neoforge-1.21.1/src/main/resources/mcheli.mixins.json b/neoforge-1.21.1/src/main/resources/mcheli.mixins.json index 19d6b73e..77522ff3 100644 --- a/neoforge-1.21.1/src/main/resources/mcheli.mixins.json +++ b/neoforge-1.21.1/src/main/resources/mcheli.mixins.json @@ -5,6 +5,7 @@ "compatibilityLevel": "JAVA_21", "client": [ "CameraInvoker", + "GameRendererInvoker", "GameRendererMixin" ], "injectors": { From d16b62faebf41862994fe707dbf5aad2e4e5bf49 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:33 +0800 Subject: [PATCH 0167/1461] Build the three 1.21.1 modules again now that they have caught up --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7c2cfc6e..827d0840 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mod_license=MCHeli Rework Licence 1.0 (see LICENSE) # Platform modules built by this project, one per Minecraft version. Each has its own # gradle.properties with the Minecraft, loader and Java versions. -platforms=forge-26.3,neoforge-26.3,fabric-26.3 +platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 # Libraries the common module compiles against. Every supported Minecraft version ships them. slf4j_version=2.0.17 From cb6c9911a770532648fb47a2713fdc69aaed4a82 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:33 +0800 Subject: [PATCH 0168/1461] Note the 1.21.1 port and its fixes in the changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8045cd0b..a0aab9cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,14 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). - NeoForge and Fabric on Minecraft 26.3 catch up with Forge: everything from phases 8 to 12 now runs there, from the drafting table and vehicle screen to smoke and sight zoom. +- Minecraft 1.21.1 on Forge, NeoForge and Fabric gets everything from phases 8 to 12: fuel, equipment, + the vehicle screen, the drafting table, team manager, UAV stations and the workshop. On 1.21.1 the PVP + button of the team manager switches the server's PVP setting until a restart, since that version has + no game rule for it, and target markers ignore view bobbing. ### Changed -- Forge, NeoForge and Fabric on 26.3 are all built on every pull request again. +- Forge, NeoForge and Fabric on 26.3 and 1.21.1 are all built on every pull request again. - Clients and servers on different loaders cannot play together, which the roadmap used to promise. Each loader refuses the other at its own handshake, before the mod runs. A pack zip still works on every loader when copied by hand. @@ -22,6 +26,8 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). ### Fixed - Fabric clients crashed at startup on 26.3 and 1.21.1, because `RollableCamera` sat in the mixin package. +- Glow particles were invisible on Fabric 1.21.1: vanilla only draws the particle types on its own list. +- On 1.21.1 the E-767 recipe failed to load, since its UAV station ingredient used the newer format. ## [1.1.0-beta.8.1] - 2026-11-06 From e96241fa4285b7b1b6c393a3ae257c0e8ede339c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:34 +0800 Subject: [PATCH 0169/1461] Tick the 1.21.1 port on the roadmap --- docs/ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 778c9780..e3f7c6f3 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -308,8 +308,8 @@ added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest - [x] Phases 8 to 12 ported to NeoForge and Fabric on 26.3 - [x] Every resource the repository holds ships in the jar (betas 4 to 8 lacked recipes, the thermal shader, the smoke particle and the drafting table model) -- [ ] Phases 8 to 12 ported to all three loaders on 1.21.1 -- [ ] Every module in `platforms` +- [x] Phases 8 to 12 ported to all three loaders on 1.21.1 +- [ ] Every module in `platforms` (26.3 and 1.21.1 are in) - [ ] `minecraft-26.2`, `minecraft-26.1`, `minecraft-1.21.11`, `minecraft-1.21.5`, `minecraft-1.20.6`, `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its placeholder modules with real glue From 43838ec5d98cc81375d70ae01864c5c2a164d088 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:34 +0800 Subject: [PATCH 0170/1461] Describe what differs on 1.21.1 since phase 7 --- docs/ARCHITECTURE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 340758b4..fa3860ee 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -106,6 +106,19 @@ the betas it is not kept in step; each final release brings it, and every other - **Camera.** Forge 1.21.1 fires its camera event after the camera is placed, so Forge uses the event. NeoForge and Fabric hook `GameRenderer.renderLevel` just before the camera rotation turns into the view matrix, the spot Forge fires from. +- **Drafting table.** There is no recipe display system. The client holds every recipe, so the + screen reads its `RecipeManager` and `DraftingTablePacket` sends the recipe's id. +- **Thermal view.** Post effects are the older `shaders/post` chains, and vanilla loads programs + from the `minecraft` namespace only, so ours is `assets/minecraft/shaders/program/mcheli_thermal`. + `GameRenderer.loadEffect` is private, so each loader hands `ClientHooks.init` a way to call it. +- **Particles.** Smoke is a custom `ParticleRenderType` too. Vanilla only draws the types on its + own list, so `ParticleEngineMixin` adds ours on Fabric; Forge and NeoForge draw every type in use. +- **Workshop preview.** With no GUI entity renderer, `WorkshopScreen` draws through + `AircraftRenderer.draw` the way vanilla draws a mob in the inventory, then clears the depth + buffer so the gizmos stay on top. +- **Shaders.** There is no `IrisCompat`: Iris's pipeline assignment only exists for 26.x. +- **Teams.** There is no PVP game rule or `TeamColor`. The team manager switches the server's PVP + setting, which lasts until a restart, and picks from the `ChatFormatting` colours. `client` is only referenced from the loader modules behind a physical client check, so a dedicated server never loads a client class. From 42539318310148619efe99da93983a352c48c5c5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:35 +0800 Subject: [PATCH 0171/1461] List the 1.21.1 modules among those built --- docs/DEVELOPMENT.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 3a55846c..a655e7ec 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -41,7 +41,8 @@ Shared properties live in the root `gradle.properties`: `mod_version`, the mod m of loader modules (`platforms`) and the library versions `common` compiles against. Each loader module has its own `gradle.properties` with its loader, Minecraft and Java versions. During the betas up to beta.8, `platforms` listed only `forge-26.3`. Roadmap phase 13 adds modules back as each -one catches up, currently the three on 26.3; the others stay in the repository until they are ported. +one catches up, currently the three on 26.3 and the three on 1.21.1; the others stay in the repository +until they are ported. `mods.toml`, `neoforge.mods.toml`, `fabric.mod.json` and `pack.mcmeta` are filled in from them at build time. What every loader module shares (source folders, jar contents, manifest, file names) is in `gradle/loader.gradle`. From 64c83878dfed99a741c59ba16586da8eb0d15014 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:08:35 +0800 Subject: [PATCH 0172/1461] Add 1.21.1 checks for icons, the workshop, PVP and Iris --- docs/TESTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/TESTING.md b/docs/TESTING.md index 56b65726..bea6cc61 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -56,6 +56,10 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi its icon. - [ ] Glow halos, tracers and the glow particle are additive and do not hide smoke behind them. - [ ] A vehicle hit by a weapon wobbles (hurt time) and takes damage only once per hit. +- [ ] Smoke grenades show their own icons, in the hand and in the drafting table's result slot. +- [ ] The workshop's model turns, zooms and pans with its gizmos staying on the right parts. +- [ ] The team manager's PVP button works, and PVP goes back to `server.properties` after a restart. +- [ ] With Sodium and Iris for 1.21.1, tracers, flares and smoke look right (or note how they look). ## Phase 1 checklist From b48c1a644dc5302f70963070d7ce609e9b7b1244 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:43 +0800 Subject: [PATCH 0173/1461] Add ClientConfig.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/ClientConfig.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ClientConfig.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ClientConfig.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ClientConfig.java new file mode 100644 index 00000000..ff258847 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ClientConfig.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Per player look and feel, in {@code config/mcheli-client.toml}. Nothing here changes how the + * game plays, so each player picks for themselves and the server never sees it. + */ +public final class ClientConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue NIGHT_GLOW; + private static final ConfigSpec.IntValue PACK_CACHE_SIZE; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("effects"); + NIGHT_GLOW = builder + .comment("Whether tracers, rocket motors, flares and explosions glow. The glow is faint by day", + "and strongest at night, in caves and under water.") + .define("nightGlow", true); + builder.pop(); + + builder.push("packs"); + PACK_CACHE_SIZE = builder + .comment("How much the content packs downloaded from servers may take up in mcheli/cache, in megabytes.", + "The packs used longest ago are deleted first; the current server's packs are always kept.", + "A single pack larger than this is not downloaded.") + .defineInRange("cacheSize", 256, 16, 16384); + builder.pop(); + + SPEC = builder.build(); + } + + private ClientConfig() { + } + + /** Reads {@code config/mcheli-client.toml}, writing it with the defaults if it is new. */ + public static void load(Path configDir) { + Path file = configDir.resolve(MCHeli.MOD_ID + "-client.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + public static boolean nightGlow() { + return SPEC.isLoaded() ? NIGHT_GLOW.get() : NIGHT_GLOW.getDefault(); + } + + public static long packCacheBytes() { + return (long) (SPEC.isLoaded() ? PACK_CACHE_SIZE.get() : PACK_CACHE_SIZE.getDefault()) << 20; + } +} From 4000a686a050d1478ae9680308dd2c91ab81239e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:43 +0800 Subject: [PATCH 0174/1461] Add CommonHooks.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/CommonHooks.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/CommonHooks.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/CommonHooks.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/CommonHooks.java new file mode 100644 index 00000000..2cf2f6a8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/CommonHooks.java @@ -0,0 +1,83 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.network.PackSync; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.repository.RepositorySource; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.level.storage.LevelResource; + +import java.util.List; + +/** + * What the mod does on game events that both sides see. Each loader module calls these from its + * own events; nothing here knows which loader that is. + */ +public final class CommonHooks { + private CommonHooks() { + } + + /** Once the registries are frozen, on the main thread: what vanilla keeps in its own tables. */ + public static void commonSetup() { + DispenserBlock.registerBehavior(ModItems.THROWABLE.get(), new ThrowableItem.DispenseBehavior()); + } + + /** + * Adds the imported original content to the resource packs (client) and data packs (server). + * Without the original zip there is nothing to add. + */ + public static RepositorySource packSource() { + return onLoad -> { + // Fabric builds some pack repositories before our initializer has run. + LegacyContent content = MCHeli.legacyContent(); + if (content != null) { + content.pack().ifPresent(onLoad); + } + }; + } + + /** Before the worlds load: read the world's server config, then load the content packs it allows. */ + public static void serverAboutToStart(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + MCHeli.content().replaceWith(MCHeli.packs().load(true)); + } + + public static void serverStopped() { + ServerConfig.unload(); + } + + public static void serverTickEnd(MinecraftServer server) { + PackSync.pump(server); + } + + public static void registerCommands(CommandDispatcher dispatcher) { + McheliCommand.register(dispatcher); + } + + /** + * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack. Every + * MCHeli recipe goes into a player's recipe book when they join: the drafting table lists them + * from there, and they can be found in the crafting table's book too. + */ + public static void playerLoggedIn(ServerPlayer player) { + PackSync.onLogin(player); + // A remote pilot whose drone did not come back with them is shown and made mortal again. + if (!(player.getVehicle() instanceof AircraftEntity uav && uav.isRemotePilot(player))) { + AircraftEntity.clearRemotePilot(player); + } + List> ours = player.level().getServer().getRecipeManager().getRecipes().stream() + .filter(holder -> holder.id().identifier().getNamespace().equals(MCHeli.MOD_ID)) + .toList(); + player.awardRecipes(ours); + } + + public static void playerLoggedOut(ServerPlayer player) { + PackSync.onLogout(player); + } +} From 8ab4e1a9642ecea8ca5b5cfe970ca8f2e2f23956 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:44 +0800 Subject: [PATCH 0175/1461] Add ContentPacks.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/ContentPacks.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ContentPacks.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ContentPacks.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ContentPacks.java new file mode 100644 index 00000000..d3bfdee5 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ContentPacks.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackFolder; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * The content packs in {@code /mcheli/packs}, loaded together with the imported 1.0.3 + * content, which always comes first. + * + *

At startup every pack in the folder is loaded, so the vehicles exist before any world does. + * When a server starts (single player included) and on {@code /mcheli reload}, the folder is + * scanned again and the server config applied: the allow list, the size limit per pack and the + * limit for all packs together. The packs that pass are what players get sent. + * + *

A zip pack is read in place, so packs stay open while their definitions and assets are in + * use. The set a reload replaces is closed on the reload after that, so a client in the same game + * that is still drawing from it never reads from a closed zip. + */ +public final class ContentPacks { + private static final int PROBLEMS_IN_CHAT = 8; + private static final String SOUNDS = "assets/mcheli/sounds/"; + + private final Path folder; + private final PendingPacks pending; + private final LegacyContent legacy; + private volatile List active = List.of(); + private volatile List problems = List.of(); + private volatile Set sounds = Set.of(); + private List retired = List.of(); + + ContentPacks(Path gameDir, LegacyContent legacy) { + this.folder = gameDir.resolve(MCHeli.MOD_ID).resolve("packs"); + this.pending = new PendingPacks(gameDir.resolve(MCHeli.MOD_ID).resolve("pending")); + this.legacy = legacy; + } + + /** {@code /mcheli/packs}. */ + public Path folder() { + return folder; + } + + /** The packs whose definitions are loaded, in load order. The import is not among them. */ + public List active() { + return active; + } + + /** Everything that went wrong in the last load, packs left out included. */ + public List problems() { + return problems; + } + + /** + * Scans the folder and loads the import and the packs. Never throws; whatever goes wrong is + * logged and kept in {@link #problems()}. + * + * @param serverRules whether to apply the server config; off at startup, when no world exists + * @return the new content, for the caller to swap in + */ + public synchronized ContentRegistry load(boolean serverRules) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not create {}", folder, e); + } + + PackFolder.Scan scan = PackFolder.scan(folder); + List found = new ArrayList<>(scan.problems()); + List accepted = new ArrayList<>(); + long total = 0; + for (ContentPack pack : scan.packs()) { + long size; + String refusal; + try { + size = pack.archiveSize(); + refusal = serverRules ? refusal(pack, size, total) : null; + } catch (IOException e) { + size = 0; + refusal = "left out, could not be read: " + e; + } + if (refusal == null) { + accepted.add(pack); + total += size; + } else { + found.add(new PackProblem(pack.id(), refusal)); + close(List.of(pack)); + } + } + for (PackProblem problem : found) { + MCHeli.LOGGER.warn("Content pack {}", problem); + } + + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, legacy.assets())); + for (ContentPack pack : accepted) { + sources.add(pack.contentSource()); + } + ContentRegistry registry = ContentRegistry.load(sources); + found.addAll(registry.problems()); + + close(retired); + PendingPacks.cleanReplaced(folder); + retired = active; + active = List.copyOf(accepted); + problems = List.copyOf(found); + sounds = soundsOf(accepted); + MCHeli.LOGGER.info("Content packs in use: {}", active.isEmpty() ? "none" : active); + return registry; + } + + /** Sound names as {@code RuntimeAssets.soundFile} finds them: {@code heli.ogg} and {@code heli1.ogg} are both {@code heli}. */ + private static Set soundsOf(List packs) { + Set names = new HashSet<>(); + for (ContentPack pack : packs) { + for (String file : pack.files()) { + if (file.startsWith(SOUNDS) && file.endsWith(".ogg")) { + String name = file.substring(SOUNDS.length(), file.length() - ".ogg".length()); + names.add(name); + if (name.endsWith("1")) { + names.add(name.substring(0, name.length() - 1)); + } + } + } + } + return Set.copyOf(names); + } + + /** Whether a pack in use brought this sound, so that the sound manager has not heard of it. */ + public boolean hasSound(String name) { + return sounds.contains(name); + } + + /** What players have sent this server, waiting for an operator. */ + public PendingPacks pending() { + return pending; + } + + private static @Nullable String refusal(ContentPack pack, long size, long total) { + if (!ServerConfig.isPackAllowed(pack.id())) { + return "left out, not on the server's allow list (packs.allowList in mcheli-server.toml)"; + } + if (size > ServerConfig.maxPackBytes()) { + return "left out, %.1f MB is more than the server allows for one pack (packs.maxPackSize, %d MB)" + .formatted(size / 1048576.0, ServerConfig.maxPackBytes() >> 20); + } + if (total + size > ServerConfig.maxTotalPackBytes()) { + return "left out, it would take the packs past the server's total limit (packs.maxTotalSize, %d MB)" + .formatted(ServerConfig.maxTotalPackBytes() >> 20); + } + return null; + } + + /** Sends the problems of the last load as chat lines, the first few of them. */ + public void reportProblems(Consumer out) { + List list = problems; + for (int i = 0; i < Math.min(list.size(), PROBLEMS_IN_CHAT); i++) { + out.accept(Component.translatable("message.mcheli.pack.problem", list.get(i).pack(), list.get(i).message())); + } + if (list.size() > PROBLEMS_IN_CHAT) { + out.accept(Component.translatable("message.mcheli.pack.more_problems", list.size() - PROBLEMS_IN_CHAT)); + } + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } +} From 3058741b49997b5c52a8430f964a4cd1bf85882d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:44 +0800 Subject: [PATCH 0176/1461] Add ContentReload.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/ContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ContentReload.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ContentReload.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ContentReload.java new file mode 100644 index 00000000..7846b7bf --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.network.PackSync; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +/** + * Reads the vehicle and weapon definitions from disk again while the game runs, for + * {@code /mcheli reload}: the server config, then the imported content and the content packs. + * Vehicles in the world pick up their new definition on their next tick (see + * {@code AircraftEntity#definition}). Every player gets the new pack list, downloads what changed + * and rebuilds their content the same way. + */ +public final class ContentReload { + private ContentReload() { + } + + /** @return the newly loaded content, already in use */ + public static ContentRegistry reload(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + ContentRegistry fresh = MCHeli.packs().load(true); + MCHeli.content().replaceWith(fresh); + MCHeli.LOGGER.info("Content reloaded: {} vehicles, {} weapons", fresh.all().size(), fresh.weapons().size()); + PackSync.sendListToAll(server); + return fresh; + } +} From 82f4a346b3a78df2018d5d874063804dde749c15 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:45 +0800 Subject: [PATCH 0177/1461] Add LegacyContent.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/LegacyContent.java | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/LegacyContent.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/LegacyContent.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/LegacyContent.java new file mode 100644 index 00000000..495bb502 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/LegacyContent.java @@ -0,0 +1,239 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ImportTarget; +import com.jnhprojects.mcheli.content.LegacyImporter; +import net.minecraft.network.chat.Component; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PathPackResources; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackCompatibility; +import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.world.flag.FeatureFlagSet; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Stream; + +/** + * The original vehicles, models, textures and sounds, imported from the player's own copy of + * MCHeli 1.0.3. + * + *

That content is EMB4's and is not redistributed with this mod. Players download the original + * zip from the MCHeli CurseForge page and put it in {@code /mcheli}. On start the zip is + * converted once by {@link LegacyImporter} into {@code mcheli/imported}, which then serves as a + * required resource pack and data pack. The zip is converted again only when it changes or the + * importer's output format does; once imported it may be deleted. + * + *

Development runs point the {@code mcheli.legacyZip} system property at the zip instead. + */ +public final class LegacyContent { + /** The file name the original download has. Any zip with the same layout works. */ + public static final String ZIP_NAME = "MC1.7.10_mcheli_1.0.3.zip"; + + private static final String PACK_ID = "mcheli_content"; + private static final String MARKER = "import.properties"; + /** The pack layout this Minecraft version reads. */ + private static final ImportTarget TARGET = ImportTarget.MODERN; + + private final Path folder; + private final Path packRoot; + private @Nullable Component problem; + + private LegacyContent(Path folder) { + this.folder = folder; + this.packRoot = folder.resolve("imported"); + } + + /** Imports the zip if needed. Never throws; a missing or broken zip leaves the mod without content. */ + static LegacyContent prepare(Path gameDir) { + LegacyContent content = new LegacyContent(gameDir.resolve(MCHeli.MOD_ID)); + try { + content.importIfNeeded(); + } catch (IOException | UncheckedIOException e) { + MCHeli.LOGGER.error("Could not import the original MCHeli content", e); + content.problem = Component.translatable("message.mcheli.content.import_failed", e.getMessage()); + } + return content; + } + + /** The imported {@code assets/mcheli} folder. It does not exist when nothing was imported. */ + public Path assets() { + return packRoot.resolve("assets").resolve(MCHeli.MOD_ID); + } + + /** Why the vehicles are missing, for the player; empty when the content is in place. */ + public Optional problem() { + return Optional.ofNullable(problem); + } + + /** + * The imported folder as a pack, for both the client resource packs and the server data packs. + * Empty when nothing was imported. + */ + public Optional pack() { + if (!Files.isDirectory(packRoot)) { + return Optional.empty(); + } + PackLocationInfo location = new PackLocationInfo(PACK_ID, + Component.translatable("pack.mcheli.content"), PackSource.BUILT_IN, Optional.empty()); + Pack.Metadata metadata = new Pack.Metadata(Component.translatable("pack.mcheli.content.description"), + PackCompatibility.COMPATIBLE, FeatureFlagSet.of(), List.of()); + // Required, so it is enabled in every world and cannot be switched off. It sits at the + // bottom so that regular resource packs can still replace its textures and models. + return Optional.of(new Pack(location, new PathPackResources.PathResourcesSupplier(packRoot), metadata, + new PackSelectionConfig(true, Pack.Position.BOTTOM, false))); + } + + private void importIfNeeded() throws IOException { + Path zip = findZip(); + if (zip == null) { + if (Files.isDirectory(packRoot)) { + MCHeli.LOGGER.info("Using the MCHeli content imported earlier into {}", packRoot); + } else { + MCHeli.LOGGER.error("MCHeli content not found. Download {} from the MCHeli CurseForge page and put it in {}", + ZIP_NAME, folder.toAbsolutePath()); + problem = Component.translatable("message.mcheli.content.missing", ZIP_NAME, folder.toAbsolutePath().toString()); + } + return; + } + + Properties source = describe(zip); + if (source.equals(readMarker())) { + return; + } + + MCHeli.LOGGER.info("Importing MCHeli content from {}", zip); + Path staging = folder.resolve("imported.tmp"); + deleteTree(staging); + LegacyImporter.Summary summary = LegacyImporter.run(zip, staging, TARGET); + try (OutputStream out = Files.newOutputStream(staging.resolve(MARKER))) { + source.store(out, "Written by MCHeli Rework. Delete this folder to import again."); + } + swapIn(staging); + + MCHeli.LOGGER.info("Imported {} files, {} sounds, {} item models and {} recipes into {}", + summary.files(), summary.sounds(), summary.itemModels(), summary.recipes(), packRoot); + for (String skipped : summary.skippedRecipes()) { + MCHeli.LOGGER.info("Recipe left out: {}", skipped); + } + } + + /** + * Replaces the previous import with the fresh one in {@code staging}. + * + *

On Windows a folder that was just deleted keeps its name for a moment while another + * program (a file watcher, a virus scanner) still has a handle in it, and renaming onto that + * name fails with "access denied". So the old import is moved out of the way rather than + * deleted, the renames are retried for a few seconds, and the old copy is deleted last, on a + * best effort basis; a copy left behind is removed on the next import. + */ + private void swapIn(Path staging) throws IOException { + Path old = folder.resolve("imported.old"); + deleteQuietly(old); + if (Files.exists(packRoot)) { + try { + moveWithRetry(packRoot, old); + } catch (IOException e) { + deleteTree(packRoot); + } + } + moveWithRetry(staging, packRoot); + deleteQuietly(old); + } + + private static void moveWithRetry(Path from, Path to) throws IOException { + for (int attempt = 1; ; attempt++) { + try { + Files.move(from, to); + return; + } catch (FileSystemException e) { + if (attempt >= 15) { + throw e; + } + try { + Thread.sleep(50L * attempt); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw e; + } + } + } + } + + private static void deleteQuietly(Path root) { + try { + deleteTree(root); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not delete {}; it is removed on the next import", root, e); + } + } + + private @Nullable Path findZip() throws IOException { + String configured = System.getProperty("mcheli.legacyZip"); + if (configured != null && !configured.isBlank()) { + Path zip = Path.of(configured); + if (Files.isRegularFile(zip)) { + return zip; + } + MCHeli.LOGGER.warn("mcheli.legacyZip points to {}, which does not exist", zip); + } + + Files.createDirectories(folder); + if (Files.isRegularFile(folder.resolve(ZIP_NAME))) { + return folder.resolve(ZIP_NAME); + } + // Browsers rename repeated downloads, so take any zip, the same way every start. + try (Stream files = Files.list(folder)) { + return files.filter(p -> p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) + .filter(Files::isRegularFile) + .sorted() + .findFirst() + .orElse(null); + } + } + + /** What the current import was made from. Comparing size and time is enough to notice a new zip. */ + private static Properties describe(Path zip) throws IOException { + Properties p = new Properties(); + p.setProperty("format", Integer.toString(LegacyImporter.FORMAT)); + p.setProperty("target", TARGET.name()); + p.setProperty("source", zip.toAbsolutePath().normalize().toString()); + p.setProperty("size", Long.toString(Files.size(zip))); + p.setProperty("modified", Long.toString(Files.getLastModifiedTime(zip).toMillis())); + return p; + } + + private Properties readMarker() throws IOException { + Properties p = new Properties(); + Path marker = packRoot.resolve(MARKER); + if (Files.isRegularFile(marker)) { + try (InputStream in = Files.newInputStream(marker)) { + p.load(in); + } + } + return p; + } + + private static void deleteTree(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + try (Stream paths = Files.walk(root)) { + for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) { + Files.delete(p); + } + } + } +} From 56eeed9c69c175a31660cd0b508bad23d54be9d1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:45 +0800 Subject: [PATCH 0178/1461] Add MCHeli.java to the 26.2 shared sources --- .../java/com/jnhprojects/mcheli/MCHeli.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/MCHeli.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/MCHeli.java new file mode 100644 index 00000000..1a890651 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/MCHeli.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.registry.ModParticles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The mod, minus the loader. Each loader module's entry point installs its {@link Platform}, calls + * {@link #init()}, registers what {@code Registration} collected and wires its events to + * {@link CommonHooks} (and, on a client, {@code ClientHooks}). + */ +public final class MCHeli { + public static final String MOD_ID = "mcheli"; + public static final Logger LOGGER = LoggerFactory.getLogger(MCHeli.class); + + private static LegacyContent legacyContent; + private static ContentPacks packs; + private static ContentRegistry content; + + private MCHeli() { + } + + /** + * Imports the original content if needed, loads the definitions and declares the registry + * entries. The vehicle list must be known here: 1.0.0 had an item per vehicle, and those items + * are still registered so old worlds keep them. + */ + public static void init() { + Platform platform = Platform.get(); + LOGGER.info("MCHeli Rework {} on {}", platform.modVersion(), platform.loaderName()); + legacyContent = LegacyContent.prepare(platform.gameDir()); + packs = new ContentPacks(platform.gameDir(), legacyContent); + content = packs.load(false); + ModBlocks.init(); + ModBlockEntities.init(); + ModDataComponents.init(); + ModEntities.init(); + ModParticles.init(); + ModMenus.init(); + ModItems.init(content); + ModCreativeTabs.init(); + } + + /** + * Vehicle and weapon definitions, from the imported original content and the content packs. + * Available from mod construction onwards. Always the same object; a reload changes what it + * holds. + */ + public static ContentRegistry content() { + return content; + } + + /** Where the original vehicle content came from, and why it is missing if it is. */ + public static LegacyContent legacyContent() { + return legacyContent; + } + + /** The content packs in {@code mcheli/packs} and the ones in use. */ + public static ContentPacks packs() { + return packs; + } +} From 4ea1aff1da5842b88609aaaf1885064da3f35ce2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:46 +0800 Subject: [PATCH 0179/1461] Port McheliCommand to 26.2, where placing an item back takes no prediction --- .../com/jnhprojects/mcheli/McheliCommand.java | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/McheliCommand.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/McheliCommand.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/McheliCommand.java new file mode 100644 index 00000000..25a3aea8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/McheliCommand.java @@ -0,0 +1,262 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.Definition; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.network.PackSubmissions; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * {@code /mcheli}: what content is loaded, and operator tools to hand out vehicles and reload the + * definitions and content packs. + *

+ *   /mcheli                              content source and counts
+ *   /mcheli list [helicopter|plane|...]  loaded vehicles
+ *   /mcheli packs                        content packs in use, and their problems
+ *   /mcheli give <players> <vehicle> [count]   (operators)
+ *   /mcheli reload                       re-read the definitions and packs (operators)
+ *   /mcheli pending                      packs players have sent, waiting (operators)
+ *   /mcheli approve <pack>               put a waiting pack in use (operators)
+ *   /mcheli reject <pack>                throw a waiting pack away (operators)
+ * 
+ */ +public final class McheliCommand { + private static final List KINDS = List.of("helicopter", "plane", "tank", "vehicle"); + private static final DynamicCommandExceptionType UNKNOWN_VEHICLE = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.unknown_vehicle", name)); + private static final DynamicCommandExceptionType UNKNOWN_PENDING = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.pending.unknown", name)); + + private McheliCommand() { + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal(MCHeli.MOD_ID) + .executes(McheliCommand::status) + .then(Commands.literal("list") + .executes(ctx -> list(ctx, null)) + .then(Commands.argument("kind", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest(KINDS, builder)) + .executes(ctx -> list(ctx, StringArgumentType.getString(ctx, "kind"))))) + .then(Commands.literal("packs") + .executes(McheliCommand::packs)) + .then(Commands.literal("give") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("targets", EntityArgument.players()) + .then(Commands.argument("vehicle", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest( + MCHeli.content().all().stream().map(AircraftDefinition::name), builder)) + .executes(ctx -> give(ctx, 1)) + .then(Commands.argument("count", IntegerArgumentType.integer(1, 64)) + .executes(ctx -> give(ctx, IntegerArgumentType.getInteger(ctx, "count"))))))) + .then(Commands.literal("reload") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::reload)) + .then(Commands.literal("pending") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::pending)) + .then(Commands.literal("approve") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::approve))) + .then(Commands.literal("reject") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::reject)))); + } + + private static CompletableFuture suggestPending(CommandContext ctx, SuggestionsBuilder builder) { + try { + return SharedSuggestionProvider.suggest(MCHeli.packs().pending().list().stream().map(PendingPacks.Submission::id), builder); + } catch (IOException e) { + return builder.buildFuture(); + } + } + + private static int pending(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List waiting; + try { + waiting = MCHeli.packs().pending().list(); + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + if (waiting.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.none"), false); + } + for (PendingPacks.Submission submission : waiting) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.entry", submission.id(), submission.manifest().version(), + submission.manifest().author(), submission.submitter(), "%.1f".formatted(submission.size() / 1048576.0), + String.join(", ", submission.definitions())), false); + List replaced = replaced(submission); + if (!replaced.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.replaces", String.join(", ", replaced)), false); + } + } + return waiting.size(); + } + + /** + * What going live would take the place of: a pack with the same id, and vehicles or weapons of + * another pack with the same file name. That is allowed, so it is the operator's call to make, + * but it should not come as a surprise. + */ + private static List replaced(PendingPacks.Submission submission) { + List names = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.id().equals(submission.id())) { + names.add("pack " + pack.id()); + } + } + ContentRegistry content = MCHeli.content(); + for (String path : submission.definitions()) { + String name = path.substring(path.lastIndexOf('/') + 1, path.length() - ".txt".length()); + Definition existing = path.startsWith("weapons/") ? content.weapon(name) : content.any(name); + if (existing != null && !existing.pack().equals(submission.id())) { + names.add(name + " (" + existing.pack() + ")"); + } + } + return names; + } + + private static PendingPacks.Submission waiting(CommandContext ctx) throws CommandSyntaxException { + String id = StringArgumentType.getString(ctx, "pack"); + PendingPacks.Submission submission; + try { + submission = MCHeli.packs().pending().find(id); + } catch (IOException e) { + submission = null; + } + if (submission == null) { + throw UNKNOWN_PENDING.create(id); + } + return submission; + } + + private static int approve(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + if (PackSubmissions.install(source.getServer(), submission)) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.approve.success", submission.id(), submission.submitter()), true); + return 1; + } + source.sendFailure(Component.translatable("commands.mcheli.approve.left_out", submission.id())); + MCHeli.packs().reportProblems(source::sendFailure); + } catch (IOException | PackException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.getMessage())); + } + return 0; + } + + private static int reject(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + PackSubmissions.reject(source.getServer(), submission); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reject.success", submission.id(), submission.submitter()), true); + return 1; + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + } + + private static int status(CommandContext ctx) { + ContentRegistry content = MCHeli.content(); + CommandSourceStack source = ctx.getSource(); + MCHeli.legacyContent().problem().ifPresent(source::sendFailure); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status", + content.helicopters().size(), content.planes().size(), content.tanks().size(), content.vehicles().size(), + content.weapons().size(), MCHeli.legacyContent().assets().toString()), false); + List packs = MCHeli.packs().active(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status.packs", packs.size(), + packs.stream().map(ContentPack::id).collect(Collectors.joining(", "))), false); + return content.all().size(); + } + + private static int packs(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List packs = MCHeli.packs().active(); + if (packs.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.none", MCHeli.packs().folder().toString()), false); + } else { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.header", packs.size(), MCHeli.packs().folder().toString()), false); + } + ContentRegistry content = MCHeli.content(); + for (ContentPack pack : packs) { + long vehicles = content.all().stream().filter(def -> def.pack().equals(pack.id())).count(); + long weapons = content.weapons().stream().filter(def -> def.pack().equals(pack.id())).count(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.entry", pack.id(), pack.manifest().version(), + pack.manifest().name(), vehicles, weapons, ContentPack.shortHash(pack.hash())), false); + if (!pack.manifest().author().isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.author", pack.manifest().author()), false); + } + } + MCHeli.packs().reportProblems(source::sendFailure); + return packs.size(); + } + + private static int list(CommandContext ctx, String kind) { + List defs = MCHeli.content().all().stream() + .filter(def -> kind == null || def.kind().equals(kind)) + .toList(); + String names = defs.stream().map(AircraftDefinition::name).collect(Collectors.joining(", ")); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.list", defs.size(), names), false); + return defs.size(); + } + + private static int give(CommandContext ctx, int count) throws CommandSyntaxException { + String name = StringArgumentType.getString(ctx, "vehicle"); + AircraftDefinition def = MCHeli.content().any(name); + if (def == null) { + throw UNKNOWN_VEHICLE.create(name); + } + Collection targets = EntityArgument.getPlayers(ctx, "targets"); + for (ServerPlayer player : targets) { + // Vehicle items do not stack, so each one is a separate stack. + for (int i = 0; i < count; i++) { + player.getInventory().placeItemBackInInventory(AircraftItem.stackOf(def)); + } + } + Component vehicle = AircraftItem.stackOf(def).getHoverName(); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.give", count, vehicle, targets.size()), true); + return targets.size(); + } + + private static int reload(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + ContentRegistry fresh = ContentReload.reload(source.getServer()); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reload.success", + fresh.all().size(), fresh.weapons().size(), MCHeli.packs().active().size()), true); + MCHeli.packs().reportProblems(source::sendFailure); + return fresh.all().size(); + } +} From c3e7e1bc5c6a5bb3699c8bd1aeb680dd2c694f40 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:46 +0800 Subject: [PATCH 0180/1461] Add ModSounds.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/ModSounds.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ModSounds.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ModSounds.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ModSounds.java new file mode 100644 index 00000000..ed6eeb7e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ModSounds.java @@ -0,0 +1,64 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.core.Holder; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Sound events from {@code assets/mcheli/sounds.json}. + * + *

The events are not put in the registry. Content files name their sounds freely and a content + * pack may bring its own, so they are created on demand as direct holders, which the sound packet + * and the sound manager both accept. + * + *

A sound a pack brought is in no {@code sounds.json}, so the sound manager would drop it as + * unknown. For those the server sends our own packet, and the client streams the file. + */ +public final class ModSounds { + /** How far a sound carries at a volume of one, and per volume above that, as in vanilla. */ + private static final float RANGE = 16.0F; + + private static final Map EVENTS = new ConcurrentHashMap<>(); + + private ModSounds() { + } + + public static SoundEvent get(String name) { + return EVENTS.computeIfAbsent(name, n -> SoundEvent.createVariableRangeEvent(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, n))); + } + + /** Plays {@code name} at the entity for everyone nearby. Call on the server. */ + public static void playAt(Entity entity, String name, float volume, float pitch) { + playAt(entity.level(), entity.getX(), entity.getY(), entity.getZ(), name, volume, pitch); + } + + /** Plays {@code name} at a position for everyone nearby. Call on the server. */ + public static void playAt(Level level, double x, double y, double z, String name, float volume, float pitch) { + if (level instanceof ServerLevel server && MCHeli.packs().hasSound(name)) { + ModNetwork.sendNear(server, x, y, z, RANGE * Math.max(volume, 1.0F), new PackSoundPacket(name, x, y, z, volume, pitch)); + } else { + level.playSound(null, x, y, z, get(name), SoundSource.NEUTRAL, volume, pitch); + } + } + + /** Plays {@code name} for one player only, such as a hit confirmation. */ + public static void playTo(ServerPlayer player, String name, float volume, float pitch) { + if (MCHeli.packs().hasSound(name)) { + ModNetwork.sendToPlayer(player, new PackSoundPacket(name, player.getX(), player.getY(), player.getZ(), volume, pitch)); + } else { + player.connection.send(new ClientboundSoundPacket(Holder.direct(get(name)), SoundSource.PLAYERS, + player.getX(), player.getY(), player.getZ(), volume, pitch, player.getRandom().nextLong())); + } + } +} From 7b32f898e219609dd40ecb905f2ef90b12faea53 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:47 +0800 Subject: [PATCH 0181/1461] Add ServerConfig.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/ServerConfig.java | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ServerConfig.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ServerConfig.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ServerConfig.java new file mode 100644 index 00000000..9db31738 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/ServerConfig.java @@ -0,0 +1,172 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Server rules, the part of the original {@code mcheli.cfg} that decides how the game plays. + * The file lives per world in {@code serverconfig/mcheli-server.toml}, where Forge kept it, and is + * never sent to clients (only the server applies these rules). It is read when the world starts + * and again on {@code /mcheli reload}. + * + *

The values only exist while a world is loaded. Before that, and on clients, the getters return + * the defaults, which match 1.0.0. + */ +public final class ServerConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue EXPLOSIONS_BREAK_BLOCKS; + private static final ConfigSpec.BooleanValue EXPLOSIONS_START_FIRES; + private static final ConfigSpec.DoubleValue VEHICLE_DAMAGE_FACTOR; + private static final ConfigSpec.BooleanValue INFINITE_AMMO; + private static final ConfigSpec.BooleanValue INFINITE_FUEL; + private static final ConfigSpec.StringListValue DISABLED_VEHICLES; + private static final ConfigSpec.StringListValue PACK_ALLOW_LIST; + private static final ConfigSpec.IntValue MAX_PACK_SIZE; + private static final ConfigSpec.IntValue MAX_TOTAL_SIZE; + private static final ConfigSpec.BooleanValue ALLOW_SUBMISSIONS; + private static final ConfigSpec.BooleanValue REQUIRE_APPROVAL; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("explosions"); + EXPLOSIONS_BREAK_BLOCKS = builder + .comment("Whether weapons and crashing vehicles break blocks. The mobGriefing game rule also applies.", + "Original: Explosion_DestroyBlock") + .define("breakBlocks", true); + EXPLOSIONS_START_FIRES = builder + .comment("Whether incendiary weapons and crashes set blocks on fire. Original: Explosion_FlamingBlock") + .define("startFires", true); + builder.pop(); + + builder.push("vehicles"); + VEHICLE_DAMAGE_FACTOR = builder + .comment("Multiplies the damage every vehicle takes, after armour. 0.5 makes them twice as tough.") + .defineInRange("damageFactor", 1.0, 0.0, 100.0); + DISABLED_VEHICLES = builder + .comment("Vehicles that cannot be placed, by definition name (the file name, such as \"ah-64\").", + "Vehicles already in the world stay.") + .defineList("disabled", List.of()); + INFINITE_FUEL = builder + .comment("Whether vehicles never run out of fuel. Original: InfinityFuel") + .define("infiniteFuel", false); + builder.pop(); + + builder.push("weapons"); + INFINITE_AMMO = builder + .comment("Whether vehicle weapons never run out of ammunition. Original: InfinityAmmo") + .define("infiniteAmmo", false); + builder.pop(); + + builder.comment("Content packs from /mcheli/packs. Players get the packs from the server when they join.", + "Changes apply when the world starts or after /mcheli reload.") + .push("packs"); + PACK_ALLOW_LIST = builder + .comment("Pack ids the server loads, such as \"rework-example\". Empty loads every pack in the folder.") + .defineList("allowList", List.of()); + MAX_PACK_SIZE = builder + .comment("Largest pack the server loads and sends to players, in megabytes.") + .defineInRange("maxPackSize", 8, 1, 512); + MAX_TOTAL_SIZE = builder + .comment("Most the server loads and sends in all, in megabytes. Packs are taken in id order until", + "the next one would go over.") + .defineInRange("maxTotalSize", 64, 1, 4096); + ALLOW_SUBMISSIONS = builder + .comment("Whether players may send this server packs they made in the workshop. They wait in mcheli/pending", + "until an operator runs /mcheli approve. Off, players can only get packs from the server's owner.") + .define("allowSubmissions", false); + REQUIRE_APPROVAL = builder + .comment("Whether a submitted pack waits for an operator. Off, any player can add vehicles and weapons for", + "everyone, and even replace existing ones, so only turn it off among people you trust. A pack taking", + "the id of a pack already in use always waits.") + .define("requireApproval", true); + builder.pop(); + + SPEC = builder.build(); + } + + private ServerConfig() { + } + + /** Reads the world's file, writing it with the defaults if it is new. */ + public static void load(Path worldDir) { + Path file = worldDir.resolve("serverconfig").resolve(MCHeli.MOD_ID + "-server.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + /** The world closed: back to the defaults. */ + public static void unload() { + SPEC.unload(); + } + + public static boolean explosionsBreakBlocks() { + return SPEC.isLoaded() ? EXPLOSIONS_BREAK_BLOCKS.get() : EXPLOSIONS_BREAK_BLOCKS.getDefault(); + } + + public static boolean explosionsStartFires() { + return SPEC.isLoaded() ? EXPLOSIONS_START_FIRES.get() : EXPLOSIONS_START_FIRES.getDefault(); + } + + public static float vehicleDamageFactor() { + double factor = SPEC.isLoaded() ? VEHICLE_DAMAGE_FACTOR.get() : VEHICLE_DAMAGE_FACTOR.getDefault(); + return (float) factor; + } + + public static boolean infiniteAmmo() { + return SPEC.isLoaded() ? INFINITE_AMMO.get() : INFINITE_AMMO.getDefault(); + } + + public static boolean infiniteFuel() { + return SPEC.isLoaded() ? INFINITE_FUEL.get() : INFINITE_FUEL.getDefault(); + } + + /** Whether the allow list lets the server load this pack. An empty list allows every pack. */ + public static boolean isPackAllowed(String id) { + if (!SPEC.isLoaded() || PACK_ALLOW_LIST.get().isEmpty()) { + return true; + } + for (String allowed : PACK_ALLOW_LIST.get()) { + if (allowed.trim().toLowerCase(Locale.ROOT).equals(id)) { + return true; + } + } + return false; + } + + public static long maxPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_PACK_SIZE.get() : MAX_PACK_SIZE.getDefault()) << 20; + } + + public static long maxTotalPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_TOTAL_SIZE.get() : MAX_TOTAL_SIZE.getDefault()) << 20; + } + + public static boolean allowSubmissions() { + return SPEC.isLoaded() ? ALLOW_SUBMISSIONS.get() : ALLOW_SUBMISSIONS.getDefault(); + } + + public static boolean requireApproval() { + return SPEC.isLoaded() ? REQUIRE_APPROVAL.get() : REQUIRE_APPROVAL.getDefault(); + } + + public static boolean isDisabled(String vehicle) { + if (!SPEC.isLoaded()) { + return false; + } + for (String name : DISABLED_VEHICLES.get()) { + if (name.trim().toLowerCase(Locale.ROOT).equals(vehicle)) { + return true; + } + } + return false; + } +} From 04a77419771cf7eac6dbef70b8d204d4ff921044 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:47 +0800 Subject: [PATCH 0182/1461] Give DraftingTableBlock the codec 26.2 still requires --- .../mcheli/block/DraftingTableBlock.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java new file mode 100644 index 00000000..8545d2b5 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java @@ -0,0 +1,100 @@ +package com.jnhprojects.mcheli.block; + +import com.mojang.serialization.MapCodec; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +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.block.state.properties.IntegerProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jspecify.annotations.Nullable; + +/** + * The drafting table, {@code MCH_DraftingTableBlock}: right-click opens the recipe browser, and + * Shift and right-click switches its lamp. It turns in eighths of a circle to face whoever placed + * it, and its block entity draws the original model. + */ +public class DraftingTableBlock extends BaseEntityBlock { + private static final MapCodec CODEC = simpleCodec(DraftingTableBlock::new); + public static final IntegerProperty ROTATION = IntegerProperty.create("rotation", 0, 7); + public static final BooleanProperty LIT = BooleanProperty.create("lit"); + private static final VoxelShape SHAPE = Block.column(16.0, 0.0, 15.0); + private static final Component TITLE = Component.translatable("block.mcheli.drafting_table"); + + public DraftingTableBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(ROTATION, 0).setValue(LIT, false)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ROTATION, LIT); + } + + /** {@code onBlockPlacedBy}: the nearest eighth of the placer's heading. */ + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + float yaw = context.getPlayer() != null ? context.getPlayer().getYRot() : 0.0F; + int rotation = (int) ((Mth.positiveModulo(yaw, 360.0F) + 22.5F) / 45.0F) % 8; + return defaultBlockState().setValue(ROTATION, rotation); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive()) { + level.setBlock(pos, state.cycle(LIT), Block.UPDATE_ALL); + level.playSound(null, pos, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.BLOCKS, 0.3F, 0.5F); + } else { + player.openMenu(state.getMenuProvider(level, pos)); + } + return InteractionResult.SUCCESS; + } + + @Override + protected @Nullable MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { + return new SimpleMenuProvider((id, inventory, player) -> new DraftingTableMenu(id, inventory, ContainerLevelAccess.create(level, pos)), TITLE); + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + /** The block entity renderer draws the table. */ + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new DraftingTableBlockEntity(pos, state); + } +} From c7366cfa0f3646fdb87682cf75d6f6dd222d4664 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:48 +0800 Subject: [PATCH 0183/1461] Add block/DraftingTableBlockEntity.java to the 26.2 shared sources --- .../mcheli/block/DraftingTableBlockEntity.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java new file mode 100644 index 00000000..56238f3f --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.block; + +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** {@code MCH_DraftingTableTileEntity}: holds nothing; it is there so the table's model can be drawn. */ +public class DraftingTableBlockEntity extends BlockEntity { + public DraftingTableBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.DRAFTING_TABLE.get(), pos, state); + } +} From cc943cc34e99b309e2cc6b91d9cf9393ddc742bb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:48 +0800 Subject: [PATCH 0184/1461] Add client/CameraAngles.java to the 26.2 shared sources --- .../mcheli/client/CameraAngles.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java new file mode 100644 index 00000000..ab62e120 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.client; + +import net.minecraft.client.Camera; + +/** + * The camera's angles for this frame, after vanilla placed the camera and before it renders, as + * the loader module hands them to {@link ClientHooks#computeCameraAngles}. Forge and NeoForge + * back this with their viewport event; Fabric with a mixin at the same spot. + */ +public interface CameraAngles { + Camera camera(); + + float partialTick(); + + float yaw(); + + float pitch(); + + float roll(); + + void setYaw(float yaw); + + void setPitch(float pitch); + + void setRoll(float roll); +} From 9831f77b34f4f0cc250da2271d1a3158e5ba3c7c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:49 +0800 Subject: [PATCH 0185/1461] Add client/ClientContentReload.java to the 26.2 shared sources --- .../mcheli/client/ClientContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java new file mode 100644 index 00000000..70bcc23e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.CreativeModeTab; + +/** + * Makes the client show the content it now holds: after the server's pack list was applied, which + * also happens after {@code /mcheli reload}. + */ +final class ClientContentReload { + private ClientContentReload() { + } + + /** Drops cached models and textures so edited and new files load, and rebuilds the creative tabs. */ + static void refresh() { + Minecraft mc = Minecraft.getInstance(); + MeshCache.INSTANCE.clear(); + RuntimeAssets.refreshTextures(); + if (mc.level != null && mc.player != null) { + boolean operator = mc.options.operatorItemsTab().get() && mc.player.canUseGameMasterBlocks(); + CreativeModeTab.ItemDisplayParameters params = + new CreativeModeTab.ItemDisplayParameters(mc.level.enabledFeatures(), operator, mc.level.registryAccess()); + ModCreativeTabs.all().forEach(tab -> tab.buildContents(params)); + } + } +} From c9ff3892dc48dc912c71ffaf355c3cba7aa9a454 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:49 +0800 Subject: [PATCH 0186/1461] Let the loaders load post effects and read the camera partial tick on 26.2 --- .../mcheli/client/ClientHooks.java | 333 ++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java new file mode 100644 index 00000000..fe428514 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -0,0 +1,333 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.client.hud.HudScripts; +import com.jnhprojects.mcheli.client.render.A10Renderer; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import com.jnhprojects.mcheli.client.render.ChainRenderer; +import com.jnhprojects.mcheli.client.render.DraftingTableRenderer; +import com.jnhprojects.mcheli.client.render.EquipmentRenderer; +import com.jnhprojects.mcheli.client.render.FlareRenderer; +import com.jnhprojects.mcheli.client.render.GltdRenderer; +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.render.ParachuteRenderer; +import com.jnhprojects.mcheli.client.render.ProjectileRenderer; +import com.jnhprojects.mcheli.client.render.RiderPose; +import com.jnhprojects.mcheli.client.render.ThrowableRenderer; +import com.jnhprojects.mcheli.client.render.UavStationRenderer; +import com.jnhprojects.mcheli.client.screen.DraftingTableScreen; +import com.jnhprojects.mcheli.client.screen.TeamManagerScreen; +import com.jnhprojects.mcheli.client.screen.UavStationScreen; +import com.jnhprojects.mcheli.client.screen.VehicleScreen; +import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; +import com.jnhprojects.mcheli.client.workshop.WorkshopClient; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.team.Markers; +import com.jnhprojects.mcheli.team.TeamManager; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.NoopRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import java.util.List; +import java.util.Map; + +/** + * Client side counterpart of {@code CommonHooks}: what the mod does on client events. The loader + * module calls these from its own events. Kept out of the shared entry point so dedicated servers + * never load these classes. + */ +public final class ClientHooks { + /** The weapon HUD's layer, drawn above the vanilla HUD. */ + public static final Identifier HUD_LAYER = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "weapons"); + + private static boolean shaderProgramsAssigned; + + private ClientHooks() { + } + + /** Loads a post effect into the game renderer, whose own method is private on 26.2. */ + public interface PostEffects { + void load(Identifier effect); + } + + private static PostEffects postEffects = effect -> { + }; + + static PostEffects postEffects() { + return postEffects; + } + + /** During mod construction, on a client only. */ + public static void init(PostEffects effects) { + postEffects = effects; + ClientConfig.load(Platform.get().configDir()); + ClientAccess.install(() -> Minecraft.getInstance().player, ClientPacks.INSTANCE); + } + + /** How a loader registers an entity renderer. */ + public interface EntityRenderers { + void register(EntityType type, EntityRendererProvider provider); + } + + public static void registerEntityRenderers(EntityRenderers renderers) { + renderers.register(ModEntities.HELICOPTER.get(), AircraftRenderer::new); + renderers.register(ModEntities.PLANE.get(), AircraftRenderer::new); + renderers.register(ModEntities.TANK.get(), AircraftRenderer::new); + renderers.register(ModEntities.VEHICLE.get(), AircraftRenderer::new); + renderers.register(ModEntities.BULLET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.BOMB.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AA_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AS_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AT_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TV_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TORPEDO.get(), ProjectileRenderer::new); + renderers.register(ModEntities.MARKER_ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.DISPENSED_ITEM.get(), ProjectileRenderer::new); + renderers.register(ModEntities.FLARE.get(), FlareRenderer::new); + renderers.register(ModEntities.A10.get(), A10Renderer::new); + renderers.register(ModEntities.PARACHUTE.get(), ParachuteRenderer::new); + renderers.register(ModEntities.THROWABLE.get(), ThrowableRenderer::new); + renderers.register(ModEntities.GLTD.get(), GltdRenderer::new); + renderers.register(ModEntities.CHAIN.get(), ChainRenderer::new); + renderers.register(ModEntities.CONTAINER.get(), context -> new EquipmentRenderer<>(context, "container", 0.2F, 0xFFBFBFBF, 1.0F)); + renderers.register(ModEntities.UAV_STATION.get(), UavStationRenderer::new); + renderers.register(ModEntities.RAPPEL.get(), NoopRenderer::new); + } + + /** How a loader registers a block entity renderer. */ + public interface BlockEntityRenderers { + void register(BlockEntityType type, + BlockEntityRendererProvider provider); + } + + public static void registerBlockEntityRenderers(BlockEntityRenderers renderers) { + renderers.register(ModBlockEntities.DRAFTING_TABLE.get(), DraftingTableRenderer::new); + } + + /** How a loader registers the screen of a menu: vanilla's own method is not public on all of them. */ + public interface MenuScreenRegistrar { + > void register(MenuType type, + MenuScreens.ScreenConstructor screen); + } + + /** On the main thread during client setup, once our menu types are registered. */ + public static void registerMenuScreens(MenuScreenRegistrar screens) { + screens.register(ModMenus.VEHICLE.get(), VehicleScreen::new); + screens.register(ModMenus.UAV_STATION.get(), UavStationScreen::new); + screens.register(ModMenus.DRAFTING_TABLE.get(), DraftingTableScreen::new); + } + + public static List keyMappings() { + return KeyBindings.ALL; + } + + /** Client resource reload listeners, by the id Fabric wants for each. */ + public static Map reloadListeners() { + return Map.of(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "meshes"), MeshCache.INSTANCE, + Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "hud_scripts"), HudScripts.INSTANCE); + } + + public static void clientTickEnd() { + if (!shaderProgramsAssigned) { + // On the first tick rather than at construction, so Iris has surely loaded by now. + shaderProgramsAssigned = true; + Glow.assignShaderPrograms(); + } + CrewController.onClientTick(); + GltdController.onClientTick(); + RangefinderController.onClientTick(); + LauncherController.onClientTick(); + VehicleCamera.tick(); + WeaponController.onClientTick(); + HudRenderer.tick(); + EngineSoundPlayer.tick(); + TargetMarkers.tick(); + WorkshopClient.tick(); + while (KeyBindings.TEAM_MANAGER.consumeClick()) { + openTeamManager(); + } + } + + /** Operators get the team manager; anyone else is told it is theirs to open. */ + private static void openTeamManager() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.gui.screen() != null) { + return; + } + if (TeamManager.mayManage(mc.player)) { + mc.gui.setScreen(new TeamManagerScreen()); + } else { + mc.player.sendOverlayMessage(Component.translatable("message.mcheli.team.operators_only")); + } + } + + /** Tilts the view with the hull and moves the eye into the cockpit or onto a TV missile. */ + public static void computeCameraAngles(CameraAngles angles) { + CrewController.onCameraAngles(angles); + GltdController.onCameraAngles(angles); + } + + /** Sets the camera's rotation, roll included, the way Forge's {@code Camera#setRotation} does. */ + public interface CameraRotation { + void set(Camera camera, float yaw, float pitch, float roll); + } + + /** + * For loaders without Forge's camera angle event: called where Forge fires it, right after + * vanilla placed the camera for this frame. Starts from the camera's own angles and no roll, + * and only touches the rotation when something changed it. + */ + public static void computeCameraAngles(Camera camera, float partialTick, CameraRotation rotation) { + Angles angles = new Angles(camera, partialTick, camera.yRot(), camera.xRot()); + computeCameraAngles(angles); + if (angles.yaw != camera.yRot() || angles.pitch != camera.xRot() || angles.roll != 0.0F) { + rotation.set(camera, angles.yaw, angles.pitch, angles.roll); + } + } + + private static final class Angles implements CameraAngles { + private final Camera camera; + private final float partialTick; + private float yaw; + private float pitch; + private float roll; + + Angles(Camera camera, float partialTick, float yaw, float pitch) { + this.camera = camera; + this.partialTick = partialTick; + this.yaw = yaw; + this.pitch = pitch; + } + + @Override + public Camera camera() { + return camera; + } + + @Override + public float partialTick() { + return partialTick; + } + + @Override + public float yaw() { + return yaw; + } + + @Override + public float pitch() { + return pitch; + } + + @Override + public float roll() { + return roll; + } + + @Override + public void setYaw(float yaw) { + this.yaw = yaw; + } + + @Override + public void setPitch(float pitch) { + this.pitch = pitch; + } + + @Override + public void setRoll(float roll) { + this.roll = roll; + } + } + + /** The field of view for this frame, narrowed while the player zooms through a sight. */ + public static float fov(float fov) { + return LauncherController.fov(RangefinderController.fov(VehicleCamera.fov(fov))); + } + + /** Whether to skip drawing the first person hand this frame. */ + public static boolean hidesHand() { + return CrewController.hidesHand() || GltdController.hidesHand() || RangefinderController.isScoped() || LauncherController.isRaised(); + } + + /** What happens to a press of the use key. */ + public enum UseKey { + /** Vanilla handles it. */ + PASS, + /** Taken, and the hand does not swing: it is a weapon trigger. */ + CONSUME, + /** Taken, and the hand swings as for a vanilla click: the player boards a vehicle. */ + CONSUME_AND_SWING + } + + public static UseKey onUseKey(InteractionHand hand) { + if (WeaponController.onUseItem() || GltdController.onUseItem()) { + return UseKey.CONSUME; + } + return VehiclePicker.onUseItem(hand) ? UseKey.CONSUME_AND_SWING : UseKey.PASS; + } + + /** The pick block key switches weapons in a seat that has any; true when it was taken. */ + public static boolean onPickBlockKey() { + return WeaponController.onPickBlock(); + } + + /** The partial tick the camera entity is drawn at, for the loaders whose event does not carry it. */ + public static float cameraPartialTicks() { + Minecraft mc = Minecraft.getInstance(); + return mc.gameRenderer.mainCamera().getCameraEntityPartialTicks(mc.getDeltaTracker()); + } + + public static void beforeRenderLiving(LivingEntityRenderState state, PoseStack pose, float partialTicks) { + RiderPose.beforeRender(state, pose, partialTicks); + } + + public static void drawHud(GuiGraphicsExtractor graphics, DeltaTracker delta) { + WeaponHud.draw(graphics, delta); + GltdController.drawHud(graphics, delta); + RangefinderController.drawHud(graphics, delta); + LauncherController.drawHud(graphics, delta); + TargetMarkers.draw(graphics, delta); + } + + /** Without the original zip there are no vehicles, so say so where the player will see it. */ + public static void loggedIn(LocalPlayer player) { + MCHeli.legacyContent().problem().ifPresent(player::sendSystemMessage); + } + + public static void loggedOut() { + ClientPacks.INSTANCE.loggedOut(); + WorkshopClient.loggedOut(); + Markers.clear(); + } +} From 7c1817a473ba731ef2c2876958d263be338e000d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:49 +0800 Subject: [PATCH 0187/1461] Add client/ClientPacks.java to the 26.2 shared sources --- .../mcheli/client/ClientPacks.java | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java new file mode 100644 index 00000000..d933e518 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java @@ -0,0 +1,338 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackRequestPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import com.jnhprojects.mcheli.platform.Platform; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * The client half of the content pack sync (the server half is {@code PackSync}). + * + *

When the server sends its pack list, every pack this client has neither in its own + * {@code mcheli/packs} nor in {@code mcheli/cache} is requested. Each arrives in chunks, is checked + * against its hash and kept as {@code mcheli/cache/.zip}. Once all are there, the content is + * rebuilt from the client's own import plus exactly the server's packs, in the server's order, so + * the vehicle a player flies (which their client simulates) has the server's definition. + * + *

In single player, and for the host of a LAN game, client and server share one registry, so + * only the client's view of the server's packs is refreshed. + * + *

The cache is capped by {@code packs.cacheSize} in the client config: after a download, the + * packs used longest ago go first. The current server's packs are always kept. + */ +public final class ClientPacks implements ClientAccess.PackReceiver { + public static final ClientPacks INSTANCE = new ClientPacks(); + + private static final Pattern CACHED = Pattern.compile("[0-9a-f]{64}\\.zip"); + + private final Path cache = Platform.get().gameDir().resolve(MCHeli.MOD_ID).resolve("cache"); + private List wanted = List.of(); + private final Map downloads = new LinkedHashMap<>(); + /** Packs this class opened for the current server; closed when the next list replaces them. */ + private List mounted = List.of(); + private int downloaded; + + private ClientPacks() { + } + + @Override + public void onPackList(PackListPacket packet) { + if (Minecraft.getInstance().hasSingleplayerServer()) { + useRoots(MCHeli.packs().active()); + ClientContentReload.refresh(); + return; + } + + wanted = packet.packs(); + downloads.clear(); + downloaded = 0; + long cap = ClientConfig.packCacheBytes(); + List request = new ArrayList<>(); + long bytes = 0; + for (PackListPacket.Entry entry : wanted) { + if (find(entry) != null) { + continue; + } + if (entry.size() > cap || entry.size() <= 0) { + say(Component.translatable("message.mcheli.pack.too_large", entry.id(), mb(entry.size()), cap >> 20)); + continue; + } + downloads.put(entry.hash(), new Download(entry)); + request.add(entry.hash()); + bytes += entry.size(); + } + + if (request.isEmpty()) { + apply(); + } else { + say(Component.translatable("message.mcheli.pack.downloading", request.size(), mb(bytes))); + ModNetwork.sendToServer(new PackRequestPacket(request)); + } + } + + @Override + public void onPackData(PackDataPacket packet) { + Download download = downloads.get(packet.hash()); + if (download == null) { + return; + } + if (!download.accept(packet)) { + fail(download, "the server sent a chunk out of order"); + return; + } + if (!download.complete()) { + return; + } + downloads.remove(packet.hash()); + try { + store(download); + downloaded++; + } catch (IOException | PackException e) { + fail(download, e.getMessage()); + return; + } + if (downloads.isEmpty()) { + apply(); + } + } + + @Override + public void onPackSound(PackSoundPacket packet) { + Minecraft.getInstance().getSoundManager().play( + new OneShotSound(packet.sound(), packet.x(), packet.y(), packet.z(), packet.volume(), packet.pitch())); + } + + /** The packs the server sent or named, for the package browser. Empty when hosting the game. */ + public List serverPacks() { + return mounted; + } + + /** Leaving a server: its packs are no longer needed. */ + void loggedOut() { + downloads.clear(); + wanted = List.of(); + RuntimeAssets.setRoots(List.of()); + close(mounted); + mounted = List.of(); + } + + private void fail(Download download, String reason) { + downloads.remove(download.entry.hash()); + MCHeli.LOGGER.error("Content pack {} could not be downloaded: {}", download.entry.id(), reason); + say(Component.translatable("message.mcheli.pack.failed", download.entry.id(), reason)); + if (downloads.isEmpty()) { + apply(); + } + } + + /** Writes a finished download to the cache once its content matches the hash it was listed with. */ + private void store(Download download) throws IOException, PackException { + String hash = download.entry.hash(); + Files.createDirectories(cache); + Path part = cache.resolve(hash + ".download.zip"); + Files.write(part, download.data); + try (ContentPack pack = ContentPack.open(part)) { + if (!pack.hash().equals(hash)) { + throw new PackException("its files do not match the hash the server listed"); + } + } catch (IOException | PackException e) { + Files.deleteIfExists(part); + throw e; + } + Files.move(part, cache.resolve(hash + ".zip"), StandardCopyOption.REPLACE_EXISTING); + } + + /** Rebuilds the content from the import and the server's packs that are now on disk. */ + private void apply() { + List opened = new ArrayList<>(); + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, MCHeli.legacyContent().assets())); + for (PackListPacket.Entry entry : wanted) { + Path file = find(entry); + if (file == null) { + continue; + } + try { + ContentPack pack = ContentPack.open(file); + if (!pack.hash().equals(entry.hash())) { + pack.close(); + MCHeli.LOGGER.warn("Content pack {} changed on disk and no longer matches the server's", file); + continue; + } + opened.add(pack); + sources.add(pack.contentSource()); + if (file.startsWith(cache)) { + Files.setLastModifiedTime(file, FileTime.from(Instant.now())); + } + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not open content pack {}", file, e); + } + } + + ContentRegistry registry = ContentRegistry.load(sources); + MCHeli.content().replaceWith(registry); + List old = mounted; + mounted = List.copyOf(opened); + useRoots(mounted); + close(old); + ClientContentReload.refresh(); + trimCache(); + if (downloaded > 0) { + say(Component.translatable("message.mcheli.pack.ready", downloaded)); + downloaded = 0; + } + } + + /** The pack on disk with the entry's content: one of the player's own packs, or a cached download. */ + private @Nullable Path find(PackListPacket.Entry entry) { + for (ContentPack own : MCHeli.packs().active()) { + if (own.hash().equals(entry.hash())) { + return own.source(); + } + } + Path cached = cache.resolve(entry.hash() + ".zip"); + return Files.isRegularFile(cached) ? cached : null; + } + + /** Deletes the cached packs used longest ago until the cache fits its cap. */ + private void trimCache() { + Set keep = new HashSet<>(); + for (PackListPacket.Entry entry : wanted) { + keep.add(entry.hash() + ".zip"); + } + List files; + try (Stream list = Files.list(cache)) { + files = list.filter(p -> CACHED.matcher(p.getFileName().toString()).matches()) + .sorted(Comparator.comparing(ClientPacks::lastUsed)) + .toList(); + } catch (IOException e) { + return; + } + long total = 0; + for (Path file : files) { + total += size(file); + } + long cap = ClientConfig.packCacheBytes(); + for (Path file : files) { + if (total <= cap) { + break; + } + if (keep.contains(file.getFileName().toString())) { + continue; + } + try { + long size = size(file); + Files.delete(file); + total -= size; + MCHeli.LOGGER.info("Removed {} from the content pack cache", file.getFileName()); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not remove {} from the content pack cache", file, e); + } + } + } + + private static FileTime lastUsed(Path file) { + try { + return Files.getLastModifiedTime(file); + } catch (IOException e) { + return FileTime.fromMillis(0); + } + } + + private static long size(Path file) { + try { + return Files.size(file); + } catch (IOException e) { + return 0; + } + } + + /** The pack loaded last overrides the others, so it is searched first. */ + private static void useRoots(List packs) { + List roots = new ArrayList<>(); + for (ContentPack pack : packs) { + roots.addFirst(pack.assets()); + } + RuntimeAssets.setRoots(roots); + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } + + private static void say(Component message) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null) { + mc.player.sendSystemMessage(message); + } else { + MCHeli.LOGGER.info(message.getString()); + } + } + + private static String mb(long bytes) { + return "%.1f".formatted(bytes / 1048576.0); + } + + /** One pack arriving from the server. */ + private static final class Download { + private final PackListPacket.Entry entry; + private byte @Nullable [] data; + private int received; + + Download(PackListPacket.Entry entry) { + this.entry = entry; + } + + boolean accept(PackDataPacket packet) { + if (packet.total() != entry.size() || packet.offset() != received + || received + packet.data().length > entry.size()) { + return false; + } + if (data == null) { + data = new byte[entry.size()]; + } + System.arraycopy(packet.data(), 0, data, received, packet.data().length); + received += packet.data().length; + return true; + } + + boolean complete() { + return data != null && received == data.length; + } + } +} From e504f83ed2bedd49bab9f08583fbf58c27986da9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:50 +0800 Subject: [PATCH 0188/1461] Add client/CrewController.java to the 26.2 shared sources --- .../mcheli/client/CrewController.java | 482 ++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/CrewController.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/CrewController.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/CrewController.java new file mode 100644 index 00000000..32af6770 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/CrewController.java @@ -0,0 +1,482 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.CameraPosition; +import com.jnhprojects.mcheli.content.aircraft.SeatDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PilotStatePacket; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Input; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +/** + * Turns local player input into vehicle control, for whichever seat the player occupies. + * + *

Keys are sampled once per client tick. The mouse is handled every frame, the way the + * original did it: for helicopters and planes, vanilla first turns the player's view, then the + * change is fed to the aircraft and the view is snapped back onto the aircraft's nose. Everyone + * else looks around freely, within the limits of their seat, and turrets follow their view. + * + *

Gunner seats and camera positions move the first person eye to the point the definition + * gives, which is how the original's camera entity behaved. Every other seat keeps its eye above + * the seat along the hull's up axis, so it tilts with the vehicle. + */ +final class CrewController { + /** Two presses of jump within this many ticks eject the player. */ + private static final int EJECT_WINDOW = 10; + + private static boolean jumpWasDown; + private static int lastJumpPress = -100; + private static int ticks; + private static boolean tracking; + private static float lastYaw; + private static float lastPitch; + + private static @Nullable AircraftEntity viewVehicle; + private static int viewSeat = -1; + private static int cameraIndex; + private static boolean gunnerSight; + + private static @Nullable MethodHandle setCameraPosition; + private static boolean cameraLookupFailed; + + private CrewController() { + } + + /** + * Like the original, the first person hand stays hidden in a vehicle: always for the pilot, + * and for anyone else who looks through a camera or has a weapon to aim. + */ + static boolean hidesHand() { + LocalPlayer player = Minecraft.getInstance().player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + return false; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return false; + } + VehicleWeapons weapons = vehicle.weapons(); + boolean armed = weapons != null && weapons.selected(seat) >= 0; + return seat == 0 || armed || tvCamera(vehicle, seat) != null || eyePosition(vehicle, def, seat) != null; + } + + private static @Nullable AircraftEntity vehicleOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof AircraftEntity vehicle && vehicle.definition() != null ? vehicle : null; + } + + static void onClientTick() { + ticks++; + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + jumpWasDown = false; + tracking = false; + viewVehicle = null; + viewSeat = -1; + // The GLTD's sight shares the zoom and camera mode; only leaving it resets them. + if (GltdController.gltdOf(player) == null) { + VehicleCamera.reset(); + } + return; + } + int seat = vehicle.seatOf(player); + if (vehicle != viewVehicle || seat != viewSeat) { + // New vehicle or seat: start from the default view. + viewVehicle = vehicle; + viewSeat = seat; + cameraIndex = 0; + gunnerSight = false; + tracking = false; + VehicleCamera.reset(); + } + + Input keys = player.input.keyPresses; + boolean jumpPressed = keys.jump() && !jumpWasDown; + jumpWasDown = keys.jump(); + boolean pilot = seat == 0 && vehicle.getControllingPassenger() == player; + + if (pilot) { + AircraftEntity.ControlInput input = vehicle.input(); + input.throttleUp = keys.forward(); + input.throttleDown = keys.backward(); + input.left = keys.left(); + input.right = keys.right(); + input.freeLook = isFreeLook(vehicle); + input.brake = vehicle instanceof TankEntity && keys.jump(); + if (input.brake) { + // The hand brake also stops pivot turns once the vehicle has come to rest. + input.throttleUp = false; + input.throttleDown = false; + double dx = vehicle.getX() - vehicle.xo; + double dz = vehicle.getZ() - vehicle.zo; + if (vehicle.visibleThrottle() <= 0.03F && dx * dx + dz * dz < 0.01) { + input.left = false; + input.right = false; + } + } + if (jumpPressed && vehicle instanceof HelicopterEntity heli) { + heli.toggleHovering(); + } + ModNetwork.sendToServer(new PilotStatePacket(vehicle.getId(), vehicle.visibleThrottle(), vehicle.reverse(), + vehicle.roll(), vehicle.isHoveringMode(), input.write())); + } + + // Eject with a double tap, except for helicopter pilots whose jump key toggles hover. + if (jumpPressed && vehicle.canEject(player) && !(pilot && vehicle instanceof HelicopterEntity)) { + if (ticks - lastJumpPress <= EJECT_WINDOW) { + send(vehicle, CrewActionPacket.Action.EJECT); + lastJumpPress = -100; + } else { + lastJumpPress = ticks; + } + } + + while (KeyBindings.SWITCH_SEAT.consumeClick()) { + send(vehicle, CrewActionPacket.Action.SWITCH_SEAT); + } + while (KeyBindings.DROP_CREW.consumeClick()) { + send(vehicle, pilot ? CrewActionPacket.Action.DROP_CREW : CrewActionPacket.Action.JUMP_OUT); + } + while (KeyBindings.SWITCH_CAMERA.consumeClick()) { + switchCamera(vehicle, seat); + } + while (KeyBindings.LANDING_GEAR.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.LANDING_GEAR); + } + } + while (KeyBindings.FOLD.consumeClick()) { + if (looksThroughSight(vehicle, seat)) { + VehicleCamera.onZoomKey(player, vehicle); + } else if (pilot) { + send(vehicle, CrewActionPacket.Action.FOLD); + } + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + // As in the original, the key works the search light in vehicles that have one. + if (vehicle.canSwitchSearchLight(player)) { + send(vehicle, CrewActionPacket.Action.SEARCH_LIGHT); + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 1.0F); + } else { + VehicleCamera.onCameraModeKey(player, vehicle); + } + } + while (KeyBindings.VEHICLE_SCREEN.consumeClick()) { + if (!vehicle.isDestroyed()) { + send(vehicle, CrewActionPacket.Action.VEHICLE_SCREEN); + } + } + while (KeyBindings.VTOL.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.VTOL); + } + } + while (KeyBindings.RACK_LOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_LOAD); + } + } + while (KeyBindings.RACK_UNLOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_UNLOAD); + } + } + } + + /** + * Whether this seat sees through a sight that can zoom: a camera position or gunner sight, the + * pilot in gunner mode, or the pilot of a UAV. The zoom key folds the wings everywhere else. + */ + private static boolean looksThroughSight(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount() || def.cameraZoom <= 1) { + return false; + } + return eyePosition(vehicle, def, seat) != null || vehicle.isGunnerMode() || def.uav; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + /** Pilot cycles the camera positions; gunners that may leave their sight toggle it. */ + private static void switchCamera(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount()) { + return; + } + SeatDefinition s = def.seats.get(seat); + if (seat == 0) { + switchPilotView(vehicle, def); + } else if (s.gunner() && s.switchGunner()) { + gunnerSight = !gunnerSight; + } + } + + /** + * The pilot's switch mode key, after {@code KeySwitchMode}: leave gunner mode, otherwise step + * through the extra camera positions, otherwise enter gunner mode where the vehicle allows it, + * otherwise go to the first extra camera. Unlike the original, leaving gunner mode never + * depends on the plane still meeting the conditions for entering it. + */ + private static void switchPilotView(AircraftEntity vehicle, AircraftDefinition def) { + int cameras = def.cameraPositions.size(); + if (vehicle.isGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + cameraIndex = 0; + } else if (cameraIndex > 0) { + cameraIndex = (cameraIndex + 1) % cameras; + } else if (vehicle.canSwitchGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + } else if (cameras >= 2) { + cameraIndex = 1; + } + } + + /** + * Free look for the pilot. Helicopters and planes look around while the key is held; tanks + * start from their {@code DefaultFreelook} setting and the key swaps it while held. + */ + private static boolean isFreeLook(AircraftEntity vehicle) { + boolean held = KeyBindings.FREE_LOOK.isDown(); + if (vehicle instanceof TankEntity) { + AircraftDefinition def = vehicle.definition(); + return def != null && def.defaultFreelook != held; + } + return held; + } + + static void onCameraAngles(CameraAngles event) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null || mc.isPaused()) { + return; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return; + } + float partialTicks = event.partialTick(); + boolean firstPerson = mc.options.getCameraType().isFirstPerson(); + + if (seat == 0 && vehicle.steersWithMouse() && !vehicle.isGunnerMode() && vehicle.getControllingPassenger() == player) { + steerWithMouse(vehicle, player, event, partialTicks, firstPerson); + } else { + tracking = false; + constrainView(vehicle, def, def.seats.get(seat), seat, player); + } + + TvMissileEntity missile = tvCamera(vehicle, seat); + if (!(firstPerson && missile != null)) { + // Every seat of every kind tilts with the hull, measured along the way the rider looks. + event.setRoll(AircraftRotation.cameraRoll(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), + vehicle.roll(partialTicks), event.yaw(), event.pitch())); + } + if (firstPerson && missile != null) { + placeCamera(event.camera(), missile.getPosition(partialTicks)); + } else if (firstPerson) { + Vector3 eye = eyePosition(vehicle, def, seat); + moveCamera(event.camera(), vehicle, def, eye != null ? eye : seatEye(vehicle, def, seat, player), seat, partialTicks); + } + } + + /** + * The normal eye, in hull space. Vanilla puts it straight above the rider in world space, + * which leaves the head outside the cockpit once the hull banks or pitches; measured along the + * hull's own up axis it stays where the rider model's head is drawn. + */ + private static Vector3 seatEye(AircraftEntity vehicle, AircraftDefinition def, int seat, LocalPlayer player) { + Vector3 pos = def.seats.get(seat).position(); + double up = player.getEyeHeight() - player.getVehicleAttachmentPoint(vehicle).y; + return new Vector3(pos.x(), pos.y() + up, pos.z()); + } + + /** The phase one pilot model: mouse movement becomes aircraft rotation, the view stays on the nose. */ + private static void steerWithMouse(AircraftEntity vehicle, LocalPlayer player, CameraAngles event, + float partialTicks, boolean firstPerson) { + boolean freeLook = KeyBindings.FREE_LOOK.isDown(); + boolean ownPitch = vehicle.isHovering(); + if (!freeLook) { + if (tracking) { + float dYaw = Mth.wrapDegrees(player.getYRot() - lastYaw); + float dPitch = player.getXRot() - lastPitch; + vehicle.applyPilotLook(dYaw, dPitch); + HudRenderer.onPilotLook(dYaw, dPitch); + } + lockView(player, vehicle.getYRot(), vehicle.yRotO, ownPitch ? null : vehicle.getXRot(), vehicle.xRotO); + lastYaw = player.getYRot(); + lastPitch = player.getXRot(); + tracking = true; + } else { + // Re-acquire the nose smoothly once free look is released. + tracking = false; + } + if (firstPerson && !freeLook) { + event.setYaw(vehicle.getYRot(partialTicks)); + if (!ownPitch) { + event.setPitch(vehicle.getXRot(partialTicks)); + } + } + } + + /** Seat limits for everyone who does not fly with the mouse. */ + private static void constrainView(AircraftEntity vehicle, AircraftDefinition def, SeatDefinition seat, int index, LocalPlayer player) { + boolean freeLookHeld = KeyBindings.FREE_LOOK.isDown(); + if (seat.fixedRotation() && !freeLookHeld) { + lockView(player, vehicle.getYRot() + seat.fixedYaw(), vehicle.yRotO + seat.fixedYaw(), + vehicle.getXRot() + seat.fixedPitch(), vehicle.xRotO + seat.fixedPitch()); + return; + } + boolean pilot = index == 0; + if (pilot && vehicle instanceof TankEntity && !isFreeLook(vehicle)) { + lockView(player, vehicle.getYRot(), vehicle.yRotO, vehicle.getXRot(), vehicle.xRotO); + return; + } + float min; + float max; + if (pilot && (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity)) { + min = def.minRotationPitch; + max = def.maxRotationPitch; + } else if (seat.gunner() || (pilot && vehicle.isGunnerMode())) { + min = seat.minPitch(); + max = seat.maxPitch(); + } else { + return; + } + // Limits are relative to the hull's tilt along the view direction. + double rel = Math.toRadians(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot())); + float tilt = (float) (vehicle.getXRot() * Math.cos(rel) + vehicle.roll() * Math.sin(rel)); + float pitch = Mth.clamp(player.getXRot(), tilt + min, tilt + max); + pitch = Mth.clamp(pitch, -90.0F, 90.0F); + if (pitch != player.getXRot()) { + player.setXRot(pitch); + player.xRotO = pitch; + } + } + + private static void lockView(LocalPlayer player, float yaw, float yawO, @Nullable Float pitch, float pitchO) { + player.setYRot(yaw); + player.yRotO = yawO; + player.setYHeadRot(yaw); + player.setYBodyRot(yaw); + if (pitch != null) { + player.setXRot(pitch); + player.xRotO = pitchO; + } + } + + /** + * Where the first person eye goes, in hull space, or {@code null} for the normal eye. Mirrors + * the original's choice between the pilot's camera positions and a gunner seat's sight. + */ + private static @Nullable Vector3 eyePosition(AircraftEntity vehicle, AircraftDefinition def, int seat) { + SeatDefinition s = def.seats.get(seat); + boolean sight = s.gunner() && (!s.switchGunner() || gunnerSight); + CameraPosition camera = null; + if (seat == 0) { + if (cameraIndex > 0 && cameraIndex < def.cameraPositions.size()) { + camera = def.cameraPositions.get(cameraIndex); + } else if (def.uav || def.alwaysCameraView || sight || vehicle.isGunnerMode() || vehicle instanceof GroundVehicleEntity) { + camera = def.cameraPositions.getFirst(); + } + } else if (sight) { + camera = s.camera() != null && s.camera() != CameraPosition.DEFAULT ? s.camera() : def.cameraPositions.getFirst(); + } + // A camera at the origin means the file never set one; the normal eye is better there. + if (camera == null || camera.position().equals(Vector3.ZERO)) { + return null; + } + return camera.position(); + } + + private static void moveCamera(Camera camera, AircraftEntity vehicle, AircraftDefinition def, Vector3 eye, int seat, float partialTicks) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + SeatDefinition s = def.seats.get(seat); + Vector3 local = s.rotatingSeat() ? vehicle.onTurret(def, eye, partialTicks) : eye; + Vector3f offset = AircraftRotation.toWorld(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), vehicle.roll(partialTicks), + local.x(), local.y(), local.z()); + placeCamera(camera, new Vec3(Mth.lerp(partialTicks, vehicle.xo, vehicle.getX()) + offset.x, + Mth.lerp(partialTicks, vehicle.yo, vehicle.getY()) + offset.y, + Mth.lerp(partialTicks, vehicle.zo, vehicle.getZ()) + offset.z)); + } + + /** + * The TV missile this seat is flying, whose nose camera replaces the eye. Like the original, + * only while the seat has a TV missile selected in its guided mode. + */ + static @Nullable TvMissileEntity tvCamera(AircraftEntity vehicle, int seat) { + TvMissileEntity missile = vehicle.tvMissile(); + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + if (missile == null || set == null || set.definition().type != WeaponType.TV_MISSILE || set.mode() != 0) { + return null; + } + return missile; + } + + static void placeCamera(Camera camera, Vec3 at) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + try { + setter.invoke(camera, at); + } catch (Throwable t) { + MCHeli.LOGGER.warn("Could not move the camera; the first person eye stays where vanilla puts it", t); + setCameraPosition = null; + cameraLookupFailed = true; + } + } + + private static Method accessible(Method method) { + method.setAccessible(true); + return method; + } + + /** {@code Camera.setPosition} is protected. Looked up once; on failure the feature turns itself off. */ + private static @Nullable MethodHandle cameraSetter() { + if (setCameraPosition == null && !cameraLookupFailed) { + try { + setCameraPosition = MethodHandles.lookup().unreflect( + accessible(Camera.class.getDeclaredMethod("setPosition", Vec3.class))); + } catch (Exception e) { + MCHeli.LOGGER.warn("Camera.setPosition is not accessible; the first person eye stays where vanilla puts it", e); + cameraLookupFailed = true; + } + } + return setCameraPosition; + } +} From dc4d0345446c549d949d0b2fe25df4ee6a8e0df7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:52 +0800 Subject: [PATCH 0189/1461] Add client/GltdController.java to the 26.2 shared sources --- .../mcheli/client/GltdController.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/GltdController.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/GltdController.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/GltdController.java new file mode 100644 index 00000000..7d2f31db --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/GltdController.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.network.EquipmentActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +/** + * The local player at a GLTD, {@code MCH_ClientGLTDTickHandler} and {@code MCH_GuiGLTD}: the + * view moves to the sight, the zoom and camera mode keys work as in a vehicle's sight, the use + * key calls the A-10 on the spot under the cross, and a small readout says when it can be called + * again. + */ +final class GltdController { + /** The original zoomed smoothly up to ten; the vehicle sights double each press. */ + private static final int MAX_ZOOM = 8; + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int CROSS = 0xC0FF4040; + + /** Edge of the use key, so holding it calls one strike rather than one per tick. */ + private static boolean useWasDown; + + private GltdController() { + } + + static @Nullable GltdEntity gltdOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof GltdEntity gltd ? gltd : null; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + GltdEntity gltd = gltdOf(player); + if (gltd == null) { + useWasDown = false; + return; + } + while (KeyBindings.FOLD.consumeClick()) { + VehicleCamera.zoomStep(player, MAX_ZOOM); + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + VehicleCamera.nextMode(player, on -> send(gltd, + on ? EquipmentActionPacket.Action.NIGHT_VISION_ON : EquipmentActionPacket.Action.NIGHT_VISION_OFF)); + } + boolean useDown = mc.gui.screen() == null && mc.options.keyUse.isDown(); + if (useDown && !useWasDown) { + send(gltd, EquipmentActionPacket.Action.CALL_STRIKE); + } + useWasDown = useDown; + } + + /** The use key belongs to the sight while sitting at a GLTD. */ + static boolean onUseItem() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + /** First person looks out of the sight rather than from behind the tripod. */ + static void onCameraAngles(CameraAngles angles) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd != null && mc.options.getCameraType().isFirstPerson()) { + CrewController.placeCamera(angles.camera(), gltd.sightPosition(angles.partialTick())); + } + } + + static boolean hidesHand() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + static void drawHud(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd == null || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int cx = g.guiWidth() / 2; + int cy = g.guiHeight() / 2; + g.fill(cx - 12, cy, cx - 3, cy + 1, CROSS); + g.fill(cx + 4, cy, cx + 13, cy + 1, CROSS); + g.fill(cx, cy - 12, cx + 1, cy - 3, CROSS); + g.fill(cx, cy + 4, cx + 1, cy + 13, CROSS); + + Font font = mc.font; + int x = cx + 20; + int y = cy + 20; + int wait = gltd.waitTicks(); + if (wait > 0) { + g.text(font, Component.translatable("hud.mcheli.gltd.wait", (wait + 19) / 20), x, y, AMBER); + } else { + g.text(font, Component.translatable("hud.mcheli.gltd.ready"), x, y, WHITE); + } + g.text(font, Component.translatable("hud.mcheli.zoom", Math.round(VehicleCamera.zoom())), x, y + 10, WHITE); + } + + private static void send(GltdEntity gltd, EquipmentActionPacket.Action action) { + ModNetwork.sendToServer(new EquipmentActionPacket(gltd.getId(), action)); + } +} From 07de60d6ae64b8ea021a8d6f85980da99f819bf8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:53 +0800 Subject: [PATCH 0190/1461] Add client/KeyBindings.java to the 26.2 shared sources --- .../mcheli/client/KeyBindings.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java new file mode 100644 index 00000000..111759ac --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.KeyMapping; +import net.minecraft.resources.Identifier; + +import java.util.List; + +/** + * Mod key bindings. Throttle, steering, hover, brake and eject reuse the vanilla movement and jump + * keys (W/S, A/D and Space by default), which matches the original defaults. Firing uses the + * vanilla use key and the pick block button switches weapons, the right and middle mouse buttons + * of the original. The rest keep the original's letters where vanilla leaves them free. + */ +public final class KeyBindings { + public static final KeyMapping.Category CATEGORY = KeyMapping.Category.register(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "main")); + + /** Hold to look around without steering. The original used Left Ctrl, which now clashes with sprint. */ + public static final KeyMapping FREE_LOOK = new KeyMapping("key.mcheli.free_look", InputConstants.KEY_LALT, CATEGORY); + /** Cycles the pilot's camera positions, or toggles the gunner sight in seats that allow it. */ + public static final KeyMapping SWITCH_CAMERA = new KeyMapping("key.mcheli.switch_camera", InputConstants.KEY_H, CATEGORY); + public static final KeyMapping LANDING_GEAR = new KeyMapping("key.mcheli.landing_gear", InputConstants.KEY_B, CATEGORY); + /** Zooms through a sight, as the original Z key did; otherwise folds hatches and wings, sweeps variable geometry wings. */ + public static final KeyMapping FOLD = new KeyMapping("key.mcheli.fold", InputConstants.KEY_Z, CATEGORY); + /** The original used F, which vanilla now uses to swap hands. */ + public static final KeyMapping VTOL = new KeyMapping("key.mcheli.vtol", InputConstants.KEY_V, CATEGORY); + public static final KeyMapping SWITCH_SEAT = new KeyMapping("key.mcheli.switch_seat", InputConstants.KEY_R, CATEGORY); + public static final KeyMapping RACK_LOAD = new KeyMapping("key.mcheli.rack_load", InputConstants.KEY_J, CATEGORY); + public static final KeyMapping RACK_UNLOAD = new KeyMapping("key.mcheli.rack_unload", InputConstants.KEY_U, CATEGORY); + /** The second weapon switch key of the original; the first is the vanilla pick block button. */ + public static final KeyMapping SWITCH_WEAPON = new KeyMapping("key.mcheli.switch_weapon", InputConstants.KEY_G, CATEGORY); + public static final KeyMapping WEAPON_MODE = new KeyMapping("key.mcheli.weapon_mode", InputConstants.KEY_X, CATEGORY); + /** The original used V, which VTOL took over. */ + public static final KeyMapping FLARE = new KeyMapping("key.mcheli.flare", InputConstants.KEY_C, CATEGORY); + /** Fuel and parachute slots, reloading and the cargo chest. The original used R, which switches seats here. */ + public static final KeyMapping VEHICLE_SCREEN = new KeyMapping("key.mcheli.vehicle_screen", InputConstants.KEY_I, CATEGORY); + /** Night vision, thermal view and back in vehicles with {@code EnableNightVision}. The original used C, now the flare key. */ + public static final KeyMapping CAMERA_MODE = new KeyMapping("key.mcheli.camera_mode", InputConstants.KEY_K, CATEGORY); + /** The pilot lets the troops down; anyone else aboard slides down a rope or jumps. The original used Y for the pilot's part. */ + public static final KeyMapping DROP_CREW = new KeyMapping("key.mcheli.drop_crew", InputConstants.KEY_Y, CATEGORY); + /** Operators open the team manager, as the original's multiplayer manager key M did. */ + public static final KeyMapping TEAM_MANAGER = new KeyMapping("key.mcheli.team_manager", InputConstants.KEY_M, CATEGORY); + + public static final List ALL = List.of(FREE_LOOK, SWITCH_CAMERA, LANDING_GEAR, FOLD, VTOL, SWITCH_SEAT, RACK_LOAD, RACK_UNLOAD, + SWITCH_WEAPON, WEAPON_MODE, FLARE, VEHICLE_SCREEN, CAMERA_MODE, DROP_CREW, TEAM_MANAGER); + + private KeyBindings() { + } +} From e81cf1a998da2f24034fb21e9bc27ed775cc0a22 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:53 +0800 Subject: [PATCH 0191/1461] Add client/LauncherController.java to the 26.2 shared sources --- .../mcheli/client/LauncherController.java | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java new file mode 100644 index 00000000..df6c23fc --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java @@ -0,0 +1,238 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.network.LauncherPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The Stinger and Javelin in the player's hands, {@code MCH_ClientLightWeaponTickHandler} and + * {@code MCH_GuiLightWeapon}. Raised, the launcher's sight covers the view and its guidance locks + * onto what the cross holds; the attack key fires once the lock is complete. Z steps the zoom, X + * the Javelin's direct and top attack modes. An empty launcher held down reloads itself from a + * missile in the inventory. + */ +final class LauncherController { + /** Ticks of raising before the sight and the lock start, as in the original. */ + private static final int RAISE_TICKS = 10; + private static final int RELOAD_TICKS = 40; + /** Top attack climbs first and needs this much room, measured flat. */ + private static final double TOP_ATTACK_MIN_DISTANCE = 40.0; + private static final int GREEN = 0xFF2AD848; + private static final int RED = 0xFFDF0808; + private static final int WHITE = 0xFFFFFFFF; + + private static @Nullable String weapon; + private static @Nullable Guidance guidance; + private static int zoomIndex; + private static int mode; + private static int reloadTicks; + private static int lockSoundTicks; + private static boolean attackWasDown; + + private LauncherController() { + } + + private static ItemStack launcher(@Nullable LocalPlayer player) { + if (player == null || !LauncherItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getItemInHand(InteractionHand.MAIN_HAND); + return stack.getItem() instanceof LauncherItem ? stack : ItemStack.EMPTY; + } + + /** Whether the player holds a launcher up to the eye. */ + static boolean isRaised() { + LocalPlayer player = Minecraft.getInstance().player; + ItemStack stack = launcher(player); + return !stack.isEmpty() && player.isUsingItem() && player.getUseItem() == stack && player.getTicksUsingItem() > RAISE_TICKS; + } + + private static @Nullable WeaponDefinition definition(ItemStack stack) { + return MCHeli.content().weapon(((LauncherItem) stack.getItem()).weapon()); + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = launcher(player); + WeaponDefinition def = stack.isEmpty() ? null : definition(stack); + if (def == null) { + weapon = null; + guidance = null; + reloadTicks = 0; + attackWasDown = false; + return; + } + String name = ((LauncherItem) stack.getItem()).weapon(); + if (!name.equals(weapon) || guidance == null) { + weapon = name; + boolean air = def.type != WeaponType.AT_MISSILE; + guidance = new Guidance(air, !air, def.ridableOnly, def.lockTime, e -> e != player && !e.isAlliedTo(player)); + zoomIndex = 0; + mode = 0; + } + boolean raised = isRaised(); + boolean attackDown = raised && mc.gui.screen() == null && mc.options.keyAttack.isDown(); + boolean attackPressed = attackDown && !attackWasDown; + attackWasDown = attackDown; + + if (LauncherItem.isLoaded(stack)) { + reloadTicks = 0; + if (raised) { + if (guidance.lock(player, player.getEyePosition())) { + lockSoundTicks = 0; + } + playLockSound(player); + } else { + guidance.clear(); + } + } else { + guidance.clear(); + reload(player, stack); + } + guidance.tick(); + + if (!raised) { + return; + } + while (KeyBindings.FOLD.consumeClick()) { + if (def.zoom.length > 1) { + zoomIndex = (zoomIndex + 1) % def.zoom.length; + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + if (def.modeNum > 1) { + mode = (mode + 1) % def.modeNum; + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 0.9F); + } + } + if (attackPressed) { + Entity target = guidance.lastLocked(); + if (LauncherItem.isLoaded(stack) && guidance.isLocked() && target != null && canFire(player, target)) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.FIRE, target.getId(), mode)); + guidance.clear(); + } else { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + } + + /** Top attack needs room to climb and dive. */ + private static boolean canFire(LocalPlayer player, @Nullable Entity target) { + if (mode == 0 || target == null) { + return true; + } + double dx = target.getX() - player.getX(); + double dz = target.getZ() - player.getZ(); + return Math.sqrt(dx * dx + dz * dz) >= TOP_ATTACK_MIN_DISTANCE; + } + + /** The tone quickens as the lock builds. */ + private static void playLockSound(LocalPlayer player) { + if (guidance == null || guidance.lockCount() <= 0) { + lockSoundTicks = 0; + return; + } + if (lockSoundTicks > 0) { + lockSoundTicks--; + return; + } + lockSoundTicks = Math.max(2, (int) (7 * (1.0 - (double) guidance.lockCount() / guidance.lockCountMax()))); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + + /** Held down and empty, with a missile to hand: two seconds to load it. */ + private static void reload(LocalPlayer player, ItemStack stack) { + LauncherItem item = (LauncherItem) stack.getItem(); + boolean hasMissile = player.getAbilities().instabuild || player.getInventory().findSlotMatchingItem(new ItemStack(item.missile())) >= 0; + if (!hasMissile || player.isUsingItem()) { + reloadTicks = 0; + return; + } + reloadTicks++; + if (reloadTicks == 10) { + player.level().playLocalSound(player, ModSounds.get("fim92_reload"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (reloadTicks == RELOAD_TICKS) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.RELOAD, 0, 0)); + } + } + + static float fov(float fov) { + if (!isRaised()) { + return fov; + } + WeaponDefinition def = definition(launcher(Minecraft.getInstance().player)); + if (def == null || def.zoom.length == 0) { + return fov; + } + float zoom = def.zoom[Math.min(zoomIndex, def.zoom.length - 1)]; + return zoom > 1.0F ? fov / zoom : fov; + } + + static void drawHud(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (!mc.options.getCameraType().isFirstPerson()) { + return; + } + ItemStack stack = launcher(player); + if (stack.isEmpty()) { + return; + } + Font font = mc.font; + if (!isRaised()) { + if (!LauncherItem.isLoaded(stack) && reloadTicks > 0) { + g.text(font, Component.translatable("hud.mcheli.launcher.reloading"), g.guiWidth() / 2 - 30, g.guiHeight() / 2 + 20, WHITE); + } + return; + } + WeaponDefinition def = definition(stack); + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + String texture = def != null && def.type == WeaponType.AT_MISSILE ? "javelin" : "stinger"; + g.blit(RenderPipelines.GUI_TEXTURED, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + texture + ".png"), + (w - size) / 2, (h - size) / 2, 0.0F, 0.0F, size, size, 256, 256, 256, 256); + + int x = w / 2 + 20; + int y = h / 2 + 20; + if (!LauncherItem.isLoaded(stack)) { + g.text(font, Component.translatable("hud.mcheli.launcher.empty"), x, y, RED); + } else if (guidance != null && guidance.isLocked()) { + Entity target = guidance.lastLocked(); + g.text(font, Component.translatable("hud.mcheli.locked"), x, y, canFire(player, target) ? GREEN : RED); + if (target != null) { + g.text(font, String.format("[%.0f m]", target.distanceTo(player)), x, y + 10, GREEN); + } + } else if (guidance != null && guidance.lockCount() > 0) { + g.text(font, Component.translatable("hud.mcheli.locking", guidance.lockCount() * 100 / Math.max(guidance.lockCountMax(), 1)), x, y, WHITE); + } + if (def != null && def.modeNum > 1) { + g.text(font, Component.translatable(mode == 0 ? "hud.mcheli.launcher.direct" : "hud.mcheli.launcher.top_attack"), x, y + 20, WHITE); + } + if (def != null && def.zoom.length > 0) { + g.text(font, Component.translatable("hud.mcheli.zoom", String.format("%.1f", def.zoom[Math.min(zoomIndex, def.zoom.length - 1)])), + x, y + 30, WHITE); + } + } +} From ad610de850c9597cb2596c1fa6ef13ff1d4f4ff8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:54 +0800 Subject: [PATCH 0192/1461] Add client/RangefinderController.java to the 26.2 shared sources --- .../mcheli/client/RangefinderController.java | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java new file mode 100644 index 00000000..80aafc99 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java @@ -0,0 +1,155 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.SpotPacket; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.Locale; + +/** + * Looking through the rangefinder, {@code MCH_ClientToolTickHandler} and {@code MCH_GuiRangeFinder}. + * Once it has been held up for a moment the scope covers the view and reads the range; the zoom + * key and the weapon mode key zoom in and out while held, the flare key picks what to spot, and + * the attack key spots. + */ +final class RangefinderController { + private static final Identifier SCOPE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/rangefinder.png"); + /** Ticks of holding it up before the scope shows, as in the original. */ + private static final int RAISE_TICKS = 8; + private static final int COOLDOWN = 60; + private static final float MIN_ZOOM = 1.2F; + private static final float MAX_ZOOM = 10.0F; + private static final double RANGE = 300.0; + private static final int GREEN = 0xFF10FF20; + private static final int RED = 0xFFFF1010; + private static final int WHITE = 0xFFFFFFFF; + private static final int GREY = 0xFF404040; + + private static float zoom = 2.0F; + private static SpotPacket.Mode mode = SpotPacket.Mode.PLAYERS_AND_VEHICLES; + private static int cooldown; + + private RangefinderController() { + } + + /** The rangefinder the player is looking through, or empty. */ + private static ItemStack scope(LocalPlayer player) { + if (player == null || !player.isUsingItem() || player.getTicksUsingItem() <= RAISE_TICKS || !RangefinderItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getUseItem(); + return stack.getItem() instanceof RangefinderItem ? stack : ItemStack.EMPTY; + } + + static boolean isScoped() { + return !scope(Minecraft.getInstance().player).isEmpty(); + } + + static void onClientTick() { + if (cooldown > 0) { + cooldown--; + } + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty()) { + zoom = 2.0F; + return; + } + if (KeyBindings.FOLD.isDown() && zoom < MAX_ZOOM) { + zoom = Math.min(zoom + zoom / 10.0F, MAX_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 1.0F); + } + if (KeyBindings.WEAPON_MODE.isDown() && zoom > MIN_ZOOM) { + zoom = Math.max(zoom - zoom / 10.0F, MIN_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 0.9F); + } + while (KeyBindings.FLARE.consumeClick()) { + mode = SpotPacket.Mode.values()[(mode.ordinal() + 1) % SpotPacket.Mode.values().length]; + // Alone in a world there is no one else to spot, as the original reasoned. + boolean alone = mc.hasSingleplayerServer() && !mc.getSingleplayerServer().isPublished(); + if (alone && mode == SpotPacket.Mode.PLAYERS_AND_VEHICLES) { + mode = SpotPacket.Mode.MOBS; + } + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 0.9F); + } + if (mc.options.keyAttack.isDown() && cooldown == 0) { + if (mode != SpotPacket.Mode.MARK_POINT && RangefinderItem.charges(stack) <= 0) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 1.0F, 1.0F); + } else { + cooldown = COOLDOWN; + ModNetwork.sendToServer(new SpotPacket(mode)); + } + } + } + + static float fov(float fov) { + return isScoped() ? fov / zoom : fov; + } + + static void drawHud(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty() || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + g.blit(RenderPipelines.GUI_TEXTURED, SCOPE, (w - size) / 2, (h - size) / 2, 0.0F, 0.0F, size, size, 256, 256, 256, 256); + + // The readouts sit on the scope texture's own labels: lay them out in its 512 wide space, + // centred, as the original did, and scale that with the scope. + float k = size / 512.0F; + g.pose().pushMatrix(); + g.pose().translate(w / 2.0F, h / 2.0F); + g.pose().scale(k, k); + drawReadouts(g, mc.font, player, stack); + g.pose().popMatrix(); + } + + /** In scope space: the centre of the view is at 0, 0. */ + private static void drawReadouts(GuiGraphicsExtractor g, Font font, LocalPlayer player, ItemStack stack) { + int charge = RangefinderItem.charges(stack) * 100 / RangefinderItem.CHARGES; + // Over BATT. + g.text(font, String.format("%3d%%", charge), -100, 55, charge > 0 ? GREEN : RED); + if (charge <= 0) { + g.text(font, Component.translatable("hud.mcheli.rangefinder.empty"), -40, 55, RED); + } else { + // Over RANGE METERS. + Vec3 eye = player.getEyePosition(); + HitResult hit = player.level().clip(new ClipContext(eye, eye.add(player.getLookAngle().scale(RANGE)), + ClipContext.Block.OUTLINE, ClipContext.Fluid.ANY, player)); + String range = hit.getType() == HitResult.Type.MISS ? "----" : String.format("%4d", Math.round(hit.getLocation().distanceTo(eye))); + g.text(font, range, 40, 55, hit.getType() == HitResult.Type.MISS ? RED : GREEN); + } + // The spot cooldown bar, and the zoom above it. + g.fill(-100, 51, -70, 53, GREEN); + g.fill(-100, 51, -100 + cooldown / 2, 53, RED); + g.text(font, String.format("x%.1f", zoom), -100, 31, WHITE); + int y = 21; + for (SpotPacket.Mode m : SpotPacket.Mode.values()) { + if (m == mode) { + g.text(font, ">", 30, y, WHITE); + } + g.text(font, Component.translatable("hud.mcheli.rangefinder." + m.name().toLowerCase(Locale.ROOT)), 40, y, m == mode ? WHITE : GREY); + y += 10; + } + } +} From c3d8df75a820bb70b208ad2dc69882125f1f77ce Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:54 +0800 Subject: [PATCH 0193/1461] Add client/RuntimeAssets.java to the 26.2 shared sources --- .../mcheli/client/RuntimeAssets.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java new file mode 100644 index 00000000..7f7afd84 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java @@ -0,0 +1,165 @@ +package com.jnhprojects.mcheli.client; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Vehicle textures, models and sounds that can change while the game runs, without a resource + * reload (F3 + T), which would stall the game for every pack loaded and is not available on a + * server at all. + * + *

Lookups try the resource manager first, so resource packs keep the last word, then the + * runtime roots: folders laid out like {@code assets/mcheli} that content can add at any time. + * Content packs and, later, the workshop register their folders here. + *

    + *
  • Textures found only in a runtime root are uploaded as a {@link DynamicTexture} the first + * time a vehicle draws them. {@link #refreshTextures} uploads every vehicle texture again + * from disk, which is how a content reload shows edited textures.
  • + *
  • Models are read through {@link #open} by the mesh cache, which a reload clears.
  • + *
  • Sounds found only in a runtime root are streamed from the file + * ({@link #soundFile}); see {@code RuntimeSoundInstance}.
  • + *
+ */ +public final class RuntimeAssets { + private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeAssets.class); + + /** Searched in order, so the pack loaded last comes first. Replaced as a whole. */ + private static volatile List roots = List.of(); + /** The pack open in the workshop, searched first whether the game has loaded it or not. */ + private static volatile @Nullable Path workshopRoot; + /** Every vehicle texture asked for, and whether we uploaded it ourselves. */ + private static final Map TEXTURES = new ConcurrentHashMap<>(); + + private RuntimeAssets() { + } + + /** + * Sets the folders laid out like {@code assets/mcheli} that are searched after the resource + * packs, the first one first. Content packs set theirs whenever the packs in use change. + */ + public static void setRoots(List folders) { + roots = List.copyOf(folders); + } + + /** Sets the {@code assets/mcheli} folder of the pack open in the workshop, or {@code null} for none. */ + public static void setWorkshopRoot(@Nullable Path folder) { + workshopRoot = folder; + } + + /** Opens an {@code mcheli} asset from the resource packs, or else from a runtime root. */ + public static @Nullable InputStream open(Identifier id) throws IOException { + Optional resource = Minecraft.getInstance().getResourceManager().getResource(id); + if (resource.isPresent()) { + return resource.get().open(); + } + Path file = find(id.getPath()); + return file != null ? Files.newInputStream(file) : null; + } + + /** + * A texture ready to bind. The first request for a texture no resource pack has, but a runtime + * root does, uploads it. Cheap after the first call, so renderers can call it every frame. + */ + public static Identifier texture(Identifier id) { + TEXTURES.computeIfAbsent(id, key -> { + if (Minecraft.getInstance().getResourceManager().getResource(key).isPresent()) { + return false; + } + return upload(key); + }); + return id; + } + + /** + * Uploads every texture vehicles have drawn so far again, read fresh from disk. Resource pack + * textures are replaced too, until the next resource reload loads them the normal way. One that + * no longer exists anywhere, because its pack is gone, is released. + */ + public static void refreshTextures() { + for (Map.Entry entry : TEXTURES.entrySet()) { + boolean ours = upload(entry.getKey()); + if (!ours && entry.getValue()) { + Minecraft.getInstance().getTextureManager().release(entry.getKey()); + } + entry.setValue(ours); + } + } + + /** Uploads one texture again, read fresh from disk: the workshop just replaced its file. */ + public static void refreshTexture(Identifier id) { + TEXTURES.put(id, upload(id)); + } + + private static boolean upload(Identifier id) { + try (InputStream in = open(id)) { + if (in == null) { + return false; + } + NativeImage image = NativeImage.read(in); + Minecraft.getInstance().getTextureManager().register(id, new DynamicTexture(id::toString, image)); + return true; + } catch (IOException e) { + LOGGER.warn("Could not load texture {}", id, e); + return false; + } + } + + /** + * Drops the textures we uploaded, so the texture manager loads them again the normal way. + * Called on resource reload. + */ + public static void clearTextures() { + TEXTURES.forEach((id, ours) -> { + if (ours) { + Minecraft.getInstance().getTextureManager().release(id); + } + }); + TEXTURES.clear(); + } + + /** + * The OGG file for a sound event of ours that no resource pack defines, such as {@code heli} + * from {@code sounds/heli.ogg} in a runtime root. Numbered variants ({@code heli1.ogg}) count. + */ + public static @Nullable Path soundFile(String event) { + Path file = find("sounds/" + event + ".ogg"); + return file != null ? file : find("sounds/" + event + "1.ogg"); + } + + private static @Nullable Path find(String path) { + Path workshop = workshopRoot; + if (workshop != null) { + Path file = inRoot(workshop, path); + if (file != null) { + return file; + } + } + for (Path root : roots) { + Path file = inRoot(root, path); + if (file != null) { + return file; + } + } + return null; + } + + private static @Nullable Path inRoot(Path root, String path) { + Path file = root.resolve(path).normalize(); + return file.startsWith(root) && Files.isRegularFile(file) ? file : null; + } +} From 61f7e13b0f59faf34cd08e95517fd38138a608bb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:55 +0800 Subject: [PATCH 0194/1461] Add client/TargetMarkers.java to the 26.2 shared sources --- .../mcheli/client/TargetMarkers.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java new file mode 100644 index 00000000..19a387a2 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java @@ -0,0 +1,87 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3fc; +import org.jspecify.annotations.Nullable; + +import java.util.Map; + +/** + * {@code MCH_GuiTargetMarker}: the team's spotted targets and marked point on screen. A target + * gets a triangle over its head with its distance; one off screen gets a diamond at the edge on + * its side. The marked point gets a diamond with its distance and coordinates. + */ +final class TargetMarkers { + private static final int TARGET = 0xFFFF4040; + private static final int POINT = 0xFFFFD000; + private static final int EDGE = 12; + + private TargetMarkers() { + } + + static void tick() { + Markers.tick(); + } + + static void draw(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || mc.player == null) { + return; + } + float partialTicks = delta.getGameTimeDeltaPartialTick(false); + for (Map.Entry spotted : Markers.spotted().entrySet()) { + Entity entity = mc.level.getEntity(spotted.getKey()); + if (entity == null || entity == mc.player || !entity.isAlive()) { + continue; + } + Vec3 top = entity.getPosition(partialTicks).add(0.0, entity.getBbHeight() + 0.5, 0.0); + mark(g, mc, top, "â–¼", TARGET, "%.0fm".formatted(mc.player.distanceTo(entity)), null); + } + BlockPos point = Markers.point(); + if (point != null) { + Vec3 at = Vec3.atCenterOf(point).add(0.0, 1.0, 0.0); + String coordinates = "%d %d %d".formatted(point.getX(), point.getY(), point.getZ()); + mark(g, mc, at, "â—†", POINT, "%.0fm".formatted(Math.sqrt(mc.player.distanceToSqr(at))), coordinates); + } + } + + private static void mark(GuiGraphicsExtractor g, Minecraft mc, Vec3 at, String symbol, int color, String distance, @Nullable String extra) { + Camera camera = mc.gameRenderer.mainCamera(); + Vector3fc forward = camera.forwardVector(); + Vec3 offset = at.subtract(camera.position()); + boolean ahead = offset.x * forward.x() + offset.y * forward.y() + offset.z * forward.z() > 0.0; + Vec3 ndc = mc.gameRenderer.projectPointToScreen(at); + int w = g.guiWidth(); + int h = g.guiHeight(); + double x = (ndc.x + 1.0) / 2.0 * w; + double y = (1.0 - ndc.y) / 2.0 * h; + Font font = mc.font; + if (ahead && x >= 0 && x <= w && y >= 0 && y <= h) { + g.centeredText(font, symbol, (int) x, (int) y - 4, color); + g.centeredText(font, distance, (int) x, (int) y + 6, color); + if (extra != null) { + g.centeredText(font, extra, (int) x, (int) y + 16, color); + } + return; + } + // Behind or off to the side: point the way along the screen edge. + double dx = ahead ? x - w / 2.0 : w / 2.0 - x; + double dy = ahead ? y - h / 2.0 : h / 2.0 - y; + double scale = Math.max(Math.abs(dx) / (w / 2.0 - EDGE), Math.abs(dy) / (h / 2.0 - EDGE)); + if (scale <= 0.0) { + return; + } + int ex = Mth.clamp((int) (w / 2.0 + dx / scale), EDGE, w - EDGE); + int ey = Mth.clamp((int) (h / 2.0 + dy / scale), EDGE, h - EDGE); + g.centeredText(font, "â—†", ex, ey - 4, color); + } +} From 7ee8cf6d61112a422b97f4c5fab5467e37ee371e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:55 +0800 Subject: [PATCH 0195/1461] Drive the thermal view through the single post effect slot of 26.2 --- .../mcheli/client/VehicleCamera.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java new file mode 100644 index 00000000..de7ef3c4 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java @@ -0,0 +1,127 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; + +import java.util.function.Consumer; + +/** + * The local crew member's sight, {@code MCH_Camera}: zoom for vehicles with a {@code CameraZoom} + * above one, and night vision and thermal view for those with {@code EnableNightVision}. Both + * reset whenever the player changes seat or leaves the vehicle. + */ +public final class VehicleCamera { + /** The original's camera modes; the HUD reads the ordinal as {@code cam_mode}. */ + public enum Mode { + NORMAL, + NIGHT_VISION, + /** Night vision seen through a white hot filter, the original's "pencil" shader mode. */ + THERMAL + } + + /** The post effect of the thermal view, {@code assets/mcheli/post_effect/thermal.json}. */ + private static final Identifier THERMAL_EFFECT = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "thermal"); + + private static float zoom = 1.0F; + private static Mode mode = Mode.NORMAL; + + private VehicleCamera() { + } + + public static float zoom() { + return zoom; + } + + public static Mode mode() { + return mode; + } + + /** Back to the plain view, on a seat change or after leaving the vehicle. */ + static void reset() { + zoom = 1.0F; + mode = Mode.NORMAL; + } + + /** + * Keeps the thermal filter in the player's post effects while the thermal view is on. The list + * is the one a server's post effect packet fills, so ours is put back if that replaces it. + */ + static void tick() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + return; + } + boolean active = THERMAL_EFFECT.equals(mc.gameRenderer.currentPostEffect()); + if (mode == Mode.THERMAL && !active) { + ClientHooks.postEffects().load(THERMAL_EFFECT); + } else if (mode != Mode.THERMAL && active) { + mc.gameRenderer.clearPostEffect(); + } + } + + /** + * {@code zoomCamera}: each press doubles the zoom up to the vehicle's {@code CameraZoom}, and + * the press after that goes back to one. The caller checks that the seat looks through a sight. + */ + static void onZoomKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + zoomStep(player, def != null && !vehicle.isDestroyed() ? def.cameraZoom : 1); + } + + /** One press of the zoom key on a sight that zooms up to {@code max}. */ + static void zoomStep(LocalPlayer player, int max) { + if (max <= 1) { + refuse(player); + return; + } + zoom = zoom >= max - 0.01F ? 1.0F : Math.min(zoom * 2.0F, max); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + + /** + * {@code switchCameraMode}: steps from the plain view to night vision, to thermal view and + * back. The server applies night vision as an effect in both of the last two; the thermal + * filter is drawn here. + */ + static void onCameraModeKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + if (def == null || !def.nightVision || vehicle.isDestroyed()) { + refuse(player); + return; + } + nextMode(player, on -> ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), + on ? CrewActionPacket.Action.NIGHT_VISION_ON : CrewActionPacket.Action.NIGHT_VISION_OFF))); + } + + /** + * Steps to the next camera mode on a sight that has night vision. {@code nightVision} asks + * the server to switch the effect on or off, only when that changes. + */ + static void nextMode(LocalPlayer player, Consumer nightVision) { + Mode next = Mode.values()[(mode.ordinal() + 1) % Mode.values().length]; + if (mode == Mode.NORMAL || next == Mode.NORMAL) { + nightVision.accept(next != Mode.NORMAL); + } + mode = next; + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + + /** The view's field of view, narrowed by the zoom. */ + public static float fov(float fov) { + return zoom > 1.0F ? fov / zoom : fov; + } + + /** {@code playSoundNG}: the key does nothing in this seat. */ + private static void refuse(LocalPlayer player) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } +} From 280ed5aedd92b547fd48dbd6c5f164c9f4c80d7c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:56 +0800 Subject: [PATCH 0196/1461] Add client/VehiclePicker.java to the 26.2 shared sources --- .../mcheli/client/VehiclePicker.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java new file mode 100644 index 00000000..9507412f --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.BoardVehiclePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Right-clicking anywhere on a vehicle's hull boards it. + * + *

Vanilla only picks an entity by its main hit box, which for most MCHeli vehicles is the + * default two by 0.7 block box at their feet; the hull's real shape lives in its extra + * {@code BoundingBox} lines. In the original those boxes were clickable too. Inflating the main box + * instead would change how the vehicle collides with terrain, so the use key looks for them here. + */ +final class VehiclePicker { + private VehiclePicker() { + } + + static boolean onUseItem(InteractionHand hand) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || hand != InteractionHand.MAIN_HAND || player.getVehicle() instanceof AircraftEntity) { + return false; + } + // Vanilla already handles a click on the main box. + if (mc.hitResult instanceof EntityHitResult hit && hit.getEntity() instanceof AircraftEntity) { + return false; + } + Hit hit = pick(player, mc.hitResult); + if (hit == null) { + return false; + } + ModNetwork.sendToServer(new BoardVehiclePacket(hit.vehicle.getId(), hit.location)); + return true; + } + + private record Hit(AircraftEntity vehicle, Vec3 location) { + } + + /** The nearest vehicle hull along the view, closer than the block or entity vanilla picked. */ + private static @Nullable Hit pick(Player player, @Nullable HitResult vanilla) { + double range = player.entityInteractionRange(); + Vec3 eye = player.getEyePosition(); + Vec3 to = eye.add(player.getViewVector(1.0F).scale(range)); + double limit = vanilla != null && vanilla.getType() != HitResult.Type.MISS ? eye.distanceToSqr(vanilla.getLocation()) : range * range; + Hit best = null; + for (AircraftEntity vehicle : player.level().getEntitiesOfClass(AircraftEntity.class, new AABB(eye, to).inflate(16.0))) { + Optional at = vehicle.clipHitBoxes(eye, to, 0.2); + vehicle.consumeHitBoxFactor(); + if (at.isPresent() && eye.distanceToSqr(at.get()) < limit) { + limit = eye.distanceToSqr(at.get()); + best = new Hit(vehicle, at.get()); + } + } + return best; + } +} From 142058bca4977a4968bc944f6432d92ca1985bfa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:56 +0800 Subject: [PATCH 0197/1461] Add client/WeaponController.java to the 26.2 shared sources --- .../mcheli/client/WeaponController.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java new file mode 100644 index 00000000..2cfff278 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.FireWeaponPacket; +import com.jnhprojects.mcheli.network.LockWarningPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The crew member's trigger: fires, switches weapons and modes, drops flares and resupplies. + * + *

Like the original, the client fires first. Its copy of the weapon decides whether the shot + * can happen (ready, loaded, locked on, a target in sight) and works out the option values; only + * then is the server asked, which fires the real projectile. + */ +final class WeaponController { + private WeaponController() { + } + + private record Seat(AircraftEntity vehicle, VehicleWeapons weapons, int seat) { + } + + private static @Nullable Seat seat(@Nullable LocalPlayer player) { + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle) || vehicle.definition() == null) { + return null; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (weapons == null || seat < 0 || seat >= vehicle.definition().seatCount()) { + return null; + } + return new Seat(vehicle, weapons, seat); + } + + /** The use key is the trigger while the seat has a weapon, so it must not also use items. */ + static boolean onUseItem() { + Seat s = seat(Minecraft.getInstance().player); + return s != null && s.weapons.selected(s.seat) >= 0; + } + + static boolean onPickBlock() { + Seat s = seat(Minecraft.getInstance().player); + if (s == null || s.weapons.sets().isEmpty()) { + return false; + } + send(s.vehicle, Minecraft.getInstance().player != null && Minecraft.getInstance().player.isShiftKeyDown() + ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + return true; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + Seat s = seat(player); + if (s == null) { + return; + } + while (KeyBindings.SWITCH_WEAPON.consumeClick()) { + send(s.vehicle, player.isShiftKeyDown() ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + WeaponSet set = s.weapons.current(s.seat); + if (set != null && set.switchMode()) { + send(s.vehicle, CrewActionPacket.Action.WEAPON_MODE); + click(); + } + } + while (KeyBindings.FLARE.consumeClick()) { + if (s.seat <= 1 && s.weapons.flares.use()) { + send(s.vehicle, CrewActionPacket.Action.FLARE); + click(); + } + } + + if (mc.gui.screen() == null && mc.options.keyUse.isDown() && !s.vehicle.isDestroyed()) { + fire(s, player); + } + } + + private static void fire(Seat s, LocalPlayer player) { + int id = s.weapons.selected(s.seat); + if (id < 0 || s.weapons.operatorSeat(id) != s.seat) { + return; + } + Shot shot = new Shot(s.vehicle, player); + boolean fired = s.weapons.use(player, s.seat, shot, (set, mount, sh) -> clientShot(s, id, set, sh, player)); + if (fired) { + ModNetwork.sendToServer(new FireWeaponPacket(s.vehicle.getId(), id, shot.option1, shot.option2)); + } + } + + /** The client half of each weapon's {@code shot}: can it fire, and with which options. */ + private static boolean clientShot(Seat s, int id, WeaponSet set, Shot shot, LocalPlayer player) { + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_2, ROCKET, MARKER_ROCKET -> { + shot.option1 = set.mode(); + yield true; + } + case AA_MISSILE, AT_MISSILE -> { + Guidance g = s.weapons.guidance(id); + if (g == null) { + yield false; + } + boolean locked = g.lock(player, viewPoint(player)); + if (g.shouldNotifyTarget() && g.target() != null) { + ModNetwork.sendToServer(new LockWarningPacket(g.target().getId())); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (!locked || g.lastLocked() == null) { + yield false; + } + shot.option1 = g.lastLocked().getId(); + shot.option2 = set.mode(); + yield true; + } + case TV_MISSILE -> { + shot.option1 = set.mode(); + shot.option2 = 0; + yield true; + } + case AS_MISSILE -> landAhead(s.vehicle, player, 1.62, 200.0, false) != null; + case CAS -> landAhead(s.vehicle, player, 2.0, 80.0, false) != null; + case TORPEDO -> !w.guidedTorpedo || landAhead(player, player, 0.0, 100.0, true) != null; + case DUMMY, SMOKE -> false; + case null -> false; + default -> true; + }; + } + + /** + * Where the player sees from: in first person the camera, which CrewController moves to gunner + * sights and launcher cameras; otherwise the player's eyes. + */ + private static Vec3 viewPoint(LocalPlayer player) { + Minecraft mc = Minecraft.getInstance(); + return mc.options.getCameraType().isFirstPerson() ? mc.gameRenderer.mainCamera().position() : player.getEyePosition(); + } + + /** A block along the player's view: dry ground for strike weapons, water for guided torpedoes. */ + private static @Nullable BlockHitResult landAhead(Entity origin, LocalPlayer player, double eyeHeight, double range, boolean water) { + Vec3 from = origin.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(Vec3.directionFromRotation(player.getXRot(), player.getYRot()).scale(range)); + BlockHitResult hit = player.level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, player)); + if (hit.getType() != HitResult.Type.BLOCK) { + return null; + } + boolean wet = false; + BlockPos pos = hit.getBlockPos(); + for (Direction d : Direction.values()) { + wet |= player.level().getFluidState(pos.relative(d)).is(FluidTags.WATER); + } + return wet == water ? hit : null; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + private static void click() { + LocalPlayer player = Minecraft.getInstance().player; + if (player != null) { + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + } +} From c027431392bf31a0bb5856e77e1ae49a6f07e53c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:57 +0800 Subject: [PATCH 0198/1461] Add client/WeaponHud.java to the 26.2 shared sources --- .../jnhprojects/mcheli/client/WeaponHud.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java new file mode 100644 index 00000000..c742003d --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java @@ -0,0 +1,123 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; + +/** + * The vehicle HUD layer. In first person it runs the seat's HUD script (the {@code HUD} line of + * the definition, or the kind's default), or {@code tv_missile} while flying a TV missile, as the + * original did; a seat whose HUD is {@code none} gets nothing. Where no script draws (third + * person, a destroyed vehicle, a script that does not exist) it shows a plain text readout of hit + * points, ammunition, reload, heat, lock and flares. + */ +final class WeaponHud { + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int RED = 0xFFFF5050; + private static final int GREEN = 0xFF60FF60; + private static final String NO_HUD = "none"; + + private WeaponHud() { + } + + /** A pilot in gunner mode gets the gunner seat's HUD, as in the original. */ + private static int hudSeat(AircraftEntity vehicle, int seat) { + return seat == 0 && vehicle.isGunnerMode() ? 1 : seat; + } + + static void draw(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + AircraftDefinition def = vehicle.definition(); + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (def == null || weapons == null || seat < 0 || seat >= def.seatCount()) { + return; + } + if (mc.options.getCameraType().isFirstPerson() && !vehicle.isDestroyed()) { + String hud = CrewController.tvCamera(vehicle, seat) != null ? "tv_missile" + : hudSeat(vehicle, seat) < def.huds.size() ? def.huds.get(hudSeat(vehicle, seat)) : null; + if (NO_HUD.equals(hud)) { + // The original's way of saying a seat has no HUD at all. + return; + } + if (hud != null && HudRenderer.draw(g, vehicle, def, player, seat, hud, delta.getGameTimeDeltaPartialTick(false))) { + return; + } + } + Font font = mc.font; + int x = g.guiWidth() - 160; + int y = g.guiHeight() - 92; + + float hp = (float) vehicle.hp() / def.maxHp; + int hpColor = vehicle.isDestroyed() ? RED : hp < 0.5F ? AMBER : GREEN; + g.text(font, Component.translatable("hud.mcheli.hp", vehicle.hp(), def.maxHp), x, y, hpColor); + y += 11; + + WeaponSet set = weapons.current(seat); + if (set != null) { + g.text(font, set.displayName(), x, y, WHITE); + y += 11; + Component ammo = vehicle.hasInfiniteAmmo(player) && set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo_infinite", set.ammo()) + : set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo", set.ammo(), set.restAmmo()) + : Component.translatable("hud.mcheli.ammo_none"); + g.text(font, ammo, x, y, set.ammo() == 0 && set.magazineSize() > 0 ? RED : WHITE); + y += 11; + Component status = null; + int color = AMBER; + if (set.isReloading()) { + status = Component.translatable("hud.mcheli.reloading"); + } else if (set.isOverheated()) { + status = Component.translatable("hud.mcheli.overheat"); + color = RED; + } else if (set.isInPreparation()) { + status = Component.translatable("hud.mcheli.preparing"); + } else if (set.magazineSize() > 0 && set.ammo() == 0 && set.restAmmo() == 0) { + status = Component.translatable("hud.mcheli.resupply", KeyBindings.VEHICLE_SCREEN.getTranslatedKeyMessage()); + color = RED; + } + Guidance lock = weapons.guidance(weapons.selected(seat)); + if (status == null && lock != null && lock.lockCount() > 0) { + status = lock.isLocked() + ? Component.translatable("hud.mcheli.locked") + : Component.translatable("hud.mcheli.locking", lock.lockCount() * 100 / Math.max(lock.lockCountMax(), 1)); + color = lock.isLocked() ? RED : AMBER; + } + if (status != null) { + g.text(font, status, x, y, color); + y += 11; + } + if (set.definition().maxHeatCount > 0) { + int width = 100; + int filled = Math.min(width, set.currentHeat * width / set.definition().maxHeatCount); + g.fill(x, y + 2, x + width, y + 5, 0x80000000); + g.fill(x, y + 2, x + filled, y + 5, set.isOverheated() ? RED : AMBER); + y += 8; + } + } else if (!weapons.sets().isEmpty()) { + g.text(font, Component.translatable("hud.mcheli.no_weapon"), x, y, WHITE); + y += 11; + } + if (seat <= 1 && weapons.flares.isFitted()) { + Component flare = vehicle.isFlareActive() ? Component.translatable("hud.mcheli.flare_active") + : weapons.flares.isPreparing() ? Component.translatable("hud.mcheli.flare_wait") + : Component.translatable("hud.mcheli.flare_ready", KeyBindings.FLARE.getTranslatedKeyMessage()); + g.text(font, flare, x, y, vehicle.isFlareActive() ? GREEN : WHITE); + } + } +} From df15240caaa68c83edc91f3da9c7e55eccbc3512 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:57 +0800 Subject: [PATCH 0199/1461] Add client/hud/HudCanvas.java to the 26.2 shared sources --- .../mcheli/client/hud/HudCanvas.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java new file mode 100644 index 00000000..d45c753b --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java @@ -0,0 +1,165 @@ +package com.jnhprojects.mcheli.client.hud; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import org.joml.Matrix3x2fStack; + +/** + * The drawing primitives of {@code MCH_HudItem}, on the 26.x GUI. + * + *

The original drew GL lines, points and quads at fractional GUI coordinates. The GUI now only + * takes whole numbers, but every element keeps a copy of the pose, so each primitive is a unit + * square stretched and turned into place. Lines are one GUI unit thick, which is what + * {@code glLineWidth(scaleFactor)} gave; blips are two units square. + */ +final class HudCanvas { + private static final float LINE_WIDTH = 1.0F; + private static final float POINT_SIZE = 2.0F; + + private final GuiGraphicsExtractor g; + private final Font font; + + HudCanvas(GuiGraphicsExtractor g, Font font) { + this.g = g; + this.font = font; + } + + Matrix3x2fStack pose() { + return g.pose(); + } + + /** Turns everything drawn until the matching {@code pop} by {@code degrees} about a point. */ + void pushRotation(double x, double y, float degrees) { + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) x, (float) y); + pose.rotate((float) Math.toRadians(degrees)); + pose.translate((float) -x, (float) -y); + } + + void pop() { + g.pose().popMatrix(); + } + + void line(double x0, double y0, double x1, double y1, int color) { + double dx = x1 - x0; + double dy = y1 - y0; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + return; + } + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) x0, (float) y0); + pose.rotate((float) Math.atan2(dy, dx)); + pose.translate(0.0F, -LINE_WIDTH / 2.0F); + pose.scale((float) length, LINE_WIDTH); + g.fill(0, 0, 1, 1, color); + pose.popMatrix(); + } + + /** {@code GL_LINES}: x, y pairs taken two points at a time. */ + void lines(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 4) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_STRIP}. */ + void strip(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 2) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_LOOP}. */ + void loop(double[] p, int color) { + strip(p, color); + if (p.length >= 4) { + line(p[p.length - 2], p[p.length - 1], p[0], p[1], color); + } + } + + /** + * {@code GL_LINES} with {@code glLineStipple(factor, pattern)}: each segment starts the 16 bit + * pattern afresh, lowest bit first, every bit covering {@code factor} GUI units. + */ + void stippled(double[] p, int color, int factor, int pattern) { + double step = Math.max(factor, 1); + for (int i = 0; i + 3 < p.length; i += 4) { + double dx = p[i + 2] - p[i]; + double dy = p[i + 3] - p[i + 1]; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + continue; + } + double ux = dx / length; + double uy = dy / length; + double runStart = -1.0; + int bit = 0; + for (double t = 0.0; t < length; t += step, bit = (bit + 1) & 15) { + boolean on = (pattern >> bit & 1) != 0; + if (on && runStart < 0.0) { + runStart = t; + } else if (!on && runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i] + ux * t, p[i + 1] + uy * t, color); + runStart = -1.0; + } + } + if (runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i + 2], p[i + 3], color); + } + } + } + + void rect(double x0, double y0, double x1, double y1, int color) { + double left = Math.min(x0, x1); + double top = Math.min(y0, y1); + double width = Math.abs(x1 - x0); + double height = Math.abs(y1 - y0); + if (width <= 0.0 || height <= 0.0) { + return; + } + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) left, (float) top); + pose.scale((float) width, (float) height); + g.fill(0, 0, 1, 1, color); + pose.popMatrix(); + } + + void point(double x, double y, int color) { + rect(x - POINT_SIZE / 2.0, y - POINT_SIZE / 2.0, x + POINT_SIZE / 2.0, y + POINT_SIZE / 2.0, color); + } + + /** + * {@code drawTexture}: texels {@code u, v, uWidth, vHeight} of a {@code textureWidth} by + * {@code textureHeight} image, drawn into a box and turned about its centre. Drawn untinted, + * as the original reset the colour to white first. + */ + void texture(Identifier texture, double left, double top, double width, double height, double u, double v, + double uWidth, double vHeight, float rotation, int textureWidth, int textureHeight) { + if (width == 0.0 || height == 0.0) { + return; + } + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) (left + width / 2.0), (float) (top + height / 2.0)); + pose.rotate((float) Math.toRadians(rotation)); + pose.translate((float) (-width / 2.0), (float) (-height / 2.0)); + pose.scale((float) width, (float) height); + g.blit(texture, 0, 0, 1, 1, + (float) (u / textureWidth), (float) ((u + uWidth) / textureWidth), + (float) (v / textureHeight), (float) ((v + vHeight) / textureHeight)); + pose.popMatrix(); + } + + void text(String text, int x, int y, int color, boolean centered) { + if (centered) { + g.centeredText(font, text, x, y, color); + } else { + g.text(font, text, x, y, color); + } + } +} From 8570181d3fd51232d5827163003dd43de1c7a423 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:58 +0800 Subject: [PATCH 0200/1461] Add client/hud/HudRenderer.java to the 26.2 shared sources --- .../mcheli/client/hud/HudRenderer.java | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java new file mode 100644 index 00000000..0115b4c3 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java @@ -0,0 +1,354 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.hud.Expression; +import com.jnhprojects.mcheli.hud.HudCommand; +import com.jnhprojects.mcheli.hud.HudScript; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.IllegalFormatException; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Runs HUD scripts, the drawing half of {@code MCH_Hud} and the {@code MCH_HudItem} subclasses. + * + *

An {@code If} hides every line up to its {@code EndIf}; {@code Exit} ends the current script + * only, so a {@code Call}er carries on; the colour carries into called scripts and over to the next + * frame. A script cannot call itself, directly or through another, the same guard the original had. + * A line that fails to format is skipped and reported once, where the original crashed. + */ +public final class HudRenderer { + private static final Logger LOGGER = LoggerFactory.getLogger(HudRenderer.class); + private static final int AZIMUTH_RED = 0xFFFF0000; + private static final String[] AZIMUTH_8 = {"S", "SW", "W", "NW", "N", "NE", "E", "SE"}; + /** {@code glLineStipple} pattern of the original's dashed lines. */ + private static final int DASHES = 0xCCCC; + + private static final HudState STATE = new HudState(); + private static final Set REPORTED = new HashSet<>(); + + private final HudCanvas canvas; + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final LocalPlayer player; + private final double centerX; + private final double centerY; + private final Set running = new HashSet<>(); + + private HudRenderer(HudCanvas canvas, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, double width, double height) { + this.canvas = canvas; + this.vehicle = vehicle; + this.def = def; + this.player = player; + this.centerX = width / 2.0; + this.centerY = height / 2.0; + } + + public static void tick() { + LocalPlayer player = Minecraft.getInstance().player; + STATE.tick(player != null && player.getVehicle() instanceof AircraftEntity vehicle ? vehicle : null); + } + + /** Fed by the pilot's mouse steering; moves the control stick indicator. */ + public static void onPilotLook(float dYaw, float dPitch) { + STATE.onPilotLook(dYaw, dPitch); + } + + /** + * Draws a HUD script for the local player's seat. + * + * @return false if no script by that name is loaded, so the caller can draw something else + */ + public static boolean draw(GuiGraphicsExtractor g, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, + int seat, String hud, float partialTicks) { + HudScript script = HudScripts.INSTANCE.get(hud); + if (script == null) { + return false; + } + double width = g.guiWidth(); + double height = g.guiHeight(); + STATE.update(vehicle, def, player, seat, width, height, partialTicks); + HudRenderer renderer = new HudRenderer(new HudCanvas(g, Minecraft.getInstance().font), vehicle, def, player, width, height); + renderer.run(script); + return true; + } + + private double eval(Expression e) { + return e.eval(STATE.vars); + } + + private double x(Expression e) { + return centerX + eval(e); + } + + private double y(Expression e) { + return centerY + eval(e); + } + + private double[] points(List coordinates) { + double[] out = new double[coordinates.size()]; + for (int i = 0; i + 1 < out.length; i += 2) { + out[i] = x(coordinates.get(i)); + out[i + 1] = y(coordinates.get(i + 1)); + } + return out; + } + + private void run(HudScript script) { + if (!running.add(script.name())) { + return; + } + try { + boolean hidden = false; + for (HudCommand command : script.commands()) { + if (command instanceof HudCommand.If c) { + hidden = eval(c.condition()) == 0.0; + continue; + } + if (command instanceof HudCommand.EndIf) { + hidden = false; + continue; + } + if (hidden) { + continue; + } + if (command instanceof HudCommand.Exit) { + return; + } + execute(script, command); + } + } finally { + running.remove(script.name()); + } + } + + private void execute(HudScript script, HudCommand command) { + int color = STATE.color; + switch (command) { + case HudCommand.Color c -> { + STATE.color = (int) (long) eval(c.argb()); + STATE.vars.set(HudVariable.COLOR, STATE.color & 0xFFFFFFFFL); + } + case HudCommand.Call c -> { + HudScript called = HudScripts.INSTANCE.get(c.hud()); + if (called != null) { + run(called); + } + } + case HudCommand.Text c -> text(script, c, color); + case HudCommand.Texture c -> { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + c.name() + ".png"); + int[] size = HudScripts.INSTANCE.textureSize(Minecraft.getInstance().getResourceManager(), texture); + canvas.texture(texture, x(c.left()), y(c.top()), eval(c.width()), eval(c.height()), + eval(c.u()), eval(c.v()), eval(c.uWidth()), eval(c.vHeight()), (float) eval(c.rotation()), size[0], size[1]); + } + case HudCommand.Rect c -> { + // The original truncated the size, not the position. + double left = x(c.left()); + double top = y(c.top()); + canvas.rect(left, top, left + (int) eval(c.width()), top + (int) eval(c.height()), color); + } + case HudCommand.Line c -> canvas.strip(points(c.points()), color); + case HudCommand.LineStipple c -> canvas.stippled(points(c.points()), color, (int) eval(c.factor()), (int) eval(c.pattern())); + case HudCommand.Radar c -> radar(c, color); + case HudCommand.Graduation c -> graduation(c, color); + case HudCommand.CameraRot c -> cameraRot(x(c.x()), y(c.y()), color); + case HudCommand.If c -> { + } + case HudCommand.EndIf c -> { + } + case HudCommand.Exit c -> { + } + } + } + + /** {@code MCH_HudItemString}: skipped while a weapon value is shown and no weapon (or magazine) applies. */ + private void text(HudScript script, HudCommand.Text c, int color) { + Object[] args = new Object[c.args().size()]; + for (int i = 0; i < args.length; i++) { + TextArg arg = c.args().get(i); + if ((arg.needsWeapon() && !STATE.hasWeapon()) || (arg.needsMagazine() && !STATE.hasMagazine())) { + return; + } + args[i] = STATE.text(arg, vehicle, def, player); + } + String text; + try { + text = String.format(Locale.ROOT, c.format(), args); + } catch (IllegalFormatException e) { + String key = script.name() + ':' + c.line(); + if (REPORTED.add(key)) { + LOGGER.warn("HUD {}.txt line {}: cannot format \"{}\": {}", script.name(), c.line(), c.format(), e.getMessage()); + } + return; + } + canvas.text(text, (int) x(c.x()), (int) y(c.y()), color, c.centered()); + } + + /** {@code MCH_HudItemRadar}: blips at half scale for a 64 unit box, turned with the view. */ + private void radar(HudCommand.Radar c, int color) { + List blips = c.enemies() ? STATE.enemyBlips : STATE.entityBlips; + if (blips.isEmpty()) { + return; + } + double rotation = Math.toRadians(eval(c.rotation())); + double left = x(c.left()); + double top = y(c.top()); + double w = eval(c.width()); + double h = eval(c.height()); + double cos = Math.cos(rotation); + double sin = Math.sin(rotation); + for (double[] blip : blips) { + double px = blip[0] / 2.0 * w / 64.0; + double py = blip[1] / 2.0 * h / 64.0; + double rx = px * cos - py * sin; + double ry = px * sin + py * cos; + if (rx > -w / 2.0 && rx < w / 2.0 && ry > -h / 2.0 && ry < h / 2.0) { + canvas.point(rx + left + w / 2.0, ry + top + h / 2.0, color); + } + } + } + + private void graduation(HudCommand.Graduation c, int color) { + int x = (int) x(c.x()); + int y = (int) y(c.y()); + double angle = eval(c.angle()); + canvas.pushRotation(x, y, (float) eval(c.roll())); + switch (c.kind()) { + case YAW -> headingTape(angle, color, x, y); + case PITCH_1 -> pitchScale(angle, color, x, y); + case PITCH_2 -> pitchLadder(angle, color, x, y, true); + case PITCH_3 -> pitchLadder(angle, color, x, y, false); + } + canvas.pop(); + } + + /** {@code drawCommonGraduationYaw}: a tick every degree, labels every three, compass points every 45. */ + private void headingTape(double playerYaw, int color, int posX, int posY) { + double yaw = HudState.rotate360(playerYaw); + posX -= 90; + double[] line = new double[76]; + int offset = (int) (yaw * 10.0) % 10; + int degree = (int) yaw - 9; + for (int i = 0; i < line.length / 4; i++, degree++) { + int azimuthX = posX + i * 10 - offset; + line[i * 4] = azimuthX; + line[i * 4 + 1] = posY; + line[i * 4 + 2] = azimuthX; + line[i * 4 + 3] = posY + (degree % 45 == 0 ? 15 : degree % 3 == 0 ? 10 : 5); + if (degree % 45 == 0) { + canvas.text(AZIMUTH_8[Math.floorMod(degree, 360) / 45], azimuthX, posY - 10, AZIMUTH_RED, true); + } else if (degree % 3 == 0) { + int heading = degree + 180; + if (heading < 0) { + heading += 360; + } + if (heading > 360) { + heading -= 360; + } + canvas.text(Integer.toString(heading), azimuthX, posY - 10, color, true); + } + } + canvas.lines(line, color); + } + + /** {@code drawCommonGraduationPitch1}: a scale of ticks on both sides that slides with the pitch. */ + private void pitchScale(double playerPitch, int color, int posX, int posY) { + int pitch = (int) playerPitch % 360; + int y = (int) (playerPitch * 10.0 % 10.0); + if (y < 0) { + y += 10; + } + int left = posX - 100; + int right = posX + 100; + posY -= 80; + double[] line = new double[144]; + int p = playerPitch < 0.0 && y != 0 ? pitch - 9 : pitch - 8; + for (int i = 0; i < line.length / 8; i++, p++) { + int outer = p % 3 == 0 ? 15 : 5; + double rowY = posY + i * 10 - y; + line[i * 8] = left - outer; + line[i * 8 + 1] = rowY; + line[i * 8 + 2] = left; + line[i * 8 + 3] = rowY; + line[i * 8 + 4] = right + outer; + line[i * 8 + 5] = rowY; + line[i * 8 + 6] = right; + line[i * 8 + 7] = rowY; + } + canvas.lines(line, color); + canvas.strip(new double[]{left - 25, posY - 90, left, posY - 90, left, posY + 90, left - 25, posY + 90}, color); + canvas.strip(new double[]{right + 25, posY - 90, right, posY - 90, right, posY + 90, right + 25, posY + 90}, color); + } + + /** + * {@code drawCommonGraduationPitch2}: a ladder with a rung every five degrees, dashed below + * the horizon. {@code wide} is {@code Pitch2} with five rungs; {@code Pitch3} has three, twice as far apart. + */ + private void pitchLadder(double playerPitch, int color, int posX, int posY, boolean wide) { + playerPitch = -playerPitch; + int pitchStep = (int) playerPitch / 5 * 5; + int start = wide ? 0 : 1; + int end = wide ? 5 : 4; + int spacing = wide ? 1 : 2; + double rest = playerPitch % 5.0; + double[] line = new double[8]; + for (int i = start; i < end; i++) { + int pitch = -(-pitchStep - 10 + i * 5); + int halfWidth = pitch != 0 ? 50 : 100; + int y = posY + (int) (-60 * spacing + rest * 6.0 * spacing + i * 30 * spacing); + line[0] = posX - halfWidth; + line[1] = y + (pitch == 0 ? 0 : pitch > 0 ? 2 : -2); + line[2] = posX - 50; + line[3] = y; + line[4] = posX + halfWidth; + line[5] = line[1]; + line[6] = posX + 50; + line[7] = y; + canvas.lines(line, color); + line[0] = posX - 50; + line[1] = y; + line[2] = posX - 30; + line[3] = y; + line[4] = posX + 50; + line[5] = y; + line[6] = posX + 30; + line[7] = y; + if (pitch >= 0) { + canvas.lines(line, color); + } else { + canvas.stippled(line, color, 1, DASHES); + } + if (pitch != 0) { + canvas.text(Integer.toString(pitch), posX - 60, y - 4, color, true); + canvas.text(Integer.toString(pitch), posX + 60, y - 4, color, true); + } + } + } + + /** {@code MCH_HudItemCameraRot}: a frame with a box showing where the crew's view points against the hull. */ + private void cameraRot(double posX, double posY, int color) { + canvas.loop(new double[]{posX - 21, posY - 11, posX + 21, posY - 11, posX + 21, posY + 11, posX - 21, posY + 11}, color); + canvas.stippled(new double[]{posX - 21, posY, posX, posY, posX + 21, posY, posX, posY, + posX, posY - 11, posX, posY, posX, posY + 11, posX, posY}, color, 1, DASHES); + float pitch = Mth.clamp(player.getXRot(), -30.0F, 70.0F); + pitch = (float) ((pitch - 20.0F) * 0.16); + float yaw = Mth.clamp(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot()) * 2.0F, -50.0F, 50.0F); + yaw = (float) (yaw * 0.34); + canvas.loop(new double[]{posX + yaw - 3, posY + pitch - 2, posX + yaw + 3, posY + pitch - 2, + posX + yaw + 3, posY + pitch + 2, posX + yaw - 3, posY + pitch + 2}, color); + } +} From fd56c7dbb9d5bf6e18947cf37b3e80a4078ce894 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:58 +0800 Subject: [PATCH 0201/1461] Add client/hud/HudScripts.java to the 26.2 shared sources --- .../mcheli/client/hud/HudScripts.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java new file mode 100644 index 00000000..6e611a23 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java @@ -0,0 +1,86 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.hud.HudFormatException; +import com.jnhprojects.mcheli.hud.HudScript; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +/** + * The HUD scripts in {@code assets/mcheli/hud}, read through the resource manager so resource + * packs can replace or add them, and reloaded with F3 + T. A script that fails to parse is logged + * and left out; the vehicles using it fall back to the plain readout. + */ +public final class HudScripts implements ResourceManagerReloadListener { + public static final HudScripts INSTANCE = new HudScripts(); + private static final Logger LOGGER = LoggerFactory.getLogger(HudScripts.class); + private static final String FOLDER = "hud"; + private static final int DEFAULT_TEXTURE_SIZE = 256; + + private Map scripts = Map.of(); + private final Map textureSizes = new HashMap<>(); + + private HudScripts() { + } + + public @Nullable HudScript get(String name) { + return scripts.get(name.toLowerCase(Locale.ROOT)); + } + + @Override + public void onResourceManagerReload(ResourceManager resources) { + Map loaded = new HashMap<>(); + resources.listResources(FOLDER, id -> id.getNamespace().equals(MCHeli.MOD_ID) && id.getPath().endsWith(".txt")) + .forEach((id, resource) -> { + String path = id.getPath(); + String name = path.substring(FOLDER.length() + 1, path.length() - 4).toLowerCase(Locale.ROOT); + try (Reader in = resource.openAsReader()) { + loaded.put(name, HudScript.parse(name, in)); + } catch (IOException | HudFormatException e) { + LOGGER.error("Skipping HUD {}: {}", id, e.getMessage()); + } + }); + scripts = Map.copyOf(loaded); + textureSizes.clear(); + LOGGER.info("Loaded {} HUD scripts", scripts.size()); + } + + /** + * Width and height of a GUI texture in pixels, read from the PNG header once. The original + * fell back to 256 when it could not tell, and so does this. + */ + int[] textureSize(ResourceManager resources, Identifier texture) { + return textureSizes.computeIfAbsent(texture, id -> readSize(resources.getResource(id))); + } + + private static int[] readSize(Optional resource) { + if (resource.isPresent()) { + try (InputStream in = resource.get().open(); DataInputStream data = new DataInputStream(in)) { + // 8 byte signature, IHDR length and type, then width and height as big endian ints. + data.skipNBytes(16); + int width = data.readInt(); + int height = data.readInt(); + if (width > 0 && height > 0) { + return new int[]{width, height}; + } + } catch (IOException e) { + LOGGER.warn("Could not read the size of a HUD texture", e); + } + } + return new int[]{DEFAULT_TEXTURE_SIZE, DEFAULT_TEXTURE_SIZE}; + } +} From 7b88a6cbeb5f069b115e9a0c160dfb6770eaf7bb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:59 +0800 Subject: [PATCH 0202/1461] Report 26.2 as the Minecraft version to HUD scripts --- .../mcheli/client/hud/HudState.java | 373 ++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java new file mode 100644 index 00000000..8bbe3a28 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java @@ -0,0 +1,373 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.client.VehicleCamera; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * What the HUD scripts read, the static state of {@code MCH_HudItem}. Ticked with the client, so + * the radar sweep, altitude and stick behave as in the original; {@link #update} then fills in the + * per frame values before a script runs. + * + *

Systems that arrive later read as their idle state for now: the vehicle inventory is empty + * and the mortar distance is unknown. + */ +final class HudState { + private static final int RADAR_RANGE = 64; + private static final int ALTITUDE_INTERVAL = 10; + private static final double STICK_MAX = 40.0; + /** Roughly the mouse units of the original per degree of pilot look. */ + private static final double STICK_PER_DEGREE = 6.0; + private static final int STICK_SAMPLES = 4; + + final HudVariable.Values vars = new HudVariable.Values(); + final List entityBlips = new ArrayList<>(); + final List enemyBlips = new ArrayList<>(); + + private @Nullable AircraftEntity vehicle; + /** Where the selected mortar would land, or -1. */ + private float mortarDistance = -1.0F; + private int radarRotate; + private int radarRotateO; + private int altitude; + private int altitudeCountdown; + private double stickX; + private double stickY; + private final double[] stickXSamples = new double[STICK_SAMPLES]; + private final double[] stickYSamples = new double[STICK_SAMPLES]; + private int stickSample; + private int fuelWarnCount; + + /** Colour set by the last {@code Color} line. Like the original it carries over between frames. */ + int color = 0xFF000000; + + // Per frame, for the text arguments. + private @Nullable WeaponSet weapon; + private String weaponName = ""; + private String weaponAmmo = ""; + private String weaponRestAmmo = ""; + private float reloadPercent; + private float reloadSeconds; + + /** Mouse look of the pilot this frame, in degrees; drives the control stick indicator. */ + void onPilotLook(float dYaw, float dPitch) { + stickX += dYaw * STICK_PER_DEGREE; + stickY += dPitch * STICK_PER_DEGREE; + double length = Math.sqrt(stickX * stickX + stickY * stickY); + if (length > STICK_MAX) { + stickX *= STICK_MAX / length; + stickY *= STICK_MAX / length; + } + } + + void tick(@Nullable AircraftEntity riding) { + if (riding != vehicle) { + vehicle = riding; + radarRotate = 0; + radarRotateO = 0; + altitudeCountdown = 0; + entityBlips.clear(); + enemyBlips.clear(); + } + stickX *= 0.85; + stickY *= 0.85; + if (riding == null || riding.definition() == null) { + return; + } + if (riding.definition().entityRadar) { + radarRotateO = radarRotate; + radarRotate += radarSpeed(riding); + if (radarRotate >= 360) { + radarRotate = 0; + radarRotateO = 0; + sweepRadar(riding); + } + } + if (--altitudeCountdown <= 0) { + altitude = measureAltitude(riding); + altitudeCountdown = ALTITUDE_INTERVAL; + } + } + + /** Degrees per tick, from each kind's client tick handler. */ + private static int radarSpeed(AircraftEntity vehicle) { + return vehicle instanceof PlaneEntity || vehicle instanceof TankEntity ? 10 : 5; + } + + /** {@code MCH_Radar.updateXZ}: mobs within 64 blocks that are not buried under five or more blocks. */ + private void sweepRadar(AircraftEntity center) { + entityBlips.clear(); + enemyBlips.clear(); + Level level = center.level(); + for (Mob mob : level.getEntitiesOfClass(Mob.class, center.getBoundingBox().inflate(RADAR_RANGE))) { + double x = mob.getX() - center.getX(); + double z = mob.getZ() - center.getZ(); + if (x * x + z * z >= RADAR_RANGE * RADAR_RANGE || isBuried(level, mob)) { + continue; + } + (mob instanceof Enemy ? enemyBlips : entityBlips).add(new double[]{x, z}); + } + } + + private static boolean isBuried(Level level, Mob mob) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(mob.getBlockX(), Math.max(mob.getBlockY() + 1, level.getMinY()), mob.getBlockZ()); + int blocks = 0; + for (; pos.getY() < level.getMaxY(); pos.move(0, 1, 0)) { + if (!level.getBlockState(pos).isAir() && ++blocks >= 5) { + return true; + } + } + return false; + } + + /** {@code updateAltitude}: blocks of air below the vehicle, looking at most 256 down. */ + private static int measureAltitude(AircraftEntity vehicle) { + Level level = vehicle.level(); + BlockPos.MutableBlockPos pos = vehicle.blockPosition().mutable(); + int top = pos.getY(); + for (int i = 0; i < 256 && top - i > level.getMinY(); i++) { + pos.setY(top - i); + if (!level.getBlockState(pos).isAir()) { + return i; + } + } + return 0; + } + + float radarRotation(float partialTicks) { + float rot = radarRotate; + float prev = radarRotateO; + if (rot < prev) { + rot += 360.0F; + } + return prev + (rot - prev) * partialTicks; + } + + void update(AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, int seat, double width, double height, + float partialTicks) { + HudVariable.Values v = vars; + v.set(HudVariable.COLOR, color & 0xFFFFFFFFL); + v.set(HudVariable.CENTER_X, width / 2.0); + v.set(HudVariable.CENTER_Y, height / 2.0); + v.set(HudVariable.WIDTH, width); + v.set(HudVariable.HEIGHT, height); + v.set(HudVariable.TIME, Math.floorMod(vehicle.level().getOverworldClockTime(), 24000L)); + v.set(HudVariable.TEST_MODE, 0.0); + v.set(HudVariable.PLYR_YAW, Mth.wrapDegrees(player.getViewYRot(partialTicks))); + v.set(HudVariable.PLYR_PITCH, player.getViewXRot(partialTicks)); + v.set(HudVariable.YAW, Mth.wrapDegrees(vehicle.getYRot(partialTicks))); + v.set(HudVariable.PITCH, vehicle.getXRot(partialTicks)); + v.set(HudVariable.ROLL, Mth.wrapDegrees(vehicle.roll(partialTicks))); + v.set(HudVariable.ALTITUDE, altitude); + v.set(HudVariable.SEA_ALT, Math.max(vehicle.getY() - vehicle.level().getSeaLevel(), 0.0)); + v.set(HudVariable.HAVE_RADAR, def.entityRadar); + v.set(HudVariable.RADAR_ROT, radarRotation(partialTicks)); + v.set(HudVariable.HP, vehicle.hp()); + v.set(HudVariable.MAX_HP, def.maxHp); + v.set(HudVariable.HP_RTO, def.maxHp > 0 ? (double) vehicle.hp() / def.maxHp : 0.0); + v.set(HudVariable.THROTTLE, vehicle.visibleThrottle()); + v.set(HudVariable.POS_X, vehicle.getX()); + v.set(HudVariable.POS_Y, vehicle.getY()); + v.set(HudVariable.POS_Z, vehicle.getZ()); + // Only the pilot's client simulates the hull, so read motion from the position instead. + double mx = vehicle.getX() - vehicle.xo; + double my = vehicle.getY() - vehicle.yo; + double mz = vehicle.getZ() - vehicle.zo; + v.set(HudVariable.MOTION_X, mx); + v.set(HudVariable.MOTION_Y, my); + v.set(HudVariable.MOTION_Z, mz); + v.set(HudVariable.SPEED, Math.sqrt(mx * mx + my * my + mz * mz)); + v.set(HudVariable.FUEL, vehicle.fuelRatio()); + v.set(HudVariable.LOW_FUEL, lowFuel(vehicle)); + updateStick(seat == 0 && vehicle.steersWithMouse()); + updateWeapon(vehicle, seat); + v.set(HudVariable.VTOL_STAT, vehicle.vtolMode()); + v.set(HudVariable.FREE_LOOK, seat == 0 && vehicle.input().freeLook); + v.set(HudVariable.GUNNER_MODE, vehicle.isGunnerMode(player)); + v.set(HudVariable.CAM_MODE, VehicleCamera.mode().ordinal()); + v.set(HudVariable.CAM_ZOOM, VehicleCamera.zoom()); + v.set(HudVariable.AUTO_PILOT, vehicle instanceof PlaneEntity && vehicle.isGunnerMode(player)); + VehicleWeapons weapons = vehicle.weapons(); + boolean flares = weapons != null && weapons.flares.isFitted(); + v.set(HudVariable.HAVE_FLARE, flares); + v.set(HudVariable.CAN_FLARE, flares && !weapons.flares.isPreparing() && !weapons.flares.isActive()); + v.set(HudVariable.INVENTORY, 0.0); + v.set(HudVariable.HOVERING, vehicle instanceof HelicopterEntity && vehicle.isHoveringMode()); + v.set(HudVariable.IS_UAV, def.uav); + double uavDistance = vehicle.stationDistance(); + // {@code getUAV_Fs}: signal strength, full at the station and gone 120 blocks out. + v.set(HudVariable.UAV_FS, uavDistance < 0.0 ? 0.0 : 1.0 - Math.min(uavDistance, 120.0) / 120.0); + } + + /** + * {@code isLowFuel}: below 10% the warning blinks, on for eight seconds out of every fourteen. + * Ticks with each update, as the original counted per frame. + */ + private boolean lowFuel(AircraftEntity vehicle) { + if (fuelWarnCount <= 0) { + fuelWarnCount = 280; + } + fuelWarnCount--; + return fuelWarnCount < 160 && vehicle.maxFuel() > 0 && vehicle.fuelRatio() < 0.1F; + } + + /** The original averaged the last four stick positions, scaled to -1..1, with up positive. */ + private void updateStick(boolean pilot) { + stickXSamples[stickSample] = pilot ? stickX / STICK_MAX : 0.0; + stickYSamples[stickSample] = pilot ? -stickY / STICK_MAX : 0.0; + stickSample = (stickSample + 1) % STICK_SAMPLES; + double x = 0.0; + double y = 0.0; + for (int i = 0; i < STICK_SAMPLES; i++) { + x += stickXSamples[i]; + y += stickYSamples[i]; + } + vars.set(HudVariable.STICK_X, x / STICK_SAMPLES); + vars.set(HudVariable.STICK_Y, y / STICK_SAMPLES); + } + + /** {@code updateWeapon} and {@code updateVarMap_Weapon}. */ + private void updateWeapon(AircraftEntity vehicle, int seat) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + weapon = set; + double reloadTime = 0.0; + double heat = 0.0; + double lock = 0.0; + int sightType = 0; + if (set != null) { + WeaponDefinition def = set.definition(); + weaponName = set.displayName(); + weaponAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.ammo()) : ""; + weaponRestAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.restAmmo()) : ""; + float wait; + if (def.delay > def.reloadTime) { + reloadSeconds = Math.abs(set.countWait); + wait = Math.min(Math.abs((float) set.countWait / Math.max(def.delay, 1)), 1.0F); + } else { + reloadSeconds = set.countReloadWait; + wait = (float) set.countReloadWait / Math.max(def.reloadTime, 1); + } + reloadTime = wait; + reloadSeconds /= 20.0F; + reloadPercent = (1.0F - wait) * 100.0F; + if (def.maxHeatCount > 0) { + heat = Math.min((double) set.currentHeat / def.maxHeatCount, 1.0); + } + Guidance guidance = weapons.guidance(weapons.selected(seat)); + if (def.sight == WeaponDefinition.Sight.LOCK && guidance != null && guidance.lockCountMax() > 0) { + lock = (double) guidance.lockCount() / guidance.lockCountMax(); + sightType = 2; + } else if (def.sight == WeaponDefinition.Sight.ROCKET) { + sightType = 1; + } + vars.set(HudVariable.IS_HEAT_WPN, def.maxHeatCount > 0); + vars.set(HudVariable.RELOADING, set.isInPreparation()); + vars.set(HudVariable.DSP_MT_DIST, def.displayMortarDistance); + // The original worked the arc out every fifth tick; it is cheap enough to follow every update. + mortarDistance = def.displayMortarDistance ? (float) set.landingDistance(vehicle) : -1.0F; + } else { + vars.set(HudVariable.IS_HEAT_WPN, 0.0); + vars.set(HudVariable.RELOADING, 0.0); + vars.set(HudVariable.DSP_MT_DIST, 0.0); + mortarDistance = -1.0F; + } + vars.set(HudVariable.RELOAD_TIME, reloadTime); + vars.set(HudVariable.WPN_HEAT, heat); + vars.set(HudVariable.SIGHT_TYPE, sightType); + vars.set(HudVariable.LOCK, lock); + vars.set(HudVariable.MT_DIST, mortarDistance); + } + + boolean hasWeapon() { + return weapon != null; + } + + boolean hasMagazine() { + return weapon != null && weapon.magazineSize() > 0; + } + + /** A {@code DrawString} argument, typed the way the original passed it to {@code String.format}. */ + @Nullable Object text(TextArg arg, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player) { + int worldTime = (int) Math.floorMod(vehicle.level().getOverworldClockTime() + 6000L, 24000L); + return switch (arg) { + case NONE -> null; + // The vehicle inventory screen and its key come later in phase 4. + case KEY_GUI -> ""; + case NAME -> def.displayName; + case ALTITUDE -> altitude; + case DATE -> new Date(); + case MC_THOR -> worldTime / 1000; + case MC_TMIN -> worldTime % 1000 * 36 / 10 / 60; + case MC_TSEC -> worldTime % 1000 * 36 / 10 % 60; + case MAX_HP -> def.maxHp; + case HP -> vehicle.hp(); + case HP_PER -> def.maxHp > 0 ? 100.0 * vehicle.hp() / def.maxHp : 0.0; + case POS_X -> vehicle.getX(); + case POS_Y -> vehicle.getY(); + case POS_Z -> vehicle.getZ(); + case MOTION_X -> vars.get(HudVariable.MOTION_X); + case MOTION_Y -> vars.get(HudVariable.MOTION_Y); + case MOTION_Z -> vars.get(HudVariable.MOTION_Z); + case INVENTORY -> 0; + case WPN_NAME -> weaponName; + case WPN_AMMO -> weaponAmmo; + case WPN_RM_AMMO -> weaponRestAmmo; + case RELOAD_PER -> reloadPercent; + case RELOAD_SEC -> reloadSeconds; + case MORTAR_DIST -> mortarDistance; + case MC_VER -> "26.2"; + case MOD_VER -> Platform.get().modVersion(); + case MOD_NAME -> "MCHeli Rework"; + case YAW -> rotate360(vehicle.getYRot() + 180.0); + case PITCH -> -vehicle.getXRot(); + case ROLL -> Mth.wrapDegrees(vehicle.roll()); + case PLYR_YAW -> rotate360(player.getYRot() + 180.0); + case PLYR_PITCH -> -player.getXRot(); + case TVM_POS_X, TVM_POS_Y, TVM_POS_Z, TVM_DIFF -> tvMissile(arg, vehicle); + case CAM_ZOOM -> VehicleCamera.zoom(); + case UAV_DIST -> Math.max(vehicle.stationDistance(), 0.0); + case THROTTLE -> vehicle.visibleThrottle() * 100.0; + }; + } + + private static double tvMissile(TextArg arg, AircraftEntity vehicle) { + TvMissileEntity missile = vehicle.tvMissile(); + if (missile == null) { + return 0.0; + } + return switch (arg) { + case TVM_POS_X -> missile.getX(); + case TVM_POS_Y -> missile.getY(); + case TVM_POS_Z -> missile.getZ(); + default -> missile.position().distanceTo(vehicle.position()); + }; + } + + static double rotate360(double degrees) { + double r = degrees % 360.0; + return r >= 0.0 ? r : r + 360.0; + } +} From 1ecb7869e71c8c0e7185810065fca959024761c2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:40:59 +0800 Subject: [PATCH 0203/1461] Add client/particle/GlowParticle.java to the 26.2 shared sources --- .../mcheli/client/particle/GlowParticle.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java new file mode 100644 index 00000000..a09d9b2e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java @@ -0,0 +1,103 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.renderer.state.level.QuadParticleRenderState; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; +import net.minecraft.util.ARGB; +import net.minecraft.util.LightCoordsUtil; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import org.jspecify.annotations.Nullable; + +/** + * {@code mcheli:glow}: a ball of light added on top of the scene. A flash swells quickly and fades + * out; a fire holds its brightness, wavering, and only fades towards the end. How bright it gets + * depends on the light where it starts, so the same explosion is a flicker at noon and lights up + * the sky at night. + */ +public class GlowParticle extends SingleQuadParticle { + private final float size; + private final float peak; + private final boolean flicker; + private float waver = 1.0F; + + GlowParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, GlowParticleOptions options, + TextureAtlasSprite sprite, float strength) { + super(level, x, y, z, sprite); + setParticleSpeed(xd, yd, zd); + this.size = options.size(); + this.peak = strength; + this.flicker = options.flicker(); + this.quadSize = size; + this.lifetime = options.lifetime(); + this.friction = 0.9F; + this.gravity = 0.0F; + this.hasPhysics = false; + setColor(ARGB.redFloat(options.color()), ARGB.greenFloat(options.color()), ARGB.blueFloat(options.color())); + this.alpha = 0.0F; + } + + @Override + public void tick() { + super.tick(); + if (flicker) { + waver = 0.7F + random.nextFloat() * 0.3F; + } + } + + @Override + public void extract(QuadParticleRenderState state, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + if (flicker) { + // Burning: hold, then die down over the last third. + alpha = peak * waver * Math.min(1.0F, (1.0F - t) * 3.0F); + } else { + // A flash: brightest at once, gone quickly. + float left = 1.0F - t; + alpha = peak * left * left; + } + super.extract(state, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return flicker ? size * (0.85F + 0.15F * waver) : size * (0.6F + 0.4F * Math.min(1.0F, t * 4.0F)); + } + + @Override + protected int getLightCoords(float partialTicks) { + return LightCoordsUtil.FULL_BRIGHT; + } + + @Override + protected Layer getLayer() { + return Glow.PARTICLE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public @Nullable Particle createParticle(GlowParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd, RandomSource random) { + float strength = Glow.strength(level, BlockPos.containing(x, y, z)); + if (strength <= 0.0F) { + return null; + } + return new GlowParticle(level, x, y, z, xd, yd, zd, options, sprites.first(), strength); + } + } +} From 21574c796df5d05728acdce92c15418f61be5836 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:00 +0800 Subject: [PATCH 0204/1461] Add client/particle/SmokeParticle.java to the 26.2 shared sources --- .../mcheli/client/particle/SmokeParticle.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java new file mode 100644 index 00000000..ff6c3e73 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java @@ -0,0 +1,83 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.renderer.state.level.QuadParticleRenderState; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; + +/** + * {@code mcheli:smoke}: a puff of coloured smoke, {@code MCH_EntityParticleSmoke}. It swells to its + * full size over the first part of its life, its colour drifts towards white, it rises slowly and + * fades out at the end. The sprites are vanilla's campfire smoke. + */ +public class SmokeParticle extends SingleQuadParticle { + private final float size; + + SmokeParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, SmokeParticleOptions options, + SpriteSet sprites, RandomSource random) { + super(level, x, y, z, sprites.get(random)); + setParticleSpeed(xd, yd, zd); + // The original drew a puff 0.1 x its size out from the middle, so size 10 is two blocks across. + this.size = options.size() * 0.1F * (0.8F + random.nextFloat() * 0.4F); + this.quadSize = size * 0.4F; + this.lifetime = options.lifetime() > 0 ? options.lifetime() : 60 + random.nextInt(40); + this.friction = 0.96F; + this.gravity = -0.002F; + this.hasPhysics = true; + setColor(ARGB.redFloat(options.color()), ARGB.greenFloat(options.color()), ARGB.blueFloat(options.color())); + this.alpha = 0.9F; + } + + @Override + public void tick() { + super.tick(); + // toWhite: pale towards the brightest channel while the colour is still strong. + float max = Math.max(rCol, Math.max(gCol, bCol)); + float min = Math.min(rCol, Math.min(gCol, bCol)); + if (max - min > 0.2F) { + rCol += (max - rCol) * 0.016F; + gCol += (max - gCol) * 0.016F; + bCol += (max - bCol) * 0.016F; + } + } + + @Override + public void extract(QuadParticleRenderState state, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + alpha = 0.9F * Math.min(1.0F, (1.0F - t) * 4.0F); + super.extract(state, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return size * (0.4F + 0.6F * Math.min(1.0F, t * 3.0F)); + } + + @Override + protected Layer getLayer() { + return Glow.SMOKE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public Particle createParticle(SmokeParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd, RandomSource random) { + return new SmokeParticle(level, x, y, z, xd, yd, zd, options, sprites, random); + } + } +} From 799a7b3f0679be7e6b89722fb99482922167a94d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:00 +0800 Subject: [PATCH 0205/1461] Port A10Renderer to the 26.2 pose stack --- .../mcheli/client/render/A10Renderer.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java new file mode 100644 index 00000000..938be6d9 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +/** The close air support A-10, {@code MCH_RenderA10}. */ +public class A10Renderer extends EntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/a10.png"); + + public A10Renderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 10.5F; + } + + public static class State extends EntityRenderState { + float yaw; + float pitch; + boolean visible; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(A10Entity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.visible = entity.isVisible(); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = state.visible ? MeshCache.INSTANCE.get("", "a-10") : null; + if (mesh != null) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(-state.pitch)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutCull(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From a82a39ce970d38b02dc351c2fc1f0f22b42f04b3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:01 +0800 Subject: [PATCH 0206/1461] Add client/render/AircraftRenderState.java to the 26.2 shared sources --- .../client/render/AircraftRenderState.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java new file mode 100644 index 00000000..d2dd770d --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java @@ -0,0 +1,77 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import org.jspecify.annotations.Nullable; + +/** + * Snapshot of a vehicle for one frame, filled in by {@link AircraftRenderer#extractRenderState}. + * Every angle is already interpolated for the frame's partial tick. + */ +public class AircraftRenderState extends EntityRenderState { + /** Definitions are immutable after loading, so the snapshot can share them. */ + public @Nullable AircraftDefinition definition; + public String texture = ""; + public float yaw; + public float pitch; + public float roll; + /** Helicopter main rotor or plane propeller angle. */ + public float rotorAngle; + /** Each helicopter blade's own angle, per rotor, so folding blades can leave the rotor's spin. */ + public float[][] bladeAngles = new float[0][]; + public float throttle; + + // Part states, 0 to 90 degrees + public float canopy; + public float landingGear; + public float hatch; + /** Rope length below the hooks while troops go down, 0 with the ropes in. */ + public float ropeLength; + /** The beams of the search lights, or false with them off. */ + public boolean searchLight; + /** Where the crew aim a turning search light, in world degrees. */ + public float searchLightYaw; + public float searchLightPitch; + public float nozzle; + public float wing; + + public float wheelRoll; + public float steer; + public final float[] roller = new float[2]; + /** Track run per side from 0 to 1, in link spacings. */ + public final float[] track = new float[2]; + public float[] rotatingParts = new float[0]; + + /** Per seat: occupant view relative to the hull, and whether anyone sits there. */ + public float[] seatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public boolean[] seatOccupied = new boolean[0]; + /** Seat of the player at this client, or -1, and whether they look through their own eyes. */ + public int localSeat = -1; + public boolean localFirstPerson; + /** Shade for the unlit glass, from the light at the vehicle. */ + public int glassColor = 0xFFFFFFFF; + /** Drawn inside a screen, as the workshop does, rather than in the world. */ + public boolean inGui; + + /** Per {@code AddPartWeapon} line, the state of the weapon it belongs to; see {@link WeaponPartState}. */ + public WeaponPartState[] weaponParts = new WeaponPartState[0]; + /** Per weapon bay, 0 to 90 degrees. */ + public float[] bays = new float[0]; + + /** How one weapon part is drawn this frame. */ + public static final class WeaponPartState { + /** Whether the part's weapon exists; without one the part follows its operator's view. */ + public boolean hasWeapon; + /** Aim relative to the weapon's default direction. */ + public float yaw; + public float pitch; + public float defaultYaw; + /** How far a turret weapon lags behind the pilot's view. */ + public float turretLag; + public float recoil; + public float barrel; + /** A missile part whose round is on its way and not reloaded yet. */ + public boolean hidden; + } +} From 40e4b8aea6cbb2212e1ad4c2b34716539ef77beb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:01 +0800 Subject: [PATCH 0207/1461] Port AircraftRenderer to the 26.2 pose stack and culling signature --- .../client/render/AircraftRenderer.java | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java new file mode 100644 index 00000000..b50ad369 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java @@ -0,0 +1,342 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.SearchLight; +import com.jnhprojects.mcheli.content.aircraft.RepellingHook; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.AnimationState; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PartState; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.debug.DebugScreenEntries; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.gizmos.GizmoStyle; +import net.minecraft.gizmos.Gizmos; +import net.minecraft.resources.Identifier; +import net.minecraft.util.LightCoordsUtil; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * Draws any MCHeli vehicle from its MQO/OBJ model, following {@code MCH_RenderAircraft}: the hull, + * the kind's own moving parts (rotors, wings, gun mounts) and then the parts every kind can have. + */ +public class AircraftRenderer extends EntityRenderer { + private static final int HULL_COLOR = 0xFF00E0FF; + private static final int GENERATED_COLOR = 0xFFFFC000; + /** Ropes take the colour of brown wool. */ + private static final Identifier ROPE = Identifier.withDefaultNamespace("textures/block/brown_wool.png"); + private static final float ROPE_WIDTH = 0.03F; + + public AircraftRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 2.0F; + } + + /** + * With F3+B on, outlines the hull boxes next to vanilla's main box: what projectiles hit and + * what other vehicles collide with. The definition's own boxes are cyan, the ones generated + * from the model amber. + */ + private static void showHull(AircraftEntity entity, AircraftDefinition def, float partialTicks) { + List hull = entity.hullBoxes(); + if (hull.size() < 2) { + return; + } + Vec3 offset = entity.getPosition(partialTicks).subtract(entity.position()); + int listed = def.boundingBoxes.size(); + try (Gizmos.TemporaryCollection ignored = Minecraft.getInstance().levelRenderer.collectPerFrameRenderThreadGizmos()) { + // The first is the main box, which vanilla draws itself; then the listed boxes, then the generated ones. + for (int i = 1; i < hull.size(); i++) { + Gizmos.cuboid(hull.get(i).move(offset), GizmoStyle.stroke(i <= listed ? HULL_COLOR : GENERATED_COLOR)); + } + } + } + + @Override + public AircraftRenderState createRenderState() { + return new AircraftRenderState(); + } + + @Override + public void extractRenderState(T entity, AircraftRenderState state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + AircraftDefinition def = entity.definition(); + state.definition = def; + state.texture = entity.texture(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.roll = entity.roll(partialTicks); + if (def == null) { + return; + } + if (Minecraft.getInstance().debugEntries.isCurrentlyEnabled(DebugScreenEntries.ENTITY_HITBOXES)) { + showHull(entity, def, partialTicks); + } + + state.rotorAngle = switch (entity) { + case HelicopterEntity heli -> heli.rotorAngle(partialTicks); + case PlaneEntity plane -> plane.rotorAngle(partialTicks); + default -> 0.0F; + }; + state.canopy = angle(entity.canopy(), partialTicks); + state.landingGear = angle(entity.landingGear(), partialTicks); + state.hatch = angle(entity.hatch(), partialTicks); + if (entity instanceof HelicopterEntity heli && def instanceof HelicopterDefinition heliDef) { + bladeAngles(heli, heliDef, state, partialTicks); + } + state.ropeLength = entity.isRepelling() ? entity.ropeLength() : 0.0F; + state.searchLight = entity.isSearchLightOn(); + if (state.searchLight) { + Entity aimer = entity.searchLightAimer(); + state.searchLightYaw = aimer != null ? aimer.getYRot(partialTicks) : state.yaw; + state.searchLightPitch = aimer != null ? aimer.getXRot(partialTicks) : state.pitch; + } + if (entity instanceof PlaneEntity plane) { + state.nozzle = angle(plane.nozzle(), partialTicks); + state.wing = angle(plane.wing(), partialTicks); + } else { + state.nozzle = 0.0F; + state.wing = 0.0F; + } + + AnimationState anim = entity.animation; + state.throttle = Mth.lerp(partialTicks, anim.prevThrottle, anim.throttle); + state.wheelRoll = Mth.lerp(partialTicks, anim.prevWheelRoll, anim.wheelRoll); + state.steer = Mth.lerp(partialTicks, anim.prevSteer, anim.steer); + for (int i = 0; i < 2; i++) { + state.roller[i] = Mth.lerp(partialTicks, anim.prevRoller[i], anim.roller[i]); + state.track[i] = lerpWrapped(anim.prevTrack[i], anim.track[i], partialTicks); + } + if (state.rotatingParts.length != anim.rotatingParts.length) { + state.rotatingParts = new float[anim.rotatingParts.length]; + } + for (int i = 0; i < anim.rotatingParts.length; i++) { + state.rotatingParts[i] = Mth.lerp(partialTicks, anim.prevRotatingParts[i], anim.rotatingParts[i]); + } + + int seats = def.seatAndRackCount(); + if (state.seatYaw.length != seats) { + state.seatYaw = new float[seats]; + state.seatPitch = new float[seats]; + state.seatOccupied = new boolean[seats]; + } + for (int i = 0; i < seats; i++) { + state.seatYaw[i] = anim.seatYaw(i, partialTicks); + state.seatPitch[i] = anim.seatPitch(i, partialTicks); + state.seatOccupied[i] = entity.occupant(i) != null; + } + Minecraft mc = Minecraft.getInstance(); + state.localSeat = mc.player != null ? entity.seatOf(mc.player) : -1; + state.localFirstPerson = mc.options.getCameraType().isFirstPerson(); + state.glassColor = glassColor(entity, state.lightCoords); + extractWeapons(entity, def, state, partialTicks); + } + + /** + * Glass is drawn without the light map, so shade it by hand: as bright as the brighter of block + * light and the sky light left after night or rain darkens it. + */ + private static int glassColor(AircraftEntity entity, int lightCoords) { + int sky = Math.max(LightCoordsUtil.sky(lightCoords) - entity.level().getSkyDarken(), 0); + int light = Math.max(LightCoordsUtil.block(lightCoords), sky); + int shade = (int) ((0.3F + 0.7F * light / 15.0F) * 255.0F); + return 0xFF000000 | shade << 16 | shade << 8 | shade; + } + + /** {@code renderWeapon}'s inputs: each part's weapon aim, recoil, barrel spin and reload state. */ + private static void extractWeapons(AircraftEntity entity, AircraftDefinition def, AircraftRenderState state, float partialTicks) { + VehicleWeapons weapons = entity.weapons(); + int parts = def.weaponParts.size(); + if (state.weaponParts.length != parts) { + state.weaponParts = new AircraftRenderState.WeaponPartState[parts]; + for (int i = 0; i < parts; i++) { + state.weaponParts[i] = new AircraftRenderState.WeaponPartState(); + } + } + WeaponSet previous = null; + int mount = 0; + for (int i = 0; i < parts; i++) { + WeaponPart part = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState out = state.weaponParts[i]; + WeaponSet set = weapons != null && !part.weapons().isEmpty() ? weapons.byName(part.weapons().getFirst()) : null; + if (set != previous) { + mount = 0; + previous = set; + } + out.hasWeapon = set != null; + if (set == null) { + continue; + } + float yaw = set.rotationYaw; + float prevYaw = set.prevRotationYaw; + if (yaw - prevYaw > 180.0F) { + prevYaw += 360.0F; + } else if (yaw - prevYaw < -180.0F) { + prevYaw -= 360.0F; + } + out.yaw = Mth.lerp(partialTicks, prevYaw, yaw) - set.defaultRotationYaw; + out.pitch = Mth.lerp(partialTicks, set.prevRotationPitch, set.rotationPitch); + out.defaultYaw = set.defaultRotationYaw; + out.turretLag = set.rotationTurretYaw; + out.barrel = Mth.lerp(partialTicks, set.prevBarrel, set.barrel); + // A part shared by several weapons kicks back with whichever fired hardest. + float recoil = set.recoil(0, partialTicks); + for (String name : part.weapons()) { + WeaponSet other = weapons.byName(name); + if (other != null) { + recoil = Math.max(recoil, other.recoil(0, partialTicks)); + } + } + out.recoil = recoil; + out.hidden = part.missile() && weapons.isMountUsed(weapons.indexOf(set.name()), mount); + mount++; + } + if (state.bays.length != def.weaponBays.size()) { + state.bays = new float[def.weaponBays.size()]; + } + for (int i = 0; i < state.bays.length; i++) { + state.bays[i] = weapons != null ? weapons.bayAngle(i, partialTicks) : 0.0F; + } + } + + private static float angle(@Nullable PartState part, float partialTicks) { + return part != null ? part.angle(partialTicks) : 0.0F; + } + + /** Interpolates a value that wraps from 1 back to 0. */ + private static float lerpWrapped(float prev, float current, float partialTicks) { + float d = current - prev; + if (d > 0.5F) { + d -= 1.0F; + } else if (d < -0.5F) { + d += 1.0F; + } + float v = prev + d * partialTicks; + return v - Mth.floor(v); + } + + @Override + protected AABB getBoundingBoxForCulling(T entity) { + // The hit box is far smaller than most models, so cull on a generous box instead. + AircraftDefinition def = entity.definition(); + double reach = def != null ? Math.max(8.0, def.markerWidth * 4.0) : 8.0; + return entity.getBoundingBox().inflate(reach, reach / 2.0, reach); + } + + @Override + public void submit(AircraftRenderState state, PoseStack poseStack, SubmitNodeCollector collector, CameraRenderState camera) { + AircraftDefinition def = state.definition; + Mesh mesh = def != null ? MeshCache.INSTANCE.get(def.folder(), def.name()) : null; + if (mesh != null) { + // Canopies and windshields are semi-transparent texels; PartRenderer draws those + // triangles blended, as the original did, and everything else as cutout. + poseStack.pushPose(); + poseStack.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + poseStack.mulPose(Axis.XP.rotationDegrees(state.pitch)); + poseStack.mulPose(Axis.ZP.rotationDegrees(state.roll)); + new PartRenderer(def, mesh, state, poseStack, collector, texture(def.folder(), state.texture), state.lightCoords, + OverlayTexture.NO_OVERLAY, state.glassColor).submitAll(); + if (state.ropeLength < 0.0F) { + ropes(def, state, poseStack, collector); + } + if (state.searchLight) { + searchLights(def, state, poseStack, collector); + } + poseStack.popPose(); + } + super.submit(state, poseStack, collector, camera); + } + + /** + * {@code renderLight}: each lamp's cone. A turning light follows the crew's aim; a fixed one + * points along the vehicle, and a steering one also turns with the front wheels. + */ + private static void searchLights(AircraftDefinition def, AircraftRenderState state, PoseStack pose, SubmitNodeCollector collector) { + for (SearchLight light : def.searchLights) { + pose.pushPose(); + pose.translate(light.position().x(), light.position().y(), light.position().z()); + if (!light.fixed()) { + pose.mulPose(Axis.YN.rotationDegrees(state.searchLightYaw - state.yaw + light.yaw())); + pose.mulPose(Axis.XP.rotationDegrees(state.searchLightPitch + 90.0F - state.pitch + light.pitch())); + } else { + float steer = light.steering() ? -state.steer * light.steeringRotation() : 0.0F; + pose.mulPose(Axis.YN.rotationDegrees(light.yaw() + steer)); + pose.mulPose(Axis.XP.rotationDegrees(90.0F + light.pitch())); + } + Glow.cone(pose, collector, light.height(), light.width() / 2.0F, light.colorStart(), light.colorEnd()); + pose.popPose(); + } + } + + private static void bladeAngles(HelicopterEntity heli, HelicopterDefinition def, AircraftRenderState state, float partialTicks) { + if (state.bladeAngles.length != def.rotors.size()) { + state.bladeAngles = new float[def.rotors.size()][]; + } + for (int r = 0; r < def.rotors.size(); r++) { + RotorDefinition rotor = def.rotors.get(r); + int blades = Math.max(rotor.blades(), 1); + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + if (state.bladeAngles[r] == null || state.bladeAngles[r].length != blades) { + state.bladeAngles[r] = new float[blades]; + } + for (int b = 0; b < blades; b++) { + state.bladeAngles[r][b] = heli.bladeAngle(r, b, step, partialTicks); + } + } + } + + /** {@code renderRope}: a dark line straight down from each hook, in the vehicle's own frame. */ + private static void ropes(AircraftDefinition def, AircraftRenderState state, PoseStack pose, SubmitNodeCollector collector) { + int light = state.lightCoords; + float length = state.ropeLength; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(ROPE), (p, buffer) -> { + for (RepellingHook hook : def.repellingHooks) { + float x = (float) hook.position().x(); + float y = (float) hook.position().y(); + float z = (float) hook.position().z(); + // Two thin crossed strips, so the rope shows from every side. + ropeStrip(p, buffer, x - ROPE_WIDTH, z, x + ROPE_WIDTH, z, y, length, light); + ropeStrip(p, buffer, x, z - ROPE_WIDTH, x, z + ROPE_WIDTH, y, length, light); + } + }); + } + + private static void ropeStrip(PoseStack.Pose p, VertexConsumer buffer, float x0, float z0, float x1, float z1, float y, float length, int light) { + ropeVertex(p, buffer, x0, y, z0, 0.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y, z1, 1.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y + length, z1, 1.0F, 1.0F, light); + ropeVertex(p, buffer, x0, y + length, z0, 0.0F, 1.0F, light); + } + + private static void ropeVertex(PoseStack.Pose p, VertexConsumer buffer, float x, float y, float z, float u, float v, int light) { + buffer.addVertex(p, x, y, z).setColor(0xFFFFFFFF).setUv(u, v).setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(p, 0.0F, 1.0F, 0.0F); + } + + private static Identifier texture(String folder, String name) { + return RuntimeAssets.texture(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + folder + "/" + name + ".png")); + } +} From 090267e8ab07afc98c3d9e76938a41f0b90df7d7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:02 +0800 Subject: [PATCH 0208/1461] Port ChainRenderer to the 26.2 pose stack and culling signature --- .../mcheli/client/render/ChainRenderer.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java new file mode 100644 index 00000000..5d7f43b7 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java @@ -0,0 +1,93 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +/** + * {@code MCH_RenderChain}: a line of links from the towed end to the end that pulls, one link + * every 0.95 blocks, each turned along the chain. + */ +public class ChainRenderer extends EntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/chain.png"); + private static final double LINK = 0.95; + private static final int GREY = 0xFF808080; + + public ChainRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + /** Towed end, relative to the chain entity. */ + Vec3 from = Vec3.ZERO; + /** From the towed end to the end that pulls. */ + Vec3 span = Vec3.ZERO; + } + + /** The chain hangs over the end that pulls but reaches to the other end; keep both in view. */ + @Override + protected AABB getBoundingBoxForCulling(ChainEntity chain) { + AABB box = super.getBoundingBoxForCulling(chain); + Entity towed = chain.towed(); + return towed != null ? box.minmax(towed.getBoundingBox()) : box; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ChainEntity chain, State state, float partialTicks) { + super.extractRenderState(chain, state, partialTicks); + Entity tow = chain.tow(); + Entity towed = chain.towed(); + if (tow == null || towed == null) { + state.span = Vec3.ZERO; + return; + } + Vec3 here = chain.getPosition(partialTicks); + Vec3 start = towed.getPosition(partialTicks); + state.from = start.subtract(here); + state.span = tow.getPosition(partialTicks).subtract(start); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("", "chain"); + double length = state.span.length(); + if (mesh != null && length > LINK) { + Vec3 step = state.span.scale(LINK / length); + float yaw = (float) Math.toDegrees(Math.atan2(step.z, step.x)); + float pitch = (float) Math.toDegrees(Math.atan2(step.y, Math.sqrt(step.x * step.x + step.z * step.z))); + int light = state.lightCoords; + pose.pushPose(); + pose.translate(state.from); + for (double left = length; left > LINK; left -= LINK) { + pose.translate(step); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-yaw)); + pose.mulPose(Axis.ZP.rotationDegrees(pitch)); + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, GREY)); + pose.popPose(); + } + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From cde090437f6e094c09e5d9c6e85eefd5ad14b244 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:02 +0800 Subject: [PATCH 0209/1461] Port DraftingTableRenderer to the 26.2 pose stack --- .../client/render/DraftingTableRenderer.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java new file mode 100644 index 00000000..665893c1 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** {@code MCH_DraftingTableRenderer}: the original model, turned to the block's eighth of a circle. */ +public class DraftingTableRenderer implements BlockEntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/block/drafting_table.png"); + /** The original greyed the table a little. */ + private static final int TINT = 0xFFBFBFBF; + + public DraftingTableRenderer(BlockEntityRendererProvider.Context context) { + } + + public static class State extends BlockEntityRenderState { + int rotation; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(DraftingTableBlockEntity table, State state, float partialTicks, Vec3 cameraPosition, + ModelFeatureRenderer.@Nullable CrumblingOverlay breakProgress) { + BlockEntityRenderer.super.extractRenderState(table, state, partialTicks, cameraPosition, breakProgress); + state.rotation = table.getBlockState().getValue(DraftingTableBlock.ROTATION); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("blocks", "drafting_table"); + if (mesh == null) { + return; + } + pose.pushPose(); + pose.translate(0.5F, 0.0F, 0.5F); + pose.mulPose(Axis.YP.rotationDegrees(-state.rotation * 45.0F + 180.0F)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, TINT)); + pose.popPose(); + } +} From de48517c353a56c93a10399f201f9329c8e3bb1e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:03 +0800 Subject: [PATCH 0210/1461] Port EquipmentRenderer to the 26.2 pose stack --- .../client/render/EquipmentRenderer.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java new file mode 100644 index 00000000..80ca1e3b --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; + +/** + * Equipment drawn as one model from the models root with the texture of the same name, turned to + * the entity's heading: the container, {@code MCH_RenderContainer}. + */ +public class EquipmentRenderer extends EntityRenderer { + private final String model; + private final Identifier texture; + private final float yOffset; + private final int tint; + + /** + * @param model the file name under {@code models/}, without extension + * @param yOffset how far the model sits below the entity's position + * @param tint ARGB the model is multiplied by; the original greyed the container a little + */ + public EquipmentRenderer(EntityRendererProvider.Context context, String model, float yOffset, int tint, float shadow) { + super(context); + this.model = model; + this.texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + model + ".png"); + this.yOffset = yOffset; + this.tint = tint; + this.shadowRadius = shadow; + } + + public static class State extends EntityRenderState { + float yaw; + float pitch; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(T entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("", model); + if (mesh != null) { + pose.pushPose(); + pose.translate(0.0F, -yOffset, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, tint)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From 2ca909fd68a96b5360c07033616eeaecf9887a40 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:03 +0800 Subject: [PATCH 0211/1461] Add client/render/FlareRenderer.java to the 26.2 shared sources --- .../mcheli/client/render/FlareRenderer.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java new file mode 100644 index 00000000..66cffb45 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and the burning debris of a crash were only their particle trails. Not in the original: + * a burning flare now glows, which is what makes a flare salvo light up the night sky. Smoke + * grenades stay invisible. + */ +public class FlareRenderer extends EntityRenderer { + private static final float RADIUS = 1.4F; + + public FlareRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + float glow; + float flicker = 1.0F; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(FlareEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.glow = entity.isSmoke() || entity.isInWater() ? 0.0F : Glow.strength(entity.level(), entity.blockPosition()); + // A cheap waver keyed to the entity, so neighbouring flares do not pulse in step. + state.flicker = 0.8F + 0.2F * (float) Math.sin((entity.tickCount + partialTicks) * 1.7F + entity.getId()); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Glow.halo(pose, collector, camera, Vec3.ZERO, RADIUS * state.flicker, GlowParticleOptions.FIRE, state.glow); + Glow.halo(pose, collector, camera, Vec3.ZERO, RADIUS * 0.35F, GlowParticleOptions.FLASH, state.glow); + super.submit(state, pose, collector, camera); + } +} From 70ae75eeb4dcde1e8b05c7ca251925882e171147 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:04 +0800 Subject: [PATCH 0212/1461] Add client/render/GlassSplit.java to the 26.2 shared sources --- .../mcheli/client/render/GlassSplit.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java new file mode 100644 index 00000000..d2a46857 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.resources.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Sorts a model's triangles into opaque ones and see-through glass, by the alpha of the texture + * under each triangle. + * + *

Drawing a whole vehicle translucent made its glass write depth before the game drew water, + * so water behind a canopy vanished. Opaque triangles now draw as cutout, like any entity, and + * only the glass draws translucent, through a type that leaves the depth buffer alone. + */ +final class GlassSplit { + static final GlassSplit INSTANCE = new GlassSplit(); + private static final Logger LOGGER = LoggerFactory.getLogger(GlassSplit.class); + /** Texels between these alphas are glass; fully clear ones are cut out, solid ones opaque. */ + private static final int MIN_GLASS_ALPHA = 10; + private static final int MAX_GLASS_ALPHA = 245; + + record Split(List opaque, List glass) { + static final Split EMPTY = new Split(List.of(), List.of()); + } + + private record Alpha(int width, int height, byte[] values) { + int at(float u, float v) { + int x = Math.floorMod((int) Math.floor(u * width), width); + int y = Math.floorMod((int) Math.floor(v * height), height); + return values[y * width + x] & 0xFF; + } + } + + private final Map> textures = new HashMap<>(); + private final Map, Map> splits = new IdentityHashMap<>(); + private final Map, Map, List>> buckets = new IdentityHashMap<>(); + + private GlassSplit() { + } + + /** Called from the render thread only. */ + Split split(List groups, Identifier texture) { + if (groups.isEmpty()) { + return Split.EMPTY; + } + return splits.computeIfAbsent(groups, g -> new HashMap<>()).computeIfAbsent(texture, t -> compute(groups, t)); + } + + /** + * A part's objects by the texture each draws with, for a vehicle with {@code PartTexture} lines. + * The lists stay the same objects from frame to frame, so {@link #split} can remember them too. + * Called from the render thread only. + */ + List buckets(List groups, Map overrides) { + return buckets.computeIfAbsent(groups, g -> new HashMap<>()) + .computeIfAbsent(Map.copyOf(overrides), o -> PartTextures.assign(groups, o)); + } + + void clear() { + textures.clear(); + splits.clear(); + buckets.clear(); + } + + private Split compute(List groups, Identifier texture) { + Alpha alpha = textures.computeIfAbsent(texture, GlassSplit::load).orElse(null); + if (alpha == null) { + return new Split(groups, List.of()); + } + List opaque = new ArrayList<>(); + List glass = new ArrayList<>(); + for (MeshGroup group : groups) { + split(group, alpha, opaque, glass); + } + return new Split(List.copyOf(opaque), List.copyOf(glass)); + } + + private static void split(MeshGroup group, Alpha alpha, List opaque, List glass) { + float[] uv = group.uvs(); + int triangles = group.vertexCount() / 3; + boolean[] isGlass = new boolean[triangles]; + int glassCount = 0; + for (int t = 0; t < triangles; t++) { + int i = t * 6; + float u = (uv[i] + uv[i + 2] + uv[i + 4]) / 3.0F; + float v = (uv[i + 1] + uv[i + 3] + uv[i + 5]) / 3.0F; + int a = alpha.at(u, v); + if (a > MIN_GLASS_ALPHA && a < MAX_GLASS_ALPHA) { + isGlass[t] = true; + glassCount++; + } + } + if (glassCount == 0) { + opaque.add(group); + } else if (glassCount == triangles) { + glass.add(group); + } else { + opaque.add(pick(group, isGlass, false, triangles - glassCount)); + glass.add(pick(group, isGlass, true, glassCount)); + } + } + + private static MeshGroup pick(MeshGroup group, boolean[] isGlass, boolean wanted, int count) { + float[] p = new float[count * 9]; + float[] n = new float[count * 9]; + float[] uv = new float[count * 6]; + int out = 0; + for (int t = 0; t < isGlass.length; t++) { + if (isGlass[t] != wanted) { + continue; + } + System.arraycopy(group.positions(), t * 9, p, out * 9, 9); + System.arraycopy(group.normals(), t * 9, n, out * 9, 9); + System.arraycopy(group.uvs(), t * 6, uv, out * 6, 6); + out++; + } + return new MeshGroup(group.name(), p, n, uv); + } + + private static Optional load(Identifier texture) { + try (InputStream in = RuntimeAssets.open(texture)) { + if (in == null) { + return Optional.empty(); + } + return Optional.of(read(in)); + } catch (IOException | RuntimeException e) { + LOGGER.warn("Could not read {} to find its glass; drawing it opaque", texture, e); + return Optional.empty(); + } + } + + private static Alpha read(InputStream in) throws IOException { + try (NativeImage image = NativeImage.read(in)) { + int w = image.getWidth(); + int h = image.getHeight(); + byte[] values = new byte[w * h]; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + values[y * w + x] = (byte) (image.getPixel(x, y) >>> 24); + } + } + return new Alpha(w, h, values); + } + } +} From bc145af0192e62ba9c44e861eafc74ac615ac2fc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:04 +0800 Subject: [PATCH 0213/1461] Build the glow pipelines from what 26.2 offers, without OIT --- .../mcheli/client/render/Glow.java | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java new file mode 100644 index 00000000..dab6b08e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java @@ -0,0 +1,213 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.effect.NightGlow; +import com.mojang.blaze3d.PrimitiveTopology; +import com.mojang.blaze3d.pipeline.BlendFunction; +import com.mojang.blaze3d.pipeline.ColorTargetState; +import com.mojang.blaze3d.pipeline.DepthStencilState; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.platform.CompareOp; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.client.renderer.BindGroupLayouts; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.rendertype.RenderSetup; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.util.ARGB; +import net.minecraft.util.LightCoordsUtil; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.phys.Vec3; + +/** + * Not in the original: light that adds to the scene instead of covering it, for tracers, rocket + * motors, flares and fireballs. Everything here blends additively and leaves the depth buffer + * alone, so overlapping glows brighten each other and never cut holes in the smoke behind them. + * With order independent transparency on, the vanilla translucent pipelines stand in. + */ +public final class Glow { + /** A white disc fading to nothing at the rim; also the {@code mcheli:glow} particle sprite. */ + public static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/particle/glow.png"); + + private static final DepthStencilState TEST_ONLY = new DepthStencilState(CompareOp.GREATER_THAN_OR_EQUAL, false); + + private static final RenderPipeline PARTICLE_PIPELINE = RenderPipeline.builder(RenderPipelines.PARTICLE_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/glow_particle")) + .withColorTargetState(new ColorTargetState(BlendFunction.LIGHTNING)) + .withDepthStencilState(TEST_ONLY) + .build(); + + /** + * Vanilla's translucent particles with depth writes off. A puff's clear edge would otherwise + * hide the puffs drawn after it, and a smoke cloud is mostly overlapping puffs. + */ + private static final RenderPipeline SMOKE_PIPELINE = RenderPipeline.builder(RenderPipelines.PARTICLE_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/smoke_particle")) + .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) + .withDepthStencilState(TEST_ONLY) + .build(); + + private static final RenderPipeline ENTITY_PIPELINE = RenderPipeline.builder(RenderPipelines.MATRICES_FOG_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/glow")) + .withVertexShader("core/entity") + .withFragmentShader("core/entity") + .withShaderDefine("EMISSIVE") + .withShaderDefine("NO_OVERLAY") + .withShaderDefine("NO_CARDINAL_LIGHTING") + .withBindGroupLayout(BindGroupLayouts.SAMPLER0) + .withVertexBinding(0, DefaultVertexFormat.ENTITY) + .withPrimitiveTopology(PrimitiveTopology.QUADS) + .withColorTargetState(new ColorTargetState(BlendFunction.LIGHTNING)) + .withDepthStencilState(TEST_ONLY) + .build(); + + /** The particle layer for {@code mcheli:glow}. */ + @SuppressWarnings("deprecation") // Vanilla's own particle layers still name the atlas this way. + public static final SingleQuadParticle.Layer PARTICLE_LAYER = new SingleQuadParticle.Layer( + true, TextureAtlas.LOCATION_PARTICLES, PARTICLE_PIPELINE); + + /** The particle layer for {@code mcheli:smoke}. */ + @SuppressWarnings("deprecation") + public static final SingleQuadParticle.Layer SMOKE_LAYER = new SingleQuadParticle.Layer( + true, TextureAtlas.LOCATION_PARTICLES, SMOKE_PIPELINE); + + private static final RenderType HALO = RenderType.create(MCHeli.MOD_ID + "_glow", RenderSetup.builder(ENTITY_PIPELINE) + .withTexture("Sampler0", TEXTURE) + .sortOnUpload() + .createRenderSetup()); + + private static final int FULL_BRIGHT = LightCoordsUtil.FULL_BRIGHT; + + private Glow() { + } + + /** + * Hands our pipelines to Iris, so a shader pack draws them with a matching program: the halos + * and tracers as emissive entities, the glow and smoke particles as translucent particles. + * Called once the mods are loaded; without Iris it does nothing. + */ + public static void assignShaderPrograms() { + IrisCompat.assign(ENTITY_PIPELINE, "EMISSIVE_ENTITIES"); + IrisCompat.assign(PARTICLE_PIPELINE, "PARTICLES_TRANSLUCENT"); + IrisCompat.assign(SMOKE_PIPELINE, "PARTICLES_TRANSLUCENT"); + } + + /** + * How strongly something at {@code pos} glows: faint by day, full at night, in caves and + * under water. 0 when the player turned the glow off. + */ + public static float strength(Level level, BlockPos pos) { + if (!ClientConfig.nightGlow()) { + return 0.0F; + } + return NightGlow.strength(level.getBrightness(LightLayer.BLOCK, pos), level.getBrightness(LightLayer.SKY, pos), level.getSkyDarken()); + } + + /** + * A disc of light facing the camera, centred {@code offset} away from the current pose. + * + * @param color RGB; {@code strength} becomes the alpha, which scales the light added + */ + public static void halo(PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera, Vec3 offset, float radius, int color, + float strength) { + if (strength <= 0.0F || radius <= 0.0F) { + return; + } + int argb = ARGB.color(alpha(strength), color); + pose.pushPose(); + pose.translate(offset); + pose.mulPose(camera.orientation); + collector.submitCustomGeometry(pose, HALO, (p, buffer) -> { + // Both windings, so culling never hides it whichever way the quad ends up facing. + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + }); + pose.popPose(); + } + + /** + * A tracer: a ribbon of light from the current pose back along {@code tail}, turned to face + * the camera, bright at the head and fading out towards the end. + * + * @param toCamera from the camera to the head, so the ribbon can be turned side on to the view + */ + public static void streak(PoseStack pose, SubmitNodeCollector collector, Vec3 toCamera, Vec3 tail, float width, int color, float strength) { + if (strength <= 0.0F || tail.lengthSqr() < 1.0E-4) { + return; + } + Vec3 side = tail.cross(toCamera); + if (side.lengthSqr() < 1.0E-8) { + // Seen exactly end on; the halo at the head covers this. + return; + } + side = side.normalize().scale(width); + int head = ARGB.color(alpha(strength), color); + int end = ARGB.color(0, color); + float sx = (float) side.x; + float sy = (float) side.y; + float sz = (float) side.z; + float tx = (float) tail.x; + float ty = (float) tail.y; + float tz = (float) tail.z; + collector.submitCustomGeometry(pose, HALO, (p, buffer) -> { + // The middle row of the disc texture gives the ribbon soft edges. + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + }); + } + + /** + * A search light's beam, {@code renderLight}: a cone {@code height} long along +Y to a disc + * {@code radius} across, fading from {@code start} at the lamp to {@code end} at the rim (ARGB). + */ + public static void cone(PoseStack pose, SubmitNodeCollector collector, float height, float radius, int start, int end) { + collector.submitCustomGeometry(pose, HALO, (p, buffer) -> { + int segments = 24; + for (int i = 0; i < segments; i++) { + float a0 = i * Mth.TWO_PI / segments; + float a1 = (i + 1) * Mth.TWO_PI / segments; + // A quad with the apex twice is the fan's triangle; the texture's bright middle colours it. + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, Mth.sin(a0) * radius, height, Mth.cos(a0) * radius, 0.5F, 0.5F, end); + vertex(buffer, p, Mth.sin(a1) * radius, height, Mth.cos(a1) * radius, 0.5F, 0.5F, end); + } + }); + } + + private static int alpha(float strength) { + return Math.clamp(Math.round(strength * 255.0F), 0, 255); + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float x, float y, float z, float u, float v, int argb) { + buffer.addVertex(pose, x, y, z) + .setColor(argb) + .setUv(u, v) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(FULL_BRIGHT) + .setNormal(pose, 0.0F, 1.0F, 0.0F); + } +} From a8fb138e2e456fcf1f48f75b8676207250a09513 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:05 +0800 Subject: [PATCH 0214/1461] Port GltdRenderer to the 26.2 pose stack --- .../mcheli/client/render/GltdRenderer.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java new file mode 100644 index 00000000..02cc82ba --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * {@code MCH_RenderGLTD}: the tripod, its head turned to where the viewer aims, and a faint red + * laser out of the head while someone uses it. The viewer's own head part is left out in first + * person, where it would fill the view. + */ +public class GltdRenderer extends EntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gltd.png"); + private static final float HEAD_PIVOT = 0.45F; + private static final double LASER_LENGTH = 100.0; + private static final int LASER = 0xFF2020; + + public GltdRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + public static class State extends EntityRenderState { + float yaw; + float headYaw; + float headPitch; + boolean drawHead = true; + boolean laser; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(GltdEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + Entity viewer = entity.getFirstPassenger(); + Minecraft mc = Minecraft.getInstance(); + state.laser = viewer != null; + state.drawHead = viewer == null || viewer != mc.player || !mc.options.getCameraType().isFirstPerson(); + state.headYaw = viewer != null ? viewer.getYRot(partialTicks) : state.yaw; + state.headPitch = viewer != null ? viewer.getXRot(partialTicks) : 0.0F; + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("", "gltd"); + if (mesh != null) { + int light = state.lightCoords; + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + draw(mesh.part("$body"), pose, collector, light); + pose.popPose(); + if (state.drawHead) { + pose.pushPose(); + pose.translate(0.0F, HEAD_PIVOT, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-state.headYaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.headPitch)); + pose.translate(0.0F, -HEAD_PIVOT, 0.0F); + draw(mesh.part("$head"), pose, collector, light); + pose.popPose(); + } + } + if (state.laser) { + float yaw = state.headYaw * Mth.DEG_TO_RAD; + float pitch = state.headPitch * Mth.DEG_TO_RAD; + Vec3 look = new Vec3(-Mth.sin(yaw) * Mth.cos(pitch), -Mth.sin(pitch), Mth.cos(yaw) * Mth.cos(pitch)); + Vec3 head = new Vec3(0.0, HEAD_PIVOT + 0.2, 0.0); + Vec3 toCamera = new Vec3(state.x, state.y, state.z).add(head).subtract(camera.pos); + pose.pushPose(); + pose.translate(head); + // Brightest at the tripod, fading out along the beam. + Glow.streak(pose, collector, toCamera, look.scale(LASER_LENGTH), 0.02F, LASER, 0.25F); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } + + private static void draw(List groups, PoseStack pose, SubmitNodeCollector collector, int light) { + if (!groups.isEmpty()) { + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(TEXTURE), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY)); + } + } +} From 7e499e079029614adf3404263de6628332d0c530 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:05 +0800 Subject: [PATCH 0215/1461] Point IrisCompat at the 26.2 pipeline package --- .../mcheli/client/render/IrisCompat.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java new file mode 100644 index 00000000..3803102e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.pipeline.RenderPipeline; + +import java.lang.reflect.Method; + +/** + * Tells Iris which shader program draws our own render pipelines. Iris only knows the vanilla + * pipelines and draws any other with a program whose vertex layout does not match, which throws + * the vertices far off: the glows turn into long coloured lines running to the horizon. Iris is + * optional, so its API is looked up by reflection instead of compiled against. + */ +final class IrisCompat { + private static final String API = "net.irisshaders.iris.api.v0.IrisApi"; + private static final String PROGRAM = "net.irisshaders.iris.api.v0.IrisProgram"; + + private IrisCompat() { + } + + /** + * Assigns {@code pipeline} to the Iris program named {@code program}, such as + * {@code EMISSIVE_ENTITIES}. Does nothing without Iris. + * + * @return whether Iris took it + */ + static boolean assign(RenderPipeline pipeline, String program) { + Class api; + try { + api = Class.forName(API); + } catch (ClassNotFoundException absent) { + MCHeli.LOGGER.debug("Iris is not installed, {} keeps its own shader", pipeline.getLocation()); + return false; + } + try { + Class programs = Class.forName(PROGRAM); + Object instance = api.getMethod("getInstance").invoke(null); + Object target = programs.getField(program).get(null); + Method assign = api.getMethod("assignPipeline", RenderPipeline.class, programs); + assign.invoke(instance, pipeline, target); + MCHeli.LOGGER.debug("Iris draws {} with its {} program", pipeline.getLocation(), program); + return true; + } catch (ReflectiveOperationException | LinkageError e) { + // An Iris without this API: glows may look wrong under a shader pack, nothing worse. + MCHeli.LOGGER.warn("Could not hand {} to Iris, glows may look wrong with shaders: {}", pipeline.getLocation(), e.toString()); + return false; + } + } +} From 20f0de1a362892da0da80042cc51e50081d655e1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:06 +0800 Subject: [PATCH 0216/1461] Add client/render/MeshCache.java to the 26.2 shared sources --- .../mcheli/client/render/MeshCache.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java new file mode 100644 index 00000000..57dad7e5 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshLoader; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Lazily loads vehicle models, from the resource packs (so they can replace them) or else from the + * runtime roots of {@link RuntimeAssets}. Models are parsed the first time a vehicle of that kind + * is drawn, and dropped on resource reload and on content reload ({@link #clear}). + */ +public final class MeshCache implements ResourceManagerReloadListener { + public static final MeshCache INSTANCE = new MeshCache(); + private static final Logger LOGGER = LoggerFactory.getLogger(MeshCache.class); + private static final String[] EXTENSIONS = {".mqo", ".obj"}; + + private final Map> meshes = new ConcurrentHashMap<>(); + + private MeshCache() { + } + + /** + * @param folder content folder, for example {@code helicopters}, or empty for the models root + * @param name definition name + * @return the mesh, or {@code null} if no model exists or it failed to load + */ + public @Nullable Mesh get(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, true)).orElse(null); + } + + /** Like {@link #get}, for optional models such as separate part files; a missing one is not logged. */ + public @Nullable Mesh find(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, false)).orElse(null); + } + + private static Optional load(String folder, String name, boolean required) { + for (String extension : EXTENSIONS) { + Identifier id = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, + (folder.isEmpty() ? "models/" : "models/" + folder + "/") + name + extension); + try (InputStream in = RuntimeAssets.open(id)) { + if (in == null) { + continue; + } + Mesh mesh = MeshLoader.load(id.toString(), in); + LOGGER.debug("Loaded {}", mesh); + return Optional.of(mesh); + } catch (IOException | RuntimeException e) { + LOGGER.error("Failed to load model {}", id, e); + return Optional.empty(); + } + } + if (required) { + LOGGER.warn("No model found for {}/{}", folder, name); + } + return Optional.empty(); + } + + /** Forgets every model and glass split, so the next frame reads them again from disk. */ + public void clear() { + meshes.clear(); + GlassSplit.INSTANCE.clear(); + } + + @Override + public void onResourceManagerReload(ResourceManager resourceManager) { + clear(); + RuntimeAssets.clearTextures(); + } +} From 9506b1bbe3b6f5a324cf2dd3a7af7d237582a6ab Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:06 +0800 Subject: [PATCH 0217/1461] Add client/render/MeshRenderer.java to the 26.2 shared sources --- .../mcheli/client/render/MeshRenderer.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java new file mode 100644 index 00000000..47cc66e3 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java @@ -0,0 +1,88 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.List; + +/** Streams {@link MeshGroup} triangles into an entity render type. */ +final class MeshRenderer { + private static final int WHITE = 0xFFFFFFFF; + + private MeshRenderer() { + } + + /** + * Entity render types draw quads, so each triangle is sent as a quad whose last corner repeats + * the third one. The GPU discards the degenerate half for free. + */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay) { + emit(groups, pose, buffer, light, overlay, WHITE); + } + + /** As above, tinted with an ARGB colour, for bullets drawn in their {@code BulletColor}. */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + /** + * As above, but only the triangles that face the camera. No vanilla type is both culled and + * free of depth writes, so glass is culled here. Back to back glass faces would otherwise sort + * in an arbitrary order and swap places as the view moves. + * + *

Entities are submitted in world-aligned space with the camera at the origin, so a front + * face (counter-clockwise, as in the original's GL setup) is one whose normal points back + * towards the origin. + */ + static void emitFrontFaces(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + Matrix4f m = pose.pose(); + Vector3f a = new Vector3f(); + Vector3f b = new Vector3f(); + Vector3f c = new Vector3f(); + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + m.transformPosition(p[v * 3], p[v * 3 + 1], p[v * 3 + 2], a); + m.transformPosition(p[v * 3 + 3], p[v * 3 + 4], p[v * 3 + 5], b); + m.transformPosition(p[v * 3 + 6], p[v * 3 + 7], p[v * 3 + 8], c); + b.sub(a); + c.sub(a); + if (b.cross(c).dot(a) >= 0.0F) { + continue; + } + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float[] p, float[] n, float[] uv, int i, int light, int overlay, int color) { + int a = i * 3; + int t = i * 2; + buffer.addVertex(pose, p[a], p[a + 1], p[a + 2]) + .setColor(color) + .setUv(uv[t], uv[t + 1]) + .setOverlay(overlay) + .setLight(light) + .setNormal(pose, n[a], n[a + 1], n[a + 2]); + } +} From 2b518f15560fc2de718042458dcbae3ebcadc004 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:07 +0800 Subject: [PATCH 0218/1461] Port ParachuteRenderer to the 26.2 pose stack --- .../client/render/ParachuteRenderer.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java new file mode 100644 index 00000000..15ff27a2 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +import java.util.List; + +/** + * {@code MCH_RenderParachute}: the item's round canopy, or the ejection seat with its canopy, + * from the models the import brought in. + */ +public class ParachuteRenderer extends EntityRenderer { + private static final Identifier ROUND = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute1.png"); + private static final Identifier SEAT = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute2.png"); + + public ParachuteRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + public static class State extends EntityRenderState { + float yaw; + ParachuteEntity.Kind kind = ParachuteEntity.Kind.ITEM; + boolean open; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ParachuteEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.kind = entity.kind(); + state.open = entity.isOpen(); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + List groups = groups(state); + if (!groups.isEmpty()) { + Identifier texture = state.kind == ParachuteEntity.Kind.ITEM ? ROUND : SEAT; + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } + + private static List groups(State state) { + if (state.kind == ParachuteEntity.Kind.ITEM) { + Mesh mesh = MeshCache.INSTANCE.get("", "parachute1"); + return mesh != null ? mesh.groups() : List.of(); + } + Mesh mesh = MeshCache.INSTANCE.get("", "parachute2"); + boolean canopy = state.kind == ParachuteEntity.Kind.DROP || state.open; + return mesh != null ? mesh.part(canopy ? "$parachute" : "$seat") : List.of(); + } +} From 3cf46530770dd56b2745ae430c3c88985c12b45e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:07 +0800 Subject: [PATCH 0219/1461] Port PartRenderer to the 26.2 pose stack --- .../mcheli/client/render/PartRenderer.java | 554 ++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java new file mode 100644 index 00000000..d6eefbe0 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java @@ -0,0 +1,554 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.CameraPart; +import com.jnhprojects.mcheli.content.aircraft.part.CrawlerTrack; +import com.jnhprojects.mcheli.content.aircraft.part.LandingGearPart; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.content.aircraft.part.PropellerPart; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import com.jnhprojects.mcheli.content.aircraft.part.ThrottlePart; +import com.jnhprojects.mcheli.content.aircraft.part.TrackRoller; +import com.jnhprojects.mcheli.content.aircraft.part.VehiclePart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.content.aircraft.part.WheelPart; +import com.jnhprojects.mcheli.content.aircraft.part.WingPart; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import org.joml.Vector3f; + +import java.util.List; + +/** + * Submits a vehicle's hull and moving parts for one frame. A port of the part routines in + * {@code MCH_RenderAircraft} and the per kind renderers; every transform is applied in hull space + * on top of the pose the caller already set up. + * + *

A part is looked up as {@code $name} inside the vehicle model, or as a separate + * {@code _} model file, the two places the original accepted. + */ +final class PartRenderer { + private static final Vector3 X_AXIS = new Vector3(1, 0, 0); + private static final Vector3 DOWN_AXIS = new Vector3(0, -1, 0); + + private final AircraftDefinition def; + private final Mesh mesh; + private final AircraftRenderState state; + private final PoseStack pose; + private final SubmitNodeCollector collector; + private final Identifier texture; + private final RenderType opaqueType; + private final RenderType glassType; + private final int light; + private final int overlay; + /** Glass draws unlit, so its colour carries the brightness of where the vehicle stands. */ + private final int glassColor; + + PartRenderer(AircraftDefinition def, Mesh mesh, AircraftRenderState state, PoseStack pose, SubmitNodeCollector collector, + Identifier texture, int light, int overlay, int glassColor) { + this.def = def; + this.mesh = mesh; + this.state = state; + this.pose = pose; + this.collector = collector; + this.texture = texture; + // Culled, as the original drew it. The models are full of back to back faces for thin + // panels, and drawing both sides of those makes them fight for the same depth. + this.opaqueType = RenderTypes.entityCutoutCull(texture); + // The one entity translucent type that does not write depth, so water behind glass still + // draws. It does not cull, so MeshRenderer drops the back faces itself. + this.glassType = RenderTypes.entityTranslucentEmissive(texture); + this.light = light; + this.overlay = overlay; + this.glassColor = glassColor; + } + + void submitAll() { + switch (def) { + case PlaneDefinition plane -> { + nozzles(plane); + wings(plane); + propellers(plane); + hull(); + } + case HelicopterDefinition heli -> { + hull(); + rotors(heli); + } + case VehicleDefinition vehicle -> { + hull(); + for (VehiclePart part : vehicle.parts) { + vehiclePart(vehicle, part); + } + } + default -> hull(); + } + weapons(); + rotatingParts(); + movingParts(def.hatches, state.hatch); + trackRollers(); + crawlerTracks(); + steeringWheels(); + wheels(); + throttles(); + cameras(); + landingGear(); + weaponBays(); + movingParts(def.canopies, state.canopy); + } + + /** + * The body and, as a {@code $} object of its own so it can have its own {@code PartTexture}, + * the cockpit. A model without {@code $body} draws as one piece, cockpit included, and no original + * model has a {@code $cockpit}. + */ + private void hull() { + submit(mesh.body()); + if (mesh.contains("$body")) { + submit(mesh.part("$cockpit")); + } + } + + // Kind specific parts + + private void rotors(HelicopterDefinition heli) { + for (int r = 0; r < heli.rotors.size(); r++) { + RotorDefinition rotor = heli.rotors.get(r); + List blade = part(rotor.part()); + if (blade.isEmpty() || isZero(rotor.axis())) { + continue; + } + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + int blades = Math.max(rotor.blades(), 1); + for (int i = 0; i < blades; i++) { + pose.pushPose(); + Vector3 p = rotor.position(); + translate(p, 1.0F); + float[] angles = r < state.bladeAngles.length ? state.bladeAngles[r] : null; + rotate(angles != null && i < angles.length ? angles[i] : state.rotorAngle + i * step, rotor.axis()); + if (!rotor.legacyPivot()) { + translate(p, -1.0F); + } + submit(blade); + pose.popPose(); + } + } + } + + private void nozzles(PlaneDefinition plane) { + for (MovingPart nozzle : plane.nozzles) { + pose.pushPose(); + rotateAround(nozzle.position(), state.nozzle, nozzle.axis()); + submit(part(nozzle.part())); + pose.popPose(); + } + } + + private void wings(PlaneDefinition plane) { + for (WingPart wing : plane.wings) { + pose.pushPose(); + rotateAround(wing.position(), state.wing * wing.factor(), wing.axis()); + submit(part(wing.part())); + for (MovingPart pylon : wing.pylons()) { + pose.pushPose(); + rotateAround(pylon.position(), state.wing * pylon.factor(), pylon.axis()); + submit(part(pylon.part())); + pose.popPose(); + } + pose.popPose(); + } + } + + /** Tilt rotors follow the nozzle state, the blades spin with the engine. */ + private void propellers(PlaneDefinition plane) { + if (!plane.hasNozzleState()) { + return; + } + for (PropellerPart rotor : plane.propellers) { + pose.pushPose(); + rotateAround(rotor.position(), state.nozzle * rotor.factor(), rotor.axis()); + submit(part(rotor.part())); + for (PropellerPart.Blade blade : rotor.blades()) { + List groups = part(blade.part()); + pose.pushPose(); + rotateAround(blade.position(), state.rotorAngle, blade.axis()); + for (int i = 0; i < blade.count(); i++) { + rotateAround(blade.position(), blade.step(), blade.axis()); + submit(groups); + } + pose.popPose(); + } + pose.popPose(); + } + } + + /** {@code MCH_RenderVehicle.drawPart}: mount parts turn with the gunner's view. */ + private void vehiclePart(VehicleDefinition vehicle, VehiclePart part) { + pose.pushPose(); + if (part.yaw() || part.pitch() || part.type() == VehiclePart.TYPE_ROTATING_BARREL) { + translate(part.position(), 1.0F); + if (part.yaw()) { + rotate(seatYaw(0), DOWN_AXIS); + } + if (part.pitch()) { + rotate(Mth.clamp(seatPitch(0), vehicle.minRotationPitch, vehicle.maxRotationPitch), X_AXIS); + } + translate(part.position(), -1.0F); + } + for (VehiclePart child : part.children()) { + vehiclePart(vehicle, child); + } + boolean gunnerLooking = state.localSeat == 0 && state.localFirstPerson; + if (part.drawFirstPerson() || !gunnerLooking) { + submit(part(part.part())); + } + pose.popPose(); + } + + // Parts every kind can have + + /** + * Gun mounts, turrets and missiles, {@code renderWeapon}. Each follows its weapon's aim, kicks + * back as it fires, spins its barrels and, for missiles, disappears until reloaded. A part + * whose weapon is missing follows the view of the seat that would fire it. + */ + private void weapons() { + float turretYaw = seatYaw(0); + for (int i = 0; i < def.weaponParts.size(); i++) { + WeaponPart w = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState ws = i < state.weaponParts.length ? state.weaponParts[i] : null; + boolean live = ws != null && ws.hasWeapon; + WeaponMount mount = mount(w); + int seat = mount != null ? mount.seatIndex() : 0; + if (w.hideInFirstPerson() && state.localFirstPerson && state.localSeat == seat) { + continue; + } + float defaultYaw = live ? ws.defaultYaw : mount != null ? mount.defaultYaw() : 0.0F; + float yaw; + float pitch; + if (live) { + yaw = ws.yaw; + pitch = ws.pitch; + } else { + yaw = Mth.wrapDegrees(seatYaw(seat) - defaultYaw); + pitch = seatPitch(seat); + } + + pose.pushPose(); + if (w.turret()) { + rotateAround(def.turretPosition, turretYaw, DOWN_AXIS); + } + translate(w.position(), 1.0F); + if (w.yaw()) { + rotate(w.turret() && !live ? yaw - turretYaw : yaw, DOWN_AXIS); + } + if (w.turret() && live) { + rotate(-(turretYaw - ws.turretLag), DOWN_AXIS); + } + // Side facing guns pitch around their own axis, which the original did by turning + // into the gun's frame and flipping the sign for guns pointing left or right. + boolean sideways = false; + if ((int) defaultYaw != 0) { + float t = Mth.wrapDegrees(defaultYaw); + sideways = (t >= 45.0F && t <= 135.0F) || (t <= -45.0F && t >= -135.0F); + rotate(-defaultYaw, DOWN_AXIS); + } + if (w.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && w.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, w.recoil() * ws.recoil); + } + if (mount != null || live) { + rotate(defaultYaw, DOWN_AXIS); + } + if (live && w.rotatingBarrel()) { + rotate(ws.barrel, w.axis()); + } + translate(w.position(), -1.0F); + if (!w.missile() || !live || !ws.hidden) { + submit(part(w.part())); + for (WeaponPart.Child child : w.children()) { + pose.pushPose(); + translate(child.position(), 1.0F); + if (child.yaw()) { + rotate(yaw, DOWN_AXIS); + } + if (live && (int) defaultYaw != 0) { + rotate(-defaultYaw, DOWN_AXIS); + } + if (child.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && child.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, -child.recoil() * ws.recoil); + } + if (live && (int) defaultYaw != 0) { + rotate(defaultYaw, DOWN_AXIS); + } + translate(child.position(), -1.0F); + submit(part(child.part())); + pose.popPose(); + } + } + pose.popPose(); + } + } + + private WeaponMount mount(WeaponPart part) { + if (part.weapons().isEmpty()) { + return null; + } + int group = def.weaponGroup(part.weapons().getFirst()); + return group >= 0 ? def.weaponGroups.get(group).mounts().getFirst() : null; + } + + private void rotatingParts() { + for (int i = 0; i < def.rotatingParts.size() && i < state.rotatingParts.length; i++) { + RotatingPart part = def.rotatingParts.get(i); + pose.pushPose(); + rotateAround(part.position(), state.rotatingParts[i], part.axis()); + submit(part(part.part())); + pose.popPose(); + } + } + + /** Hatches and canopies: swing by {@code angle * factor}, or slide along their offset. */ + private void movingParts(List parts, float angle) { + for (MovingPart part : parts) { + pose.pushPose(); + moveOrSwing(part, angle); + submit(part(part.part())); + pose.popPose(); + } + } + + private void moveOrSwing(MovingPart part, float angle) { + if (part.slide()) { + translate(part.position(), angle / 90.0F); + } else { + rotateAround(part.position(), angle * part.factor(), part.axis()); + } + } + + private void trackRollers() { + for (TrackRoller roller : def.trackRollers) { + pose.pushPose(); + rotateAround(roller.position(), state.roller[roller.side()], X_AXIS); + submit(part(roller.part())); + pose.popPose(); + } + } + + /** + * Each link slides towards the next one by the track's run, so the loop appears to move. + * The link part is modelled at the origin and placed at each point of the outline. + */ + private void crawlerTracks() { + for (CrawlerTrack track : def.crawlerTracks) { + List link = part(track.part()); + List links = track.links(); + int count = links.size() - 1; + if (link.isEmpty() || count < 1) { + continue; + } + float run = state.track[track.side()]; + for (int i = 0; i < count; i++) { + CrawlerTrack.Link cur = links.get(i); + CrawlerTrack.Link next = links.get((i + 1) % count); + float angleNext = next.angle(); + if (angleNext - cur.angle() < -180.0F) { + angleNext += 360.0F; + } else if (angleNext - cur.angle() > 180.0F) { + angleNext -= 360.0F; + } + pose.pushPose(); + pose.translate(0.0F, cur.y() + (next.y() - cur.y()) * run, cur.z() + (next.z() - cur.z()) * run); + pose.mulPose(Axis.XN.rotationDegrees(cur.angle() + (angleNext - cur.angle()) * run)); + submit(link); + pose.popPose(); + } + } + } + + private void steeringWheels() { + for (WheelPart wheel : def.steeringWheels) { + pose.pushPose(); + rotateAround(wheel.position(), state.steer * wheel.steerAngle(), wheel.axis()); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void wheels() { + for (WheelPart wheel : def.wheels) { + pose.pushPose(); + rotateAround(wheel.pivot(), state.steer * wheel.steerAngle(), wheel.axis()); + rotateAround(wheel.position(), state.wheelRoll, X_AXIS); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void throttles() { + for (ThrottlePart lever : def.throttleParts) { + pose.pushPose(); + rotateAround(lever.position(), state.throttle * lever.angle(), lever.axis()); + translate(lever.slide(), state.throttle); + submit(part(lever.part())); + pose.popPose(); + } + } + + /** Sensor turrets look where the gunner looks, or the pilot when the gunner seat is empty. */ + private void cameras() { + int seat = state.seatOccupied.length > 1 && state.seatOccupied[1] ? 1 : 0; + for (CameraPart camera : def.cameraParts) { + pose.pushPose(); + translate(camera.position(), 1.0F); + if (camera.yawSync()) { + rotate(seatYaw(seat), DOWN_AXIS); + } + if (camera.pitchSync()) { + rotate(seatPitch(seat), X_AXIS); + } + translate(camera.position(), -1.0F); + submit(part(camera.part())); + pose.popPose(); + } + } + + /** + * Gear legs swing up with the gear state; reversed parts close as the gear rises, and gear bay + * doors open fully halfway through and shut again. + */ + private void landingGear() { + float angle = state.landingGear; + float reverse = 90.0F - angle; + float door = Math.min(90.0F, 90.0F * Mth.sin(angle * 2.0F * Mth.DEG_TO_RAD) * 3.0F); + for (LandingGearPart gear : def.landingGear) { + pose.pushPose(); + translate(gear.position(), 1.0F); + if (gear.reverse()) { + rotate(reverse * gear.factor(), gear.axis()); + } else { + rotate((gear.hatch() ? door : angle) * gear.factor(), gear.axis()); + } + if (gear.secondAxis() != null) { + rotate((gear.reverse() ? reverse : angle) * gear.secondFactor(), gear.secondAxis()); + } + translate(gear.position(), -1.0F); + if (gear.slide() != null) { + float f = angle / 90.0F; + translate(gear.slide(), gear.reverse() ? 1.0F - f : f); + } + submit(part(gear.part())); + pose.popPose(); + } + } + + /** Bays swing open while a crew member has one of their weapons selected. */ + private void weaponBays() { + for (int i = 0; i < def.weaponBays.size(); i++) { + WeaponBayPart bay = def.weaponBays.get(i); + pose.pushPose(); + moveOrSwing(bay.door(), i < state.bays.length ? state.bays[i] : 0.0F); + submit(part(bay.door().part())); + pose.popPose(); + } + } + + // Helpers + + private float seatYaw(int seat) { + return seat >= 0 && seat < state.seatYaw.length ? state.seatYaw[seat] : 0.0F; + } + + private float seatPitch(int seat) { + return seat >= 0 && seat < state.seatPitch.length ? state.seatPitch[seat] : 0.0F; + } + + private List part(String name) { + List groups = mesh.part("$" + name); + if (!groups.isEmpty()) { + return groups; + } + Mesh separate = MeshCache.INSTANCE.find(def.folder(), def.name() + "_" + name); + return separate != null ? separate.groups() : List.of(); + } + + private void submit(List groups) { + if (groups.isEmpty()) { + return; + } + if (def.partTextures.isEmpty()) { + submit(groups, texture, opaqueType, glassType); + return; + } + for (PartTextures.Bucket bucket : GlassSplit.INSTANCE.buckets(groups, def.partTextures)) { + if (bucket.texture() == null) { + submit(bucket.groups(), texture, opaqueType, glassType); + } else { + Identifier own = RuntimeAssets.texture(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, + "textures/" + def.folder() + "/" + bucket.texture() + ".png")); + submit(bucket.groups(), own, RenderTypes.entityCutoutCull(own), RenderTypes.entityTranslucentEmissive(own)); + } + } + } + + private void submit(List groups, Identifier texture, RenderType opaqueType, RenderType glassType) { + GlassSplit.Split split = GlassSplit.INSTANCE.split(groups, texture); + if (!split.opaque().isEmpty()) { + collector.submitCustomGeometry(pose, opaqueType, (p, buffer) -> MeshRenderer.emit(split.opaque(), p, buffer, light, overlay)); + } + if (!split.glass().isEmpty()) { + // Culling glass relies on the camera sitting at the origin, which is not so in a GUI. + collector.submitCustomGeometry(pose, glassType, state.inGui + ? (p, buffer) -> MeshRenderer.emit(split.glass(), p, buffer, light, overlay, glassColor) + : (p, buffer) -> MeshRenderer.emitFrontFaces(split.glass(), p, buffer, light, overlay, glassColor)); + } + } + + private void translate(Vector3 v, float scale) { + pose.translate((float) v.x() * scale, (float) v.y() * scale, (float) v.z() * scale); + } + + private void rotateAround(Vector3 pivot, float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + translate(pivot, 1.0F); + rotate(degrees, axis); + translate(pivot, -1.0F); + } + + /** Like {@code glRotatef}: any axis length, a zero axis does nothing. */ + private void rotate(float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + Vector3f a = new Vector3f((float) axis.x(), (float) axis.y(), (float) axis.z()).normalize(); + pose.mulPose(Axis.of(a).rotationDegrees(degrees)); + } + + private static boolean isZero(Vector3 v) { + return v.x() * v.x() + v.y() * v.y() + v.z() * v.z() < 1.0E-12; + } +} From b393c2bc5c7f965d4ab9a759ea9b507c3fccabb1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:08 +0800 Subject: [PATCH 0220/1461] Make the translucent culled render type 26.2 only offers for the item target --- .../client/render/ProjectileRenderer.java | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java new file mode 100644 index 00000000..128708b3 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java @@ -0,0 +1,181 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.weapon.WeaponColor; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderSetup; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Util; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +import java.util.function.Function; + +/** + * Draws rounds, rockets, missiles and bombs from {@code models/bullets}, tinted with the weapon's + * {@code BulletColor} (its in-water colour below the surface) and always fully lit, as the + * original's {@code MCH_RenderBulletBase} did. + * + *

Not in the original: rounds trail a tracer and rocket motors burn with a glow, faint by day + * and bright at night, so a firefight in the dark can be followed by eye. Nothing glows under + * water. + */ +public class ProjectileRenderer extends EntityRenderer { + /** Vanilla on 26.2 only offers this one drawn into the item target. */ + private static final Function TRANSLUCENT_CULL = Util.memoize(texture -> RenderType.create( + MCHeli.MOD_ID + "_translucent_cull", RenderSetup.builder(RenderPipelines.ENTITY_TRANSLUCENT_CULL) + .withTexture("Sampler0", texture) + .useLightmap() + .useOverlay() + .affectsCrumbling() + .sortOnUpload() + .setOutline(RenderSetup.OutlineProperty.AFFECTS_OUTLINE) + .createRenderSetup())); + private static final int FULL_BRIGHT = 0xF000F0; + /** Warm tracer colour for rounds whose weapon keeps the default white tint. */ + private static final int TRACER = 0xFFC46A; + /** The longest tracer ribbon, in blocks; fast rounds cover far more than this per tick. */ + private static final double MAX_TRACER = 6.0; + /** How far behind the projectile's centre the motor glow sits, in blocks. */ + private static final double MOTOR_OFFSET = 0.6; + + public ProjectileRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + enum GlowKind { + NONE, + TRACER, + MOTOR + } + + public static class State extends EntityRenderState { + String model = ""; + float yaw; + float pitch; + int color = 0xFFFFFFFF; + boolean visible = true; + GlowKind glowKind = GlowKind.NONE; + float glow; + int glowColor = TRACER; + /** Distance covered over the last tick, which points the tracer and the motor glow. */ + Vec3 travel = Vec3.ZERO; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ProjectileEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.model = entity.model(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.visible = !entity.isSpent() && (!(entity instanceof BombEntity bomb) || bomb.isVisible()); + WeaponDefinition w = entity.weapon(); + boolean inWater = inWater(entity.level(), entity); + WeaponColor c = w != null ? (inWater ? w.colorInWater : w.color) : WeaponColor.WHITE; + state.color = c.argb(); + + state.glowKind = inWater || entity.isSpent() ? GlowKind.NONE : glowKind(entity); + if (state.glowKind != GlowKind.NONE) { + state.glow = Glow.strength(entity.level(), entity.blockPosition()); + state.travel = new Vec3(entity.getX() - entity.xo, entity.getY() - entity.yo, entity.getZ() - entity.zo); + if (state.glowKind == GlowKind.MOTOR) { + state.glowColor = GlowParticleOptions.FIRE; + } else { + // A weapon with its own tint keeps it for the tracer; the default white burns warm. + int tint = state.color & 0xFFFFFF; + state.glowColor = tint == 0xFFFFFF ? TRACER : tint; + } + } + } + + private static GlowKind glowKind(ProjectileEntity entity) { + if (entity instanceof BulletEntity) { + return GlowKind.TRACER; + } + if (entity instanceof RocketEntity || entity instanceof MarkerRocketEntity || entity instanceof AAMissileEntity + || entity instanceof ASMissileEntity || entity instanceof ATMissileEntity || entity instanceof TvMissileEntity) { + return GlowKind.MOTOR; + } + return GlowKind.NONE; + } + + /** The original looked for water at the round and up to two blocks above it. */ + private static boolean inWater(Level level, ProjectileEntity e) { + for (int y = 0; y < 3; y++) { + if (level.getFluidState(BlockPos.containing(e.getX() + 0.5, e.getY() + 1.5 - y, e.getZ() + 0.5)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = state.visible ? MeshCache.INSTANCE.find("bullets", state.model) : null; + if (mesh != null) { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/" + state.model + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + int color = state.color; + collector.submitCustomGeometry(pose, TRANSLUCENT_CULL.apply(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, FULL_BRIGHT, OverlayTexture.NO_OVERLAY, color)); + pose.popPose(); + } + if (state.visible) { + submitGlow(state, pose, collector, camera); + } + super.submit(state, pose, collector, camera); + } + + private static void submitGlow(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + if (state.glowKind == GlowKind.NONE || state.glow <= 0.0F) { + return; + } + double speed = state.travel.length(); + Vec3 back = speed > 1.0E-3 ? state.travel.scale(-1.0 / speed) : Vec3.ZERO; + switch (state.glowKind) { + case TRACER -> { + Vec3 toCamera = new Vec3(state.x - camera.pos.x, state.y - camera.pos.y, state.z - camera.pos.z); + Glow.streak(pose, collector, toCamera, back.scale(Math.min(speed, MAX_TRACER)), 0.08F, state.glowColor, state.glow); + Glow.halo(pose, collector, camera, Vec3.ZERO, 0.35F, state.glowColor, state.glow); + } + case MOTOR -> { + Vec3 nozzle = back.scale(MOTOR_OFFSET); + Glow.halo(pose, collector, camera, nozzle, 1.1F, state.glowColor, state.glow); + Glow.halo(pose, collector, camera, nozzle, 0.4F, GlowParticleOptions.FLASH, state.glow); + } + default -> { + } + } + } +} From 46a9500acca07fce343b9066caadb3b77632d55d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:08 +0800 Subject: [PATCH 0221/1461] Add client/render/RiderPose.java to the 26.2 shared sources --- .../mcheli/client/render/RiderPose.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java new file mode 100644 index 00000000..9ddb1690 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Tilts players riding a vehicle with its hull, as the original did from its render hook. + * + *

Vanilla keeps riders upright, so a banked plane left the pilot standing straight up through + * the canopy. The tilt is the hull's pitch and roll expressed in the world frame, applied about + * the rider's vehicle attachment point (the hip), which is the point that sits on the seat. The + * rider's own body yaw is applied by the renderer afterwards, so turning in the seat still works. + * + *

The entity render dispatcher pushes a pose around every entity, so nothing needs popping here. + */ +public final class RiderPose { + private RiderPose() { + } + + /** Called before a living entity is drawn, with the pose the renderer is about to use. */ + public static void beforeRender(LivingEntityRenderState renderState, PoseStack pose, float partialTicks) { + // Render states carry no entity reference; players are the ones that keep their id. + if (!(renderState instanceof AvatarRenderState state)) { + return; + } + ClientLevel level = Minecraft.getInstance().level; + Entity rider = level != null ? level.getEntity(state.id) : null; + if (rider == null || !(rider.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + float yaw = vehicle.getYRot(partialTicks); + float pitch = vehicle.getXRot(partialTicks); + float roll = vehicle.roll(partialTicks); + if (pitch == 0.0F && roll == 0.0F) { + return; + } + Quaternionf tilt = AircraftRotation.orientation(yaw, pitch, roll).rotateY((float) Math.toRadians(yaw)); + Vec3 hip = rider.getVehicleAttachmentPoint(vehicle); + pose.rotateAround(tilt, (float) hip.x, (float) hip.y, (float) hip.z); + } +} From 8cb41f170a515c4411de8e2128f48831af69d19d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:10 +0800 Subject: [PATCH 0222/1461] Port ThrowableRenderer to the 26.2 pose stack --- .../client/render/ThrowableRenderer.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java new file mode 100644 index 00000000..a693052b --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +/** {@code MCH_RenderThrowable}: the grenade's own model from {@code models/throwable}. */ +public class ThrowableRenderer extends EntityRenderer { + public ThrowableRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + String name = ""; + float yaw; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ThrowableEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.name = entity.definitionName(); + state.yaw = entity.getYRot(partialTicks); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = state.name.isEmpty() ? null : MeshCache.INSTANCE.get("throwable", state.name); + if (mesh != null) { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/throwable/" + state.name + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From d71793dcf8ccbda2013e259ef33fadc0d2ae7f4a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:10 +0800 Subject: [PATCH 0223/1461] Port UavStationRenderer to the 26.2 pose stack --- .../client/render/UavStationRenderer.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java new file mode 100644 index 00000000..e3f7f9c7 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +import java.util.List; + +/** + * The UAV station and the portable controller, {@code MCH_RenderUavStation}. The station's screen + * lights up while a drone is flown from it; the controller's lid, keyboard half and display fold + * open as it is opened, and light up once fully open. + */ +public class UavStationRenderer extends EntityRenderer { + private static final String[] MODEL = {"uav_station", "uav_portable_controller"}; + /** The original greyed both a little. */ + private static final int TINT = 0xFFBFBFBF; + + public UavStationRenderer(EntityRendererProvider.Context context) { + super(context); + shadowRadius = 1.0F; + } + + public static class State extends EntityRenderState { + float yaw; + int kind; + boolean lit; + float cover; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(UavStationEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.kind = entity.kind(); + state.cover = entity.cover(partialTicks); + state.lit = state.kind == UavStationEntity.PORTABLE ? state.cover > 0.95F : entity.isLinked(); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + int index = state.kind == UavStationEntity.PORTABLE ? 1 : 0; + Mesh mesh = MeshCache.INSTANCE.get("", MODEL[index]); + if (mesh != null) { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + MODEL[index] + (state.lit ? "_on" : "") + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = state.lightCoords; + if (index == 0) { + draw(pose, collector, texture, mesh.groups(), light); + } else { + draw(pose, collector, texture, mesh.part("$body"), light); + folding(pose, collector, texture, mesh.part("$cover"), state.cover * 60.0F, -0.1812F, -0.3186F, light); + folding(pose, collector, texture, mesh.part("$laptop_cover"), state.cover * 95.0F, -0.1808F, -0.0422F, light); + folding(pose, collector, texture, mesh.part("$display"), state.cover * -85.0F, -0.1807F, 0.2294F, light); + } + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } + + /** {@code renderRotPart}: turns a part about the x axis through its hinge. */ + private static void folding(PoseStack pose, SubmitNodeCollector collector, Identifier texture, List part, + float angle, float y, float z, int light) { + pose.pushPose(); + pose.translate(0.0F, y, z); + pose.mulPose(Axis.XN.rotationDegrees(angle)); + pose.translate(0.0F, -y, -z); + draw(pose, collector, texture, part, light); + pose.popPose(); + } + + private static void draw(PoseStack pose, SubmitNodeCollector collector, Identifier texture, List groups, int light) { + if (!groups.isEmpty()) { + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY, TINT)); + } + } +} From a5c1d119ff00692433b5bc1f8b4de084d3735055 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:11 +0800 Subject: [PATCH 0224/1461] Add client/screen/DraftingTableScreen.java to the 26.2 shared sources --- .../client/screen/DraftingTableScreen.java | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java new file mode 100644 index 00000000..ec38e26d --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java @@ -0,0 +1,335 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.workshop.WorkshopHomeScreen; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.network.DraftingTablePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeCollection; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; +import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * The drafting table's recipe browser, {@code MCH_DraftingTableGui}. Pick a list (helicopters, + * planes, tanks, ground vehicles or equipment), step through its recipes, and Create makes the one + * shown into the output slot. The right page shows the original's illustrated description for + * equipment and a vehicle's numbers for vehicles; the original turned a 3D model there instead. + * + *

The recipes come from the player's recipe book, which holds every MCHeli recipe from login. + */ +public class DraftingTableScreen extends AbstractContainerScreen { + private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/drafting_table.png"); + private static final int TEXTURE_SIZE = 512; + /** The description pages take this part of their 512 square texture. */ + private static final int PAGE_WIDTH = 340; + private static final int PAGE_HEIGHT = 380; + private static final int PAGE_X = 210; + private static final int PAGE_Y = 20; + private static final int GRID_X = 121; + private static final int GRID_Y = 34; + private static final int TEXT = 0xFF6A3A00; + private static final int WHITE = 0xFFFFFFFF; + private static final String[] LISTS = {"helicopter", "plane", "vehicle", "tank", "item"}; + + private final List> lists = new ArrayList<>(); + private int list; + private int index; + private int page; + private int pageCount; + private @Nullable Button create; + private @Nullable Button previous; + private @Nullable Button next; + private @Nullable Button previousPage; + private @Nullable Button nextPage; + + public DraftingTableScreen(DraftingTableMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title, 400, 240); + inventoryLabelX = 30; + inventoryLabelY = 130; + } + + @Override + protected void init() { + super.init(); + collectRecipes(); + for (int i = 0; i < LISTS.length; i++) { + int which = i; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.list." + LISTS[i]), b -> selectList(which)) + .bounds(leftPos + 20, topPos + 20 + i * 20, 90, 20).build()); + } + create = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.create"), b -> create()) + .bounds(leftPos + 120, topPos + 89, 50, 20).build()); + previous = addRenderableWidget(Button.builder(Component.literal("<<"), b -> step(-1)).bounds(leftPos + 120, topPos + 111, 36, 20).build()); + next = addRenderableWidget(Button.builder(Component.literal(">>"), b -> step(1)).bounds(leftPos + 155, topPos + 111, 35, 20).build()); + previousPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.previous_page"), b -> turnPage(-1)) + .bounds(leftPos + 210, topPos + 210, 60, 20).build()); + nextPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.next_page"), b -> turnPage(1)) + .bounds(leftPos + 270, topPos + 210, 60, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.workshop"), b -> openWorkshop()) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.drafting_table.workshop_hint"))) + .bounds(leftPos + 334, topPos + 210, 56, 20).build()); + selectList(list); + } + + /** The workshop is a screen of its own and needs no block, so the table's menu closes first. */ + private void openWorkshop() { + minecraft.player.closeContainer(); + minecraft.gui.setScreen(new WorkshopHomeScreen()); + } + + /** Every MCHeli recipe the recipe book holds, sorted into the five lists by what it makes. */ + private void collectRecipes() { + lists.clear(); + for (int i = 0; i < LISTS.length; i++) { + lists.add(new ArrayList<>()); + } + ContextMap context = SlotDisplayContext.fromLevel(minecraft.level); + for (RecipeCollection collection : minecraft.player.getRecipeBook().getCollections()) { + for (RecipeDisplayEntry entry : collection.getRecipes()) { + ItemStack result = entry.display().result().resolveForFirstStack(context); + if (!result.isEmpty() && BuiltInRegistries.ITEM.getKey(result.getItem()).getNamespace().equals(MCHeli.MOD_ID)) { + lists.get(listOf(result.getItem())).add(entry); + } + } + } + for (List entries : lists) { + entries.sort(Comparator.comparing(e -> e.display().result().resolveForFirstStack(context).getHoverName().getString())); + } + } + + private static int listOf(Item item) { + if (item == ModItems.HELICOPTER.get()) { + return 0; + } + if (item == ModItems.PLANE.get()) { + return 1; + } + if (item == ModItems.VEHICLE.get()) { + return 2; + } + if (item == ModItems.TANK.get()) { + return 3; + } + return 4; + } + + private void selectList(int which) { + list = which; + index = 0; + showRecipe(); + } + + private void step(int direction) { + int size = lists.get(list).size(); + if (size > 0) { + index = Math.floorMod(index + direction, size); + showRecipe(); + } + } + + private void turnPage(int direction) { + page = Math.clamp(page + direction, 0, Math.max(pageCount - 1, 0)); + updateButtons(); + } + + private void showRecipe() { + page = 0; + ItemStack result = result(); + pageCount = descriptionPages(result); + updateButtons(); + } + + private @Nullable RecipeDisplayEntry current() { + List entries = lists.get(list); + return entries.isEmpty() ? null : entries.get(Math.min(index, entries.size() - 1)); + } + + private ItemStack result() { + RecipeDisplayEntry entry = current(); + return entry != null ? entry.display().result().resolveForFirstStack(SlotDisplayContext.fromLevel(minecraft.level)) : ItemStack.EMPTY; + } + + /** The original's description pages for equipment: {@code drafting_table_desc/_.png}. */ + private int descriptionPages(ItemStack result) { + if (result.isEmpty() || AircraftItem.vehicleOf(result) != null) { + return 0; + } + String name = descriptionName(result); + int pages = 0; + while (pages < 10 && minecraft.getResourceManager().getResource(pageTexture(name, pages)).isPresent()) { + pages++; + } + return pages; + } + + private static String descriptionName(ItemStack result) { + if (result.getItem() == ModItems.THROWABLE.get()) { + String throwable = result.get(ModDataComponents.THROWABLE.get()); + return throwable != null ? throwable : "throwable"; + } + return BuiltInRegistries.ITEM.getKey(result.getItem()).getPath(); + } + + private static Identifier pageTexture(String name, int page) { + return Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/drafting_table_desc/" + name + "_" + page + ".png"); + } + + private boolean canCreate() { + RecipeDisplayEntry entry = current(); + if (entry == null || menu.getSlot(menu.slots.size() - 1).hasItem() && !minecraft.player.getAbilities().instabuild) { + return false; + } + if (minecraft.player.getAbilities().instabuild) { + return true; + } + StackedItemContents contents = new StackedItemContents(); + minecraft.player.getInventory().fillStackedContents(contents); + return entry.canCraft(contents); + } + + private void create() { + RecipeDisplayEntry entry = current(); + if (entry != null) { + ModNetwork.sendToServer(new DraftingTablePacket(entry.id())); + } + } + + private void updateButtons() { + if (create == null || previous == null || next == null || previousPage == null || nextPage == null) { + return; + } + boolean any = current() != null; + create.active = any && canCreate(); + previous.active = lists.get(list).size() > 1; + next.active = lists.get(list).size() > 1; + previousPage.visible = pageCount > 1; + nextPage.visible = pageCount > 1; + previousPage.active = page > 0; + nextPage.active = page < pageCount - 1; + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractBackground(graphics, mouseX, mouseY, a); + graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, TEXTURE_SIZE, TEXTURE_SIZE); + if (pageCount > 0) { + String name = descriptionName(result()); + graphics.blit(RenderPipelines.GUI_TEXTURED, pageTexture(name, page), leftPos + PAGE_X, topPos + PAGE_Y, 0.0F, 0.0F, + PAGE_WIDTH / 2, PAGE_HEIGHT / 2, PAGE_WIDTH, PAGE_HEIGHT, TEXTURE_SIZE, TEXTURE_SIZE); + } + } + + @Override + protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, TEXT, false); + RecipeDisplayEntry entry = current(); + if (entry == null) { + graphics.text(font, Component.translatable("gui.mcheli.drafting_table.empty"), 120, 20, TEXT, false); + return; + } + ItemStack result = result(); + graphics.text(font, font.plainSubstrByWidth(result.getHoverName().getString(), 85), 120, 20, WHITE, true); + graphics.text(font, (index + 1) + " / " + lists.get(list).size(), 120, 134, TEXT, false); + ingredients(graphics, entry.display(), mouseX, mouseY); + graphics.item(result, 178, 52); + if (pageCount == 0) { + details(graphics, result); + } + } + + /** {@code drawItemRecipe}: the 3x3 grid, each slot cycling through what it accepts once a second. */ + private void ingredients(GuiGraphicsExtractor graphics, RecipeDisplay display, int mouseX, int mouseY) { + ContextMap context = SlotDisplayContext.fromLevel(minecraft.level); + List slots; + int width; + switch (display) { + case ShapedCraftingRecipeDisplay shaped -> { + slots = shaped.ingredients(); + width = shaped.width(); + } + case ShapelessCraftingRecipeDisplay shapeless -> { + slots = shapeless.ingredients(); + width = 3; + } + default -> { + return; + } + } + int tick = (int) (minecraft.level.getGameTime() / 20); + for (int i = 0; i < slots.size() && i < 9; i++) { + List options = slots.get(i).resolveForStacks(context); + if (options.isEmpty()) { + continue; + } + ItemStack stack = options.get(tick % options.size()); + int x = GRID_X + (i % width) * 18; + int y = GRID_Y + (i / width) * 18; + graphics.item(stack, x, y); + if (mouseX - leftPos >= x && mouseX - leftPos < x + 16 && mouseY - topPos >= y && mouseY - topPos < y + 16) { + graphics.setTooltipForNextFrame(font, stack, mouseX - leftPos, mouseY - topPos); + } + } + } + + /** The vehicle's numbers, where the original turned its model. */ + private void details(GuiGraphicsExtractor graphics, ItemStack result) { + String name = AircraftItem.vehicleOf(result); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + return; + } + List lines = new ArrayList<>(); + lines.add(Component.translatable("gui.mcheli.drafting_table.hp", def.maxHp)); + lines.add(Component.translatable("gui.mcheli.drafting_table.speed", "%.2f".formatted(def.speed))); + lines.add(Component.translatable("gui.mcheli.drafting_table.seats", def.seatCount())); + lines.add(Component.translatable("gui.mcheli.drafting_table.fuel", def.maxFuel)); + lines.add(Component.translatable("gui.mcheli.drafting_table.weapons")); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + String weaponName = weapon != null && weapon.displayName != null ? weapon.displayName : group.weaponType(); + lines.add(Component.literal(" " + weaponName + (group.mounts().size() > 1 ? " x" + group.mounts().size() : ""))); + } + int y = PAGE_Y + 20; + for (Component line : lines) { + if (y > PAGE_Y + 180) { + break; + } + graphics.text(font, font.split(line, 170).getFirst(), PAGE_X, y, TEXT, false); + y += 10; + } + } +} From d4cb6dfb2adcf08be62cc274655a508912da4160 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:11 +0800 Subject: [PATCH 0225/1461] Add client/screen/TeamManagerScreen.java to the 26.2 shared sources --- .../client/screen/TeamManagerScreen.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java new file mode 100644 index 00000000..8578f949 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java @@ -0,0 +1,135 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.TeamPacket; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.TeamColor; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * The team manager, {@code MCH_GuiScoreboard_Main} and {@code MCH_GuiScoreboard_CreateTeam}, for + * operators setting up a match: switch PvP, shuffle everyone into the teams, make a team, send + * everyone to their spawn point and clear away every vehicle. The teams and their players are + * listed on the right. + */ +public class TeamManagerScreen extends Screen { + private static final int WIDTH = 150; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + + private @Nullable EditBox name; + private int color = TeamColor.RED.ordinal(); + private boolean friendlyFire; + private @Nullable Button colorButton; + private @Nullable Button friendlyFireButton; + private @Nullable Button createButton; + + public TeamManagerScreen() { + super(Component.translatable("gui.mcheli.team.title")); + } + + @Override + protected void init() { + int left = width / 2 - WIDTH - 10; + int y = 40; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.pvp"), b -> send(TeamPacket.Action.PVP)) + .bounds(left, y, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.shuffle"), b -> send(TeamPacket.Action.SHUFFLE)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.shuffle_hint"))) + .bounds(left, y + 22, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.jump_spawn"), b -> send(TeamPacket.Action.JUMP_TO_SPAWN)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.jump_spawn_hint"))) + .bounds(left, y + 44, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.destroy_all"), b -> send(TeamPacket.Action.DESTROY_ALL)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.destroy_all_hint"))) + .bounds(left, y + 66, WIDTH, 20).build()); + + int create = y + 104; + name = addRenderableWidget(new EditBox(font, left, create, WIDTH, 20, Component.translatable("gui.mcheli.team.name"))); + name.setMaxLength(16); + name.setHint(Component.translatable("gui.mcheli.team.name")); + name.setResponder(text -> updateButtons()); + colorButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + color = (color + 1) % TeamColor.VALUES.size(); + updateButtons(); + }).bounds(left, create + 22, WIDTH, 20).build()); + friendlyFireButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + friendlyFire = !friendlyFire; + updateButtons(); + }).bounds(left, create + 44, WIDTH, 20).build()); + createButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.create"), b -> { + ModNetwork.sendToServer(new TeamPacket(TeamPacket.Action.CREATE_TEAM, name.getValue(), color, friendlyFire)); + name.setValue(""); + }).bounds(left, create + 66, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 - 50, height - 28, 100, 20).build()); + updateButtons(); + } + + private void send(TeamPacket.Action action) { + ModNetwork.sendToServer(new TeamPacket(action)); + } + + private void updateButtons() { + if (colorButton == null || friendlyFireButton == null || createButton == null || name == null) { + return; + } + TeamColor teamColor = TeamColor.VALUES.get(color); + colorButton.setMessage(Component.translatable("gui.mcheli.team.color", + Component.literal(teamColor.getSerializedName()).withColor(teamColor.textColor().getValue()))); + friendlyFireButton.setMessage(Component.translatable(friendlyFire ? "gui.mcheli.team.friendly_fire_on" : "gui.mcheli.team.friendly_fire_off")); + String value = name.getValue().trim(); + createButton.active = !value.isEmpty() && !value.contains(" "); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractRenderState(graphics, mouseX, mouseY, a); + graphics.centeredText(font, title, width / 2, 15, TEXT); + int left = width / 2 - WIDTH - 10; + graphics.text(font, Component.translatable("gui.mcheli.team.new_team"), left, 132, DIM); + teams(graphics, width / 2 + 10, 40); + } + + /** The scoreboard's teams and who is in them, as this client sees the scoreboard. */ + private void teams(GuiGraphicsExtractor graphics, int x, int y) { + if (minecraft.level == null) { + return; + } + List teams = new ArrayList<>(minecraft.level.getScoreboard().getPlayerTeams()); + if (teams.isEmpty()) { + graphics.text(font, Component.translatable("gui.mcheli.team.none"), x, y, DIM); + return; + } + int bottom = height - 40; + for (PlayerTeam team : teams) { + if (y > bottom) { + break; + } + graphics.text(font, team.getFormattedDisplayName(), x, y, TEXT); + y += 11; + for (String player : team.getPlayers()) { + if (y > bottom) { + break; + } + graphics.text(font, " " + player, x, y, DIM); + y += 10; + } + y += 4; + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From ad3b5e13298490f24a33db3a1d8cfc9a881c0f91 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:12 +0800 Subject: [PATCH 0226/1461] Add client/screen/UavStationScreen.java to the 26.2 shared sources --- .../client/screen/UavStationScreen.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java new file mode 100644 index 00000000..0a9c3da6 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_GuiUavStation}: the slot for the drone, the launch offset + * per axis with its buttons, and Continue to take the last drone flown from here again. + */ +public class UavStationScreen extends AbstractContainerScreen { + private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/uav_station.png"); + private static final String[] AXES = {"X", "Y", "Z"}; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF0000; + + private @Nullable Button continueButton; + + public UavStationScreen(UavStationMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title, 176, 166); + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + int x = width / 2 - 5; + int y = height / 2 - 76; + for (int axis = 0; axis < 3; axis++) { + for (int step = 0; step < UavStationMenu.STEPS.length; step++) { + int s = UavStationMenu.STEPS[step]; + int id = axis * 4 + step; + addRenderableWidget(Button.builder(Component.literal((s > 0 ? "+" : "") + s), b -> press(id)) + .bounds(x + step * 20, y + axis * 22, 20, 20).build()); + } + } + continueButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.uav_station.continue"), b -> press(UavStationMenu.BUTTON_CONTINUE)) + .bounds(x - 77, y + 44, 50, 20).build()); + continueButton.active = menu.canContinue(); + } + + @Override + protected void containerTick() { + super.containerTick(); + if (continueButton != null) { + continueButton.active = menu.canContinue(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractBackground(graphics, mouseX, mouseY, a); + graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, 256, 256); + } + + @Override + protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + UavStationEntity station = menu.station(); + ItemStack stack = menu.getSlot(0).getItem(); + AircraftDefinition def = stack.isEmpty() ? null : UavStationEntity.definitionOf(stack); + Component label; + int color = WHITE; + if (!stack.isEmpty() && (def == null || !def.uav)) { + label = Component.translatable("gui.mcheli.uav_station.not_uav"); + color = RED; + } else if (station != null && station.kind() == UavStationEntity.PORTABLE && def != null && !def.smallUav) { + label = Component.translatable("gui.mcheli.uav_station.small_only"); + color = RED; + } else { + label = title; + } + graphics.text(font, label, 8, 6, color, false); + graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, WHITE, false); + for (int axis = 0; axis < 3; axis++) { + graphics.text(font, String.format("%s.%+d", AXES[axis], menu.offset(axis)), 58, 15 + axis * 22, WHITE, false); + } + } +} From 71be9c711a38212f3bb5478c01d270e1e8e4a9b0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:12 +0800 Subject: [PATCH 0227/1461] Add client/screen/VehicleScreen.java to the 26.2 shared sources --- .../mcheli/client/screen/VehicleScreen.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java new file mode 100644 index 00000000..a320b923 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java @@ -0,0 +1,180 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGui}: fuel and parachute slots on the left, the weapon + * panel on the right with its reload cost and button, and a button into the cargo chest for + * vehicles that have one. The panel is drawn from the chest texture so it looks like vanilla. + */ +public class VehicleScreen extends AbstractContainerScreen { + private static final Identifier CHEST = Identifier.withDefaultNamespace("textures/gui/container/generic_54.png"); + private static final Identifier SLOT = Identifier.withDefaultNamespace("container/slot"); + /** Height of the part above the player inventory. */ + private static final int TOP = 88; + private static final int PANEL_X = 72; + private static final int PANEL_WIDTH = 96; + private static final int LABEL = 0xFF404040; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF5555; + private static final int GREEN = 0xFF55FF55; + + private int weapon; + private boolean cargoButton; + private @Nullable Button reload; + private @Nullable Button previous; + private @Nullable Button next; + + public VehicleScreen(VehicleMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title, 176, TOP + 96); + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + previous = addRenderableWidget(Button.builder(Component.literal("<"), b -> step(-1)) + .bounds(leftPos + PANEL_X, topPos + 68, 16, 16).build()); + reload = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.reload"), b -> press(weapon)) + .bounds(leftPos + PANEL_X + 18, topPos + 68, PANEL_WIDTH - 36, 16).build()); + next = addRenderableWidget(Button.builder(Component.literal(">"), b -> step(1)) + .bounds(leftPos + PANEL_X + PANEL_WIDTH - 16, topPos + 68, 16, 16).build()); + AircraftEntity vehicle = menu.vehicle(); + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + cargoButton = def != null && def.cargoRows() > 0; + if (cargoButton) { + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.cargo"), b -> press(VehicleMenu.BUTTON_CARGO)) + .bounds(leftPos + imageWidth - 52, topPos + 3, 46, 13).build()); + } + updateButtons(); + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + private int weaponCount() { + AircraftEntity vehicle = menu.vehicle(); + VehicleWeapons weapons = vehicle != null ? vehicle.weapons() : null; + return weapons != null ? Math.min(weapons.sets().size(), VehicleMenu.MAX_WEAPONS) : 0; + } + + private void step(int direction) { + int count = weaponCount(); + if (count > 0) { + weapon = Math.floorMod(weapon + direction, count); + updateButtons(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + private void updateButtons() { + if (reload == null || previous == null || next == null) { + return; + } + int count = weaponCount(); + reload.visible = count > 0; + previous.visible = count > 1; + next.visible = count > 1; + reload.active = menu.canReload(weapon); + AircraftEntity vehicle = menu.vehicle(); + boolean grounded = vehicle != null && vehicle.canSupply(); + reload.setTooltip(grounded ? null : Tooltip.create(Component.translatable("gui.mcheli.vehicle.reload_hint"))); + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractBackground(graphics, mouseX, mouseY, a); + int x = leftPos; + int y = topPos; + // The chest's title bar, its plain middle stretched down to the inventory, then the inventory. + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y, 0.0F, 0.0F, imageWidth, 17, 256, 256); + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y + 17, 0.0F, 12.0F, imageWidth, TOP - 17, imageWidth, 1, 256, 256); + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y + TOP, 0.0F, 126.0F, imageWidth, 96, 256, 256); + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SLOT, x + VehicleMenu.FUEL_X + i * 18 - 1, y + VehicleMenu.FUEL_Y - 1, 18, 18); + } + for (int i = 0; i < menu.parachuteSlots(); i++) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SLOT, x + VehicleMenu.PARACHUTE_X + i * 18 - 1, y + VehicleMenu.PARACHUTE_Y - 1, 18, 18); + } + graphics.fill(x + PANEL_X - 2, y + 17, x + PANEL_X + PANEL_WIDTH + 2, y + 66, 0xFF373737); + } + + @Override + protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + AircraftEntity vehicle = menu.vehicle(); + // Long names only give way to the Cargo button, where there is one. + int titleWidth = cargoButton ? imageWidth - 64 : imageWidth - 16; + graphics.text(font, font.plainSubstrByWidth(title.getString(), titleWidth), titleLabelX, titleLabelY, LABEL, false); + graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, LABEL, false); + if (vehicle == null) { + return; + } + if (vehicle.maxFuel() > 0) { + // A percentage and a gauge: the exact units would run into the weapon panel. + int percent = Math.round(vehicle.fuelRatio() * 100.0F); + graphics.text(font, Component.translatable("gui.mcheli.vehicle.fuel", percent), VehicleMenu.FUEL_X, VehicleMenu.FUEL_Y - 11, LABEL, false); + int gaugeY = VehicleMenu.FUEL_Y + 18; + int width = VehicleInventory.FUEL_SLOTS * 18 - 2; + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width, gaugeY + 3, 0xFF373737); + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width * percent / 100, gaugeY + 3, percent <= 10 ? RED : GREEN); + } + if (menu.parachuteSlots() > 0) { + graphics.text(font, Component.translatable("gui.mcheli.vehicle.parachute"), VehicleMenu.PARACHUTE_X, VehicleMenu.PARACHUTE_Y - 11, LABEL, false); + } + weaponPanel(graphics, vehicle); + } + + /** Name, rounds carried against the maximum, and what one reload costs. */ + private void weaponPanel(GuiGraphicsExtractor graphics, AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null && weaponCount() > 0 ? weapons.set(Math.min(weapon, weaponCount() - 1)) : null; + if (set == null) { + graphics.text(font, Component.translatable("gui.mcheli.vehicle.no_weapons"), PANEL_X, 20, WHITE, false); + return; + } + graphics.text(font, font.plainSubstrByWidth(set.displayName(), PANEL_WIDTH), PANEL_X, 20, WHITE, false); + int rounds = menu.rounds(weapon); + int max = set.maxAmmo(); + int color = rounds == 0 && max > 0 ? RED : rounds >= max ? GREEN : WHITE; + graphics.text(font, rounds + " / " + max, PANEL_X, 31, color, false); + int itemX = PANEL_X; + for (RoundItem cost : set.definition().roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null) { + continue; + } + graphics.item(new ItemStack(item), itemX, 42); + graphics.text(font, String.valueOf(cost.count()), itemX + 17, 47, WHITE, false); + itemX += 32; + if (itemX > PANEL_X + PANEL_WIDTH - 16) { + break; + } + } + } +} From c01b3cff581e014d985252a87d1e2b645f346c5b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:12 +0800 Subject: [PATCH 0228/1461] Add client/sound/EngineSound.java to the 26.2 shared sources --- .../mcheli/client/sound/EngineSound.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java new file mode 100644 index 00000000..b32040f5 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** + * A looping sound of one vehicle: its engine, or the rotor loop some vehicles have alongside it. + * Ported from {@code MCH_SoundUpdater}. + * + *

Volume and pitch follow the vehicle's throttle every tick, the pitch along the definition's + * curve if it has one and otherwise the way the kind of vehicle always has. For listeners outside + * the vehicle the sound is placed between them and the vehicle, so that the 16 block range of a + * normal sound stretches to the definition's {@code SoundRange}, and the pitch bends slightly as the + * vehicle approaches or leaves, which is what the original did. + */ +final class EngineSound extends RuntimeSoundInstance { + private static final float MAX_DOPPLER = 0.2F; + + private final AircraftEntity vehicle; + private final EngineSoundSource source; + private final boolean rotor; + private float doppler; + private double lastDistance = -1.0; + + EngineSound(AircraftEntity vehicle, EngineSoundSource source, String sound, boolean rotor) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.vehicle = vehicle; + this.source = source; + this.rotor = rotor; + this.looping = true; + this.delay = 0; + update(); + } + + @Override + public void tick() { + if (vehicle.isRemoved() || vehicle.definition() == null) { + stop(); + return; + } + update(); + if (volume <= 0.0F) { + stop(); + } + } + + private void update() { + AircraftDefinition def = vehicle.definition(); + LocalPlayer player = Minecraft.getInstance().player; + if (def == null || player == null) { + volume = 0.0F; + return; + } + volume = (rotor ? def.rotorSoundVolume : def.soundVolume) * source.engineVolume(); + PitchCurve curve = rotor ? def.rotorSoundPitchCurve : def.soundPitchCurve; + float pitch = def.soundPitch * (curve != null ? curve.at(vehicle.visibleThrottle()) : source.enginePitch()); + boolean aboard = player.getRootVehicle() == vehicle.getRootVehicle(); + if (aboard) { + x = vehicle.getX(); + y = vehicle.getY(); + z = vehicle.getZ(); + } else { + double dx = vehicle.getX() - player.getX(); + double dz = vehicle.getZ() - player.getZ(); + double distance = Math.sqrt(dx * dx + dz * dz); + if (lastDistance >= 0.0) { + double closing = lastDistance - distance; + if (Math.abs(closing) > 0.3) { + doppler = Math.clamp(doppler + (float) (closing / 40.0), -MAX_DOPPLER, MAX_DOPPLER); + } + } + doppler *= 0.9F; + pitch += doppler; + lastDistance = distance; + + double scale = def.soundRange / 16.0; + x = player.getX() + dx / scale; + y = player.getY() + (vehicle.getY() - player.getY()) / scale; + z = player.getZ() + dz / scale; + } + this.pitch = Math.max(pitch, 0.0F); + } + + @Override + public boolean canStartSilent() { + return true; + } +} From 6c8f96fb598c0bd24e4780e18fd5e63cc0f68784 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:13 +0800 Subject: [PATCH 0229/1461] Add client/sound/EngineSoundPlayer.java to the 26.2 shared sources --- .../client/sound/EngineSoundPlayer.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java new file mode 100644 index 00000000..62e52284 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.Entity; + +import java.util.HashMap; +import java.util.Map; + +/** + * Starts engine and rotor loops for vehicles near the player and restarts them after they fall + * silent. The sounds themselves track their vehicle until it stops or disappears. + */ +public final class EngineSoundPlayer { + /** Ticks to wait before restarting a loop, as the original did, so a stuttering engine does not spam. */ + private static final int RESTART_DELAY = 20; + + /** By {@link #key}, since a vehicle can have two loops. */ + private static final Map PLAYING = new HashMap<>(); + private static final Map COOLDOWN = new HashMap<>(); + + private EngineSoundPlayer() { + } + + public static void tick() { + Minecraft mc = Minecraft.getInstance(); + ClientLevel level = mc.level; + LocalPlayer player = mc.player; + if (level == null || player == null || mc.isPaused()) { + if (level == null) { + PLAYING.clear(); + COOLDOWN.clear(); + } + return; + } + PLAYING.values().removeIf(EngineSound::isStopped); + COOLDOWN.replaceAll((id, ticks) -> ticks - 1); + COOLDOWN.values().removeIf(ticks -> ticks <= 0); + + for (Entity entity : level.entitiesForRendering()) { + if (!(entity instanceof AircraftEntity vehicle) || !(entity instanceof EngineSoundSource source)) { + continue; + } + AircraftDefinition def = vehicle.definition(); + if (def == null) { + continue; + } + double range = def.soundRange * 1.2; + if (vehicle.distanceToSqr(player) > range * range) { + continue; + } + start(mc, vehicle, source, def.soundOrDefault(), def.soundVolume, false); + start(mc, vehicle, source, def.rotorSound, def.rotorSoundVolume, true); + } + } + + private static void start(Minecraft mc, AircraftEntity vehicle, EngineSoundSource source, String sound, float volume, boolean rotor) { + long key = key(vehicle, rotor); + if (sound.isEmpty() || PLAYING.containsKey(key) || COOLDOWN.containsKey(key) || volume * source.engineVolume() <= 0.01F) { + return; + } + EngineSound loop = new EngineSound(vehicle, source, sound, rotor); + mc.getSoundManager().play(loop); + PLAYING.put(key, loop); + COOLDOWN.put(key, RESTART_DELAY); + } + + private static long key(AircraftEntity vehicle, boolean rotor) { + return (long) vehicle.getId() << 1 | (rotor ? 1L : 0L); + } +} From 61b7fb266d07e487703ce9035ed5ca33692ef07a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:13 +0800 Subject: [PATCH 0230/1461] Add client/sound/OneShotSound.java to the 26.2 shared sources --- .../mcheli/client/sound/OneShotSound.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java new file mode 100644 index 00000000..31825a4f --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java @@ -0,0 +1,22 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** A sound from a content pack played once at a point, such as a gun firing. */ +public final class OneShotSound extends RuntimeSoundInstance { + public OneShotSound(String sound, double x, double y, double z, float volume, float pitch) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.x = x; + this.y = y; + this.z = z; + this.volume = volume; + this.pitch = pitch; + } + + @Override + public void tick() { + // It stays where it was started and ends when the file does. + } +} From b905da619a48dc7d106bacfa534527174db2f7a3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:14 +0800 Subject: [PATCH 0231/1461] Override resolve, the name 26.2 gives getOrResolve --- .../client/sound/RuntimeSoundInstance.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java new file mode 100644 index 00000000..fb3ee2e3 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -0,0 +1,79 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.JOrbisAudioStream; +import net.minecraft.client.sounds.LoopingAudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.client.sounds.WeighedSoundEvents; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.util.Util; +import net.minecraft.util.valueproviders.ConstantFloat; +import org.jspecify.annotations.Nullable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * A sound that plays through the sound manager when a resource pack defines its event, and + * otherwise streams the OGG file a runtime root holds for it ({@link RuntimeAssets#soundFile}). + * Content added while the game runs can bring sounds this way without touching + * {@code sounds.json} or reloading resources. + */ +public abstract class RuntimeSoundInstance extends AbstractTickableSoundInstance { + private @Nullable Path file; + + protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSource random) { + super(event, source, random); + } + + @Override + public @Nullable WeighedSoundEvents resolve(SoundManager manager) { + WeighedSoundEvents events = super.resolve(manager); + if (events != null) { + return events; + } + file = RuntimeAssets.soundFile(identifier.getPath()); + if (file == null) { + return null; + } + // Streamed, so the file is read as it plays and never cached as a buffer. + sound = new Sound(identifier, new ConstantFloat(1.0F), new ConstantFloat(1.0F), 1, Sound.Type.FILE, true, false, 16); + return new WeighedSoundEvents(identifier, null); + } + + /** + * Forge and NeoForge call this through their sound instance extension, which it overrides + * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + */ + public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { + return openStream(buffers, sound.getPath(), looping); + } + + /** Streams the file found on disk, or the resource {@code path} when there is none. */ + public CompletableFuture openStream(SoundBufferLibrary buffers, Identifier path, boolean looping) { + Path source = file; + if (source == null) { + return buffers.getStream(path, looping); + } + return CompletableFuture.supplyAsync(() -> { + try { + InputStream in = new BufferedInputStream(Files.newInputStream(source)); + return looping ? new LoopingAudioStream(JOrbisAudioStream::new, in) : new JOrbisAudioStream(in); + } catch (IOException e) { + throw new CompletionException(e); + } + }, Util.nonCriticalIoPool()); + } +} From 7c8daa4c8023e7eb181d854dc1ea402c32a2cd74 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:14 +0800 Subject: [PATCH 0232/1461] Add client/workshop/WorkshopClient.java to the 26.2 shared sources --- .../client/workshop/WorkshopClient.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java new file mode 100644 index 00000000..a239b5fc --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSubmitPacket; +import com.jnhprojects.mcheli.network.TestFlightPacket; +import net.minecraft.client.Minecraft; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; + +/** + * What the workshop does beyond its screens: takes the player to a test flight and back, and sends + * a pack to the server. The screen stays alive during a flight, so the undo history and the view are + * as they were when the pilot gets out. + */ +public final class WorkshopClient { + /** If no vehicle has come after this long, the server turned the flight down and said why. */ + private static final int SPAWN_TIMEOUT_TICKS = 100; + /** About 1.2 MB a second, so a download-sized pack does not crowd out the game's own packets. */ + private static final int SUBMIT_CHUNKS_PER_TICK = 2; + + private static @Nullable WorkshopScreen waiting; + private static boolean airborne; + private static int ticks; + private static byte @Nullable [] submitting; + private static int submitted; + + private WorkshopClient() { + } + + static void startTestFlight(WorkshopScreen screen, String vehicle) { + waiting = screen; + airborne = false; + ticks = 0; + Minecraft.getInstance().gui.setScreen(null); + ModNetwork.sendToServer(new TestFlightPacket(vehicle)); + } + + /** Starts sending a pack's zip to the server, a few chunks a tick. */ + static void submit(byte[] zip) { + submitting = zip; + submitted = 0; + } + + /** How far along the zip being sent is, from 0 to 100, or -1 when none is. */ + static int submitProgress() { + byte[] zip = submitting; + return zip != null ? (int) (submitted * 100L / zip.length) : -1; + } + + public static void tick() { + tickSubmit(); + WorkshopScreen screen = waiting; + Minecraft mc = Minecraft.getInstance(); + if (screen == null || mc.player == null) { + return; + } + boolean flying = mc.player.getVehicle() instanceof AircraftEntity; + airborne |= flying; + ticks++; + if (airborne ? !flying : ticks > SPAWN_TIMEOUT_TICKS) { + waiting = null; + // Not over another screen the player has opened in flight, such as the pause menu. + if (mc.gui.screen() == null) { + mc.gui.setScreen(screen); + } + } + } + + private static void tickSubmit() { + byte[] zip = submitting; + if (zip == null) { + return; + } + for (int i = 0; i < SUBMIT_CHUNKS_PER_TICK && submitted < zip.length; i++) { + int end = Math.min(submitted + PackSubmitPacket.CHUNK_SIZE, zip.length); + ModNetwork.sendToServer(new PackSubmitPacket(submitted, zip.length, Arrays.copyOfRange(zip, submitted, end))); + submitted = end; + } + if (submitted >= zip.length) { + submitting = null; + } + } + + public static void loggedOut() { + submitting = null; + WorkshopScreen screen = waiting; + waiting = null; + if (screen != null) { + screen.releaseAssets(); + } + } +} From 12d8b25784d5d6eff3967f9e38a2425ff7e86cd6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:15 +0800 Subject: [PATCH 0233/1461] Add client/workshop/WorkshopHomeScreen.java to the 26.2 shared sources --- .../client/workshop/WorkshopHomeScreen.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java new file mode 100644 index 00000000..4fec9f18 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java @@ -0,0 +1,201 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.workshop.ModelImport; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopKind; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Where the workshop starts: the vehicles in the content packs on the left, ready to open, and on + * the right a form to start a new one from an OBJ file, which can be dropped on the window. + */ +public class WorkshopHomeScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int FORM_WIDTH = 190; + private static final int MAX_LIST_WIDTH = 300; + private static final int LINE_HEIGHT = 10; + + private final List entries; + private int scroll; + private WorkshopKind kind = WorkshopKind.HELICOPTER; + private String vehicleText = ""; + private String packText = ""; + private String modelText = ""; + private @Nullable Component error; + private @Nullable EditBox vehicleBox; + private @Nullable EditBox packBox; + private @Nullable EditBox modelBox; + + public WorkshopHomeScreen() { + super(Component.translatable("gui.mcheli.workshop.title")); + entries = WorkshopProject.list(MCHeli.packs().folder()); + } + + @Override + protected void init() { + listRows(); + form(); + } + + private int listWidth() { + return Math.clamp(width - FORM_WIDTH - 30, 100, MAX_LIST_WIDTH); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + private void listRows() { + int rows = visibleRows(); + for (int i = scroll; i < entries.size() && i < scroll + rows; i++) { + WorkshopProject.Entry entry = entries.get(i); + String suffix = entry.zipped() ? " (zip)" : ""; + addRenderableWidget(Button.builder(Component.literal(entry.pack() + " / " + entry.vehicle() + suffix), b -> open(entry)) + .bounds(10, TOP + (i - scroll) * ROW, listWidth(), 20).build()); + } + } + + private void form() { + int x = width - FORM_WIDTH - 10; + int y = TOP; + vehicleBox = box(x, y + 10, Component.translatable("gui.mcheli.workshop.new.vehicle"), vehicleText, 64); + vehicleBox.setResponder(text -> { + // The pack follows the vehicle's name until the player gives it its own. + if (packBox != null && packBox.getValue().equals(vehicleText)) { + packBox.setValue(text); + } + vehicleText = text; + }); + packBox = box(x, y + 44, Component.translatable("gui.mcheli.workshop.new.pack"), packText, 64); + packBox.setResponder(text -> packText = text); + addRenderableWidget(Button.builder(kindName(), b -> { + kind = WorkshopKind.values()[(kind.ordinal() + 1) % WorkshopKind.values().length]; + b.setMessage(kindName()); + }).bounds(x, y + 68, FORM_WIDTH, 20).build()); + modelBox = box(x, y + 102, Component.translatable("gui.mcheli.workshop.new.model"), modelText, 1024); + modelBox.setResponder(text -> modelText = text); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.new.create"), b -> create()) + .bounds(x, y + 126, FORM_WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.home.packs"), b -> minecraft.gui.setScreen(new WorkshopPacksScreen())) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private EditBox box(int x, int y, Component name, String value, int maxLength) { + EditBox box = new EditBox(font, x, y, FORM_WIDTH, 18, name); + box.setMaxLength(maxLength); + box.setValue(value); + return addRenderableWidget(box); + } + + private Component kindName() { + return Component.translatable("gui.mcheli.workshop.new.kind", Component.translatable("gui.mcheli.workshop.kind." + kind.id())); + } + + private void open(WorkshopProject.Entry entry) { + try { + minecraft.gui.setScreen(new WorkshopScreen(WorkshopProject.open(MCHeli.packs().folder(), entry))); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + private void create() { + String vehicle = vehicleText.trim(); + AircraftDefinition existing = MCHeli.content().any(vehicle); + if (existing != null && existing.pack().equals(ContentSource.ORIGINAL)) { + // The import's models come from a resource pack, which would win over this vehicle's. + error = Component.translatable("gui.mcheli.workshop.new.name_taken", vehicle); + return; + } + try { + Path obj = Path.of(modelText.trim().replace("\"", "")); + String author = minecraft.player != null ? minecraft.player.getGameProfile().name() : ""; + WorkshopProject project = WorkshopProject.create(MCHeli.packs().folder(), packText.trim(), kind, vehicle, author, + obj, ModelImport.findTexture(obj)); + minecraft.gui.setScreen(new WorkshopScreen(project)); + } catch (InvalidPathException e) { + error = Component.translatable("gui.mcheli.workshop.new.bad_path", modelText); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + /** A file dropped on the window fills in the form, its name standing in for the vehicle's. */ + @Override + public void onFilesDrop(List files) { + if (files.isEmpty() || modelBox == null || vehicleBox == null) { + return; + } + Path file = files.getFirst(); + modelBox.setValue(file.toString()); + if (vehicleText.isEmpty()) { + String name = file.getFileName().toString(); + int dot = name.lastIndexOf('.'); + vehicleBox.setValue((dot > 0 ? name.substring(0, dot) : name).toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "-")); + } + error = null; + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int max = Math.max(entries.size() - visibleRows(), 0); + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, max); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractRenderState(graphics, mouseX, mouseY, a); + graphics.centeredText(font, title, width / 2, 10, TEXT); + graphics.text(font, Component.translatable("gui.mcheli.workshop.home.open"), 10, TOP - 12, DIM); + if (entries.isEmpty()) { + graphics.text(font, Component.translatable("gui.mcheli.workshop.home.none"), 10, TOP, DIM); + } + int x = width - FORM_WIDTH - 10; + graphics.text(font, Component.translatable("gui.mcheli.workshop.home.new"), x, TOP - 12, DIM); + graphics.text(font, Component.translatable("gui.mcheli.workshop.new.vehicle"), x, TOP, DIM); + graphics.text(font, Component.translatable("gui.mcheli.workshop.new.pack"), x, TOP + 34, DIM); + graphics.text(font, Component.translatable("gui.mcheli.workshop.new.model"), x, TOP + 92, DIM); + int y = TOP + 150; + for (var line : font.split(Component.translatable("gui.mcheli.workshop.home.drop"), FORM_WIDTH)) { + graphics.text(font, line, x, y, DIM); + y += LINE_HEIGHT; + } + if (error != null) { + y += 2; + for (var line : font.split(error, FORM_WIDTH)) { + graphics.text(font, line, x, y, ERROR); + y += LINE_HEIGHT; + } + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From db8bbd64b497afc414890b89882de66260df2407 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:15 +0800 Subject: [PATCH 0234/1461] Open the packs folder through Util on 26.2 --- .../client/workshop/WorkshopPacksScreen.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java new file mode 100644 index 00000000..07005448 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java @@ -0,0 +1,218 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.ClientPacks; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.workshop.PackCatalog; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Util; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * The package browser: the packs in {@code mcheli/packs} and those the server in use sent, who made + * them and what is in them. A zip someone gave the player can be dropped on it to install. + * + *

This only ever reads the packs and adds to the folder. Removing one is left to the file manager + * the Open folder button opens, since a pack can be the only copy of someone's work. + */ +public class WorkshopPacksScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int GOOD = 0xFF80E080; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int LIST_WIDTH = 170; + private static final int LINE_HEIGHT = 10; + private static final int VEHICLES_SHOWN = 8; + + /** A pack in the list, and where it came from. */ + private record Row(PackCatalog.Info info, boolean fromServer) { + } + + private final List rows = new ArrayList<>(); + private final List problems = new ArrayList<>(); + private int scroll; + private int selected; + private @Nullable Component message; + private int messageColor = DIM; + + public WorkshopPacksScreen() { + super(Component.translatable("gui.mcheli.workshop.packs.title")); + read(); + } + + private void read() { + rows.clear(); + problems.clear(); + for (ContentPack pack : ClientPacks.INSTANCE.serverPacks()) { + try { + rows.add(new Row(PackCatalog.info(pack), true)); + } catch (IOException e) { + problems.add(new PackProblem(pack.id(), "could not be read: " + e)); + } + } + PackCatalog.Listing own = PackCatalog.read(MCHeli.packs().folder()); + for (PackCatalog.Info info : own.packs()) { + // A pack the player has that the server also uses is listed once, as the server's. + if (rows.stream().noneMatch(row -> row.info().hash().equals(info.hash()))) { + rows.add(new Row(info, false)); + } + } + problems.addAll(own.problems()); + selected = Math.clamp(selected, 0, Math.max(rows.size() - 1, 0)); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + @Override + protected void init() { + scroll = Math.clamp(scroll, 0, Math.max(rows.size() - visibleRows(), 0)); + for (int i = scroll; i < rows.size() && i < scroll + visibleRows(); i++) { + int index = i; + Button row = addRenderableWidget(Button.builder(Component.literal(rows.get(i).info().manifest().name()), b -> { + selected = index; + rebuildWidgets(); + }).bounds(10, TOP + (i - scroll) * ROW, LIST_WIDTH, 20).build()); + row.active = i != selected; + } + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.packs.open_folder"), b -> openFolder()) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private void openFolder() { + Path folder = MCHeli.packs().folder(); + try { + Files.createDirectories(folder); + Util.getPlatform().openPath(folder); + } catch (IOException e) { + say(Component.literal(e.toString()), ERROR); + } + } + + private void say(Component text, int color) { + message = text; + messageColor = color; + } + + @Override + public void onFilesDrop(List files) { + for (Path file : files) { + if (!file.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) { + say(Component.translatable("gui.mcheli.workshop.packs.not_zip", file.getFileName().toString()), ERROR); + continue; + } + try { + PackManifest installed = PackCatalog.install(file, MCHeli.packs().folder()); + say(Component.translatable("gui.mcheli.workshop.packs.installed", installed.name()), GOOD); + read(); + rebuildWidgets(); + } catch (WorkshopException e) { + say(Component.literal(e.getMessage()), ERROR); + } + } + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, Math.max(rows.size() - visibleRows(), 0)); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void onClose() { + minecraft.gui.setScreen(new WorkshopHomeScreen()); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractRenderState(graphics, mouseX, mouseY, a); + graphics.centeredText(font, title, width / 2, 10, TEXT); + if (rows.isEmpty()) { + graphics.text(font, Component.translatable("gui.mcheli.workshop.packs.none"), 10, TOP, DIM); + } + int x = 10 + LIST_WIDTH + 10; + int w = width - x - 10; + int y = TOP; + if (selected < rows.size()) { + y = details(graphics, rows.get(selected), x, y, w) + LINE_HEIGHT; + } + for (PackProblem problem : problems) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.problem", problem.pack(), problem.message()), x, y, w, ERROR); + } + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.drop"), x, y + 4, w, DIM); + if (message != null) { + wrapped(graphics, message, x, y + 4, w, messageColor); + } + } + + private int details(GuiGraphicsExtractor graphics, Row row, int x, int y, int w) { + PackCatalog.Info info = row.info(); + PackManifest m = info.manifest(); + y = wrapped(graphics, Component.literal(m.name() + " " + m.version()), x, y, w, TEXT); + if (!m.author().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.author", m.author()), x, y, w, TEXT); + } + if (!m.description().isEmpty()) { + y = wrapped(graphics, Component.literal(m.description()), x, y + 2, w, DIM); + } + y += 4; + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.id", m.id()), x, y, w, DIM); + // A small pack is still something, and 0.0 MB reads as an empty one. + String megabytes = "%.1f".formatted(Math.max(info.size() / 1048576.0, 0.1)); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.size", megabytes, ContentPack.shortHash(info.hash())), x, y, w, DIM); + Component source = row.fromServer() ? Component.translatable("gui.mcheli.workshop.packs.from_server") + : Component.literal(info.source().getFileName().toString() + (info.zipped() ? "" : "/")); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.source", source), x, y, w, DIM); + y += 4; + if (!info.vehicles().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.vehicles", info.vehicles().size(), shortList(info.vehicles())), x, y, w, TEXT); + } + if (!info.weapons().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.weapons", info.weapons().size(), shortList(info.weapons())), x, y, w, TEXT); + } + return y; + } + + private static String shortList(List names) { + if (names.size() <= VEHICLES_SHOWN) { + return String.join(", ", names); + } + return String.join(", ", names.subList(0, VEHICLES_SHOWN)) + ", ..."; + } + + private int wrapped(GuiGraphicsExtractor graphics, Component text, int x, int y, int w, int color) { + for (FormattedCharSequence line : font.split(text, w)) { + graphics.text(font, line, x, y, color); + y += LINE_HEIGHT; + } + return y; + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From b96df933fb0934d7a9f933744b3215618a50d443 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:16 +0800 Subject: [PATCH 0235/1461] Add client/workshop/WorkshopScreen.java to the 26.2 shared sources --- .../client/workshop/WorkshopScreen.java | 1646 +++++++++++++++++ 1 file changed, 1646 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java new file mode 100644 index 00000000..fbe83884 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java @@ -0,0 +1,1646 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.render.AircraftRenderState; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.workshop.AssetImport; +import com.jnhprojects.mcheli.workshop.EditHistory; +import com.jnhprojects.mcheli.workshop.PartField; +import com.jnhprojects.mcheli.workshop.PartType; +import com.jnhprojects.mcheli.workshop.TuningKey; +import com.jnhprojects.mcheli.workshop.WorkshopCamera; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopPart; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.FormattedCharSequence; +import org.joml.Matrix3x2fStack; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.TreeSet; +import java.util.function.Consumer; + +/** + * The workshop's editor. The vehicle turns in the middle, drawn by the game's own vehicle renderer + * through a GUI entity, so what shows is what will fly. The parts list is on the left, the values + * of what is selected on the right, and gizmos for every part lie over the model. + * + *

Gizmos are plain GUI drawing at the points {@link WorkshopCamera#project} gives. They always + * show, even when the part is inside the hull, which is where most seats and pivots are. + * + *

Left drag on empty space turns the view, right drag moves it, the wheel zooms. Dragging a + * handle moves the part in the plane of the screen and an arrow along its axis, snapping to a + * sixteenth of a block unless Shift is held. + */ +public class WorkshopScreen extends Screen { + private static final int LEFT = 112; + private static final int RIGHT = 144; + private static final int TOP = 26; + private static final int BOTTOM = 26; + private static final int LIST_ROW = 16; + private static final int FIELD_ROW = 20; + private static final int LABEL_ROW = 10; + + private static final int PANEL = 0xFF181818; + private static final int VIEW = 0xFF2A2F36; + private static final int GRID = 0xFF3A414A; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int WARNING = 0xFFFFC040; + private static final int INVALID = 0xFFFF6060; + private static final int X_COLOR = 0xFFFF5050; + private static final int Y_COLOR = 0xFF60E060; + private static final int Z_COLOR = 0xFF5090FF; + private static final int SELECTED = 0xFFFFFFFF; + private static final int HIT_BOX = 0xFF00E0FF; + private static final int AXIS = 0xFFFF60FF; + + private static final int HANDLE_RADIUS = 3; + private static final double PICK_DISTANCE = 6.0; + private static final double ARROW_LENGTH = 34.0; + private static final double SNAP = 16.0; + private static final int GRID_HALF_SIZE = 16; + private static final int GRID_STEP = 2; + private static final double ZOOM_STEP = 1.15; + + private static final int PREVIEW_SIZE = 64; + private static final int PLOT_HEIGHT = 56; + private static final float PITCH_SNAP = 20.0F; + private static final String DEFAULT_CURVE = "0:0.7, 0.25:0.85, 0.5:1, 0.75:1.15, 1:1.3"; + + private enum Tab { + PARTS, PAINT, SOUND, TUNING, PACK + } + + private enum Drag { + NONE, ORBIT, PAN, PLANE, AXIS_X, AXIS_Y, AXIS_Z, CURVE + } + + private record Label(Component text, int x, int y, int color) { + } + + private record WrappedLine(FormattedCharSequence text, int x, int y) { + } + + private record MergeKey(int line, String field, int component) { + } + + private final WorkshopProject project; + private final WorkshopCamera camera = new WorkshopCamera(); + private final EditHistory> history = new EditHistory<>(); + private final List

Movement is simulated wherever {@link #isLocalInstanceAuthoritative()} is true. With a player + * at the controls that is the pilot's client, exactly like vanilla boats, and the server checks + * the result through the regular vehicle move packet. Without a pilot the server simulates the + * vehicle on its own. Roll, throttle and key state are not part of the vanilla packet, so the + * pilot sends them separately and the server republishes them through synched data. + * + *

Seats are explicit. Vanilla only knows an ordered passenger list, so the seat each passenger + * occupies is kept in a synched seat map of entity ids. Seat zero is the pilot and decides who + * controls the vehicle; crew seats follow, then cargo racks. + */ +public abstract class AircraftEntity extends VehicleEntity { + private static final EntityDataAccessor DATA_TYPE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TEXTURE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_HP = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_THROTTLE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_REVERSE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_ROLL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_HOVERING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_GUNNER_MODE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_INPUT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_PARTS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SEATS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_SELECTION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_USE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TV_MISSILE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_FLARE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SINKING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_FUEL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + /** The UAV station a drone is flown from, by entity id, or 0. */ + private static final EntityDataAccessor DATA_STATION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_REPELLING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SEARCH_LIGHT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + + /** Marks a player flying a drone from a station, so a stale hide can be undone at login. */ + public static final String REMOTE_PILOT_TAG = "mcheli_remote_pilot"; + + // Bits of DATA_PARTS, the same assignment the original used for its part status word. + protected static final int PART_CANOPY = 0; + protected static final int PART_NOZZLE = 1; + protected static final int PART_LANDING_GEAR = 2; + protected static final int PART_WING = 3; + protected static final int PART_HATCH = 4; + + private static final int EMPTY = -1; + /** How far above its hit box the original drew a vehicle's model; see {@link #waterDepth}. */ + private static final float LEGACY_MODEL_LIFT = 0.35F; + /** How fast a cooked-off boat goes under, in blocks per tick (about 0.4 blocks a second). */ + private static final double SINK_SPEED = 0.02; + /** The bow-up pitch and the list a sinking boat settles into, in degrees. */ + private static final float SINK_PITCH = 14.0F; + private static final float SINK_LIST = 12.0F; + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + protected final ControlInput input = new ControlInput(); + public final AnimationState animation = new AnimationState(); + + /** Throttle in [0, 1] as seen by the simulating side. Other sides read {@link #DATA_THROTTLE}. */ + protected double throttle; + /** Reverse throttle in [0, 0.6] for vehicles with {@code EnableBack}. */ + protected float reverse; + protected float roll; + public float rollO; + protected int modeSwitchCooldown; + private int ticksAlive; + + private @Nullable AircraftDefinition cachedDefinition; + private String cachedName = ""; + private int cachedGeneration; + + protected @Nullable PartState canopy; + protected @Nullable PartState landingGear; + protected @Nullable PartState hatch; + private final List parts = new ArrayList<>(); + + private int[] seatIds = new int[0]; + private int pendingSeat = EMPTY; + private final Map savedSeats = new HashMap<>(); + private final Map lastSeats = new HashMap<>(); + /** The station a drone is flown from, and where its pilot goes back to. Server only. */ + private @Nullable UUID station; + private @Nullable Vec3 stationReturn; + /** Remote pilots leaving this tick, by entity id, with where they come back down. */ + private final Map remoteReturns = new HashMap<>(); + /** Where a workshop test flight started, or {@code null} for an ordinary vehicle. Server only. */ + private @Nullable Vec3 testFlightStart; + private boolean testFlightOver; + /** How far the ropes hang below their hooks, as a negative number; both sides let them out. */ + private float ropeLength; + private int repellingTicks; + private int lastRope; + /** Passengers are leaving one by one under parachutes. Server only. */ + private boolean droppingTroops; + + private @Nullable VehicleWeapons weapons; + private int @Nullable [] savedAmmo; + private @Nullable TvMissileEntity tvMissile; + + // Damage model + /** Damage factor of the extra hit box the last projectile ray struck, {@code lastBBDamageFactor}. */ + private float hitBoxFactor = 1.0F; + private int timeSinceHit; + /** The fuel load from which an aircraft counts as heavy: the H8K flying boat and up. */ + private static final int HEAVY_FUEL = 3000; + /** How long a wreck stays, burning, before it disappears: 30 seconds. */ + private static final int WRECK_TICKS = 600; + /** The wreck fire dies down over the last five seconds. */ + private static final int WRECK_FADE_TICKS = 100; + /** Ticks until a destroyed wreck disappears; above zero means destroyed. Server only. */ + private int despawnCount; + /** Ticks the client has seen this vehicle as a wreck, for the fire to die down on time. */ + private int wreckTicks; + /** Ticks since this aircraft was shot down, for the crash spin. Counted on both sides. */ + private int spinTicks; + private boolean exploded; + /** Blasts still to come while a wrecked boat cooks off, and ticks until the next one. */ + private int cookOffBlasts; + private int cookOffDelay; + private @Nullable Entity cookOffCause; + private int alertCount; + + /** Fuel burnt but not yet taken from the tank, which only holds whole units. Server only. */ + private double fuelBurnt; + /** Ticks a fuel truck nearby keeps this vehicle from burning fuel. Server only. */ + private int fuelSuppliedCount; + /** One bit per seat whose occupant has night vision on. Server only, not saved. */ + private int nightVisionSeats; + /** Fuel and parachute slots and the cargo chest. Only the server's copy holds anything. */ + private final VehicleInventory inventory = new VehicleInventory(this); + + // Crashes, measured from position so they work whichever side simulates the vehicle. + /** Blocks per tick into terrain, sideways, that count as a crash: about 25 m/s. */ + private static final double TERRAIN_CRASH_SPEED = 1.25; + /** + * How far a hull reaches past its main box, at most, when looking for neighbours: other + * vehicles are found by their main box, and the longest hulls stretch about this far from it. + */ + private static final double HULL_REACH = 24.0; + /** Closing speed between two vehicles that counts as a crash: about 10 m/s. */ + private static final double VEHICLE_CRASH_SPEED = 0.5; + /** Highest point since a ground vehicle left the ground, or NaN while it is on it. */ + private double airborneFromY = Double.NaN; + private @Nullable Vec3 lastTickPos; + private Vec3 lastVelocity = Vec3.ZERO; + private int collisionCooldown; + + protected AircraftEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_TYPE, ""); + builder.define(DATA_TEXTURE, ""); + builder.define(DATA_HP, 0); + builder.define(DATA_THROTTLE, 0.0F); + builder.define(DATA_REVERSE, 0.0F); + builder.define(DATA_ROLL, 0.0F); + builder.define(DATA_HOVERING, false); + builder.define(DATA_GUNNER_MODE, false); + builder.define(DATA_INPUT, (byte) 0); + builder.define(DATA_PARTS, 0); + builder.define(DATA_SEATS, ""); + builder.define(DATA_WEAPON_SELECTION, ""); + builder.define(DATA_WEAPON_USE, 0); + builder.define(DATA_TV_MISSILE, 0); + builder.define(DATA_FLARE, false); + builder.define(DATA_SINKING, false); + builder.define(DATA_FUEL, 0); + builder.define(DATA_STATION, 0); + builder.define(DATA_REPELLING, false); + builder.define(DATA_SEARCH_LIGHT, false); + } + + // Definition + + /** + * Looks the definition up by name. Returns {@code null} for vehicles whose content was removed. + * After a content reload the next call picks up the new definition object. + */ + public @Nullable AircraftDefinition definition() { + String name = typeName(); + int generation = MCHeli.content().generation(); + if (!name.equals(cachedName) || generation != cachedGeneration) { + boolean reloaded = name.equals(cachedName); + cachedName = name; + cachedGeneration = generation; + AircraftDefinition previous = cachedDefinition; + cachedDefinition = name.isEmpty() ? null : lookup(name); + if (cachedDefinition != null && cachedDefinition != previous) { + if (reloaded && weapons != null) { + // The same vehicle with new numbers: keep the rounds it carries. + savedAmmo = weapons.saveAmmo(); + } + onDefinitionChanged(cachedDefinition); + } + } + return cachedDefinition; + } + + /** Finds a definition of this entity's kind. */ + protected abstract @Nullable AircraftDefinition lookup(String name); + + /** Builds the per vehicle state that depends on the definition: seats, parts, animation arrays. */ + protected void onDefinitionChanged(AircraftDefinition def) { + if (seatIds.length != def.seatAndRackCount()) { + int[] resized = new int[def.seatAndRackCount()]; + Arrays.fill(resized, EMPTY); + System.arraycopy(seatIds, 0, resized, 0, Math.min(seatIds.length, resized.length)); + seatIds = resized; + } + parts.clear(); + canopy = def.canopies.isEmpty() ? null : addPart(PART_CANOPY, 3.5F, PartState.Sounds.CANOPY); + landingGear = def.landingGear.isEmpty() ? null : addPart(PART_LANDING_GEAR, 2.5F, PartState.Sounds.LANDING_GEAR); + hatch = def.hatches.isEmpty() ? null : addPart(PART_HATCH, 1.5F, PartState.Sounds.HATCH); + animation.resize(def); + weapons = new VehicleWeapons(this, def); + if (savedAmmo != null) { + weapons.loadAmmo(savedAmmo); + savedAmmo = null; + } + } + + protected PartState addPart(int bit, float speed, PartState.Sounds sounds) { + PartState state = new PartState(this, bit, speed, sounds); + // A vehicle that comes into view with a part already switched should not animate it. + state.force(state.status()); + parts.add(state); + return state; + } + + /** Sets which vehicle this is. Call once, right after construction, before adding to the level. */ + public void setType(AircraftDefinition definition) { + entityData.set(DATA_TYPE, definition.name()); + entityData.set(DATA_TEXTURE, definition.textures().getFirst()); + entityData.set(DATA_HP, definition.maxHp); + entityData.set(DATA_FUEL, definition.maxFuel); + refreshDimensions(); + definition(); + } + + public String typeName() { + return entityData.get(DATA_TYPE); + } + + public String texture() { + String texture = entityData.get(DATA_TEXTURE); + return texture.isEmpty() ? typeName() : texture; + } + + public int hp() { + return entityData.get(DATA_HP); + } + + /** + * {@code repair}: adds hit points up to the maximum. A wreck stays a wreck. Server only. + * + * @return whether anything was repaired + */ + public boolean repair(int amount) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || hp() >= def.maxHp) { + return false; + } + entityData.set(DATA_HP, Math.min(hp() + amount, def.maxHp)); + return true; + } + + /** + * {@code switchNextTextureName}: the next paint scheme the definition lists. Server only. + * + * @return false when the vehicle has only one + */ + public boolean nextTexture() { + AircraftDefinition def = definition(); + if (def == null || def.textures().size() < 2) { + return false; + } + List textures = def.textures(); + int next = (textures.indexOf(texture()) + 1) % textures.size(); + entityData.set(DATA_TEXTURE, textures.get(next)); + return true; + } + + // Fuel + + public int fuel() { + return entityData.get(DATA_FUEL); + } + + public int maxFuel() { + AircraftDefinition def = definition(); + return def != null ? def.maxFuel : 0; + } + + /** {@code getFuelP}: how full the tank is, 0 to 1. Vehicles without a tank read as full. */ + public float fuelRatio() { + int max = maxFuel(); + return max <= 0 ? 1.0F : (float) fuel() / max; + } + + /** {@code canUseFuel}: a vehicle without a tank never runs dry. */ + public boolean hasFuel() { + return maxFuel() <= 0 || fuel() > 1; + } + + /** Sets the tank outright; a drone leaves its station with 5%. Server only. */ + public void setFuelLevel(int fuel) { + entityData.set(DATA_FUEL, Math.clamp(fuel, 0, Math.max(maxFuel(), 0))); + } + + /** + * Pours up to {@code amount} into the tank. Server only. + * + * @return how much went in + */ + public int refuel(int amount) { + int added = Math.clamp(maxFuel() - fuel(), 0, Math.max(amount, 0)); + if (added > 0) { + entityData.set(DATA_FUEL, fuel() + added); + } + return added; + } + + // Night vision + + /** A crew member switched night vision on or off. Server only. */ + public void setNightVision(Player player, boolean on) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (seat < 0 || seat >= 31) { + return; + } + if (on && def != null && def.nightVision && !isDestroyed()) { + nightVisionSeats |= 1 << seat; + } else if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(player); + } + } + + /** Keeps night vision on for everyone in the crew who switched it on, see {@link NightVision}. */ + private void tickNightVision() { + if (nightVisionSeats == 0) { + return; + } + if (isDestroyed()) { + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null) { + NightVision.end(crew); + } + } + nightVisionSeats = 0; + return; + } + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (hasNightVision(seat) && crew != null) { + NightVision.topUp(crew); + } + } + } + + /** Only the first 31 seats can switch it on; racks and the like come after them anyway. */ + private boolean hasNightVision(int seat) { + return seat >= 0 && seat < 31 && (nightVisionSeats & 1 << seat) != 0; + } + + /** + * {@code updateFuel}: once a second the engine burns its {@code FuelConsumption}, scaled by the + * throttle and topping out at 70% throttle. Nothing burns next to a fuel truck or with the + * server's infinite fuel on. + */ + private void burnFuel(AircraftDefinition def) { + if (def.maxFuel <= 0 || isDestroyed()) { + return; + } + if (fuelSuppliedCount > 0) { + fuelSuppliedCount--; + return; + } + if (ticksAlive % 20 != 0 || fuel() <= 1 || throttle <= 0.0 || ServerConfig.infiniteFuel()) { + return; + } + fuelBurnt += Math.min(throttle * 1.4, 1.0) * def.fuelConsumption; + if (fuelBurnt > 1.0) { + int whole = (int) fuelBurnt; + fuelBurnt -= whole; + entityData.set(DATA_FUEL, Math.max(fuel() - whole, 0)); + } + } + + /** + * {@code supplyFuel}: a fuel truck ({@code FuelSupplyRange}) tops up every vehicle within range, + * 30 units every half second. While it is on the ground it only reaches vehicles that are on + * the ground too. + */ + private void supplyFuel(AircraftDefinition def) { + float range = def.fuelSupplyRange; + if (range <= 0.0F || ticksAlive % 10 != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + if (other == this) { + continue; + } + if (!onGround() || other.onGround()) { + other.refuel(30); + } + other.fuelSuppliedCount = 40; + } + } + + // Inventory and supply + + public VehicleInventory inventory() { + return inventory; + } + + /** + * {@code canSupply}: fuel cans and resupply only work while the vehicle stands on something, + * within three blocks; a vehicle that cannot float must not be in the water either. + */ + public boolean canSupply() { + AircraftDefinition def = definition(); + return def != null && hasBlockBelow(1, 3, false) && (def.floats || !isInWater()); + } + + /** {@code openGui}: shows a crew member the vehicle screen. Server only. */ + public void openScreen(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || player.getVehicle() != this) { + return; + } + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new VehicleMenu(id, playerInventory, this), displayName(def))); + } + + /** + * {@code openInventory}: shows the cargo chest of a vehicle with an {@code InventorySize}. + * Server only. + * + * @return false when the vehicle has none + */ + public boolean openCargo(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || def.cargoRows() <= 0 || isDestroyed() || !inventory.stillValid(player)) { + return false; + } + int rows = Math.min(def.cargoRows(), 6); + MenuType type = switch (rows) { + case 1 -> MenuType.GENERIC_9x1; + case 2 -> MenuType.GENERIC_9x2; + case 3 -> MenuType.GENERIC_9x3; + case 4 -> MenuType.GENERIC_9x4; + case 5 -> MenuType.GENERIC_9x5; + default -> MenuType.GENERIC_9x6; + }; + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new ChestMenu(type, id, playerInventory, inventory.cargo(), rows), + displayName(def))); + return true; + } + + /** The vehicle's name as its item shows it. */ + private static Component displayName(AircraftDefinition def) { + return Component.translatableWithFallback("item.mcheli." + def.name(), def.displayName); + } + + /** + * {@code supplyAmmoToOtherAircraft}: an ammunition truck ({@code AmmoSupplyRange}) tops up the + * weapons of every vehicle within range that stands on something, a tenth of each weapon's + * maximum every two seconds. + */ + private void supplyAmmo(AircraftDefinition def) { + float range = def.ammoSupplyRange; + if (range <= 0.0F || ticksAlive % SupplyRules.AMMO_TRUCK_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + VehicleWeapons theirs = other.weapons(); + if (other == this || theirs == null || other.isDestroyed() || !other.canSupply()) { + continue; + } + for (WeaponSet set : theirs.sets()) { + int rounds = SupplyRules.roundsFromTruck(set.ammo() + set.restAmmo(), set.maxAmmo()); + if (rounds > 0) { + set.setRestAmmo(set.restAmmo() + rounds); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + } + } + } + } + + /** {@code repairOtherAircraft}: a repair vehicle ({@code RepairOtherVehicles}) mends those in range. */ + private void repairOthers(AircraftDefinition def) { + float range = def.repairOtherVehiclesRange; + if (range <= 0.0F || ticksAlive % SupplyRules.REPAIR_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + AircraftDefinition theirs = other.definition(); + if (other != this && theirs != null) { + other.repair(SupplyRules.repair(other.hp(), theirs.maxHp, def.repairOtherVehiclesValue)); + } + } + } + + /** Whether the flight model holds a hover: in hover mode, or while the pilot mans the guns. */ + public boolean isHovering() { + return isHoveringMode() || isGunnerMode(); + } + + /** The hover mode the helicopter pilot switches with jump. */ + public boolean isHoveringMode() { + return entityData.get(DATA_HOVERING); + } + + protected void setHovering(boolean hovering) { + entityData.set(DATA_HOVERING, hovering); + } + + // Pilot gunner mode + + /** + * Pilot gunner mode ({@code EnableGunnerMode}): the pilot leaves the controls to man the + * weapons from the camera position. Helicopters hold a hover; planes level off and circle. + */ + public boolean isGunnerMode() { + return entityData.get(DATA_GUNNER_MODE); + } + + /** {@code canSwitchGunnerMode}: the kinds add their own attitude and speed limits. */ + public boolean canSwitchGunnerMode() { + AircraftDefinition def = definition(); + if (def == null || !def.gunnerMode || !isCanopyClosed() || isDestroyed() || isHoveringMode()) { + return false; + } + // Without ConcurrentGunnerMode a player in the gunner seat has the guns already. + return def.concurrentGunnerMode || !(occupant(1) instanceof Player); + } + + /** Server side. Leaving gunner mode is always allowed; entering it only when {@link #canSwitchGunnerMode}. */ + public void setGunnerMode(boolean on) { + if (!on || canSwitchGunnerMode()) { + entityData.set(DATA_GUNNER_MODE, on); + } + } + + /** Whether {@code entity} is the pilot and currently in gunner mode. */ + public boolean isGunnerMode(Entity entity) { + return isGunnerMode() && seatOf(entity) == 0; + } + + /** Drops gunner mode once its conditions no longer hold. Server side, every tick. */ + private void checkGunnerMode(AircraftDefinition def) { + if (isGunnerMode() && (occupant(0) == null || isDestroyed() + || (!def.concurrentGunnerMode && occupant(1) instanceof Player))) { + entityData.set(DATA_GUNNER_MODE, false); + } + } + + // Throttle, roll and input + + /** Throttle for animation and sound, valid on every side. */ + public float visibleThrottle() { + return isSimulatingHere() ? (float) throttle : entityData.get(DATA_THROTTLE); + } + + public float visibleReverse() { + return isSimulatingHere() ? reverse : entityData.get(DATA_REVERSE); + } + + public float roll() { + return isSimulatingHere() ? roll : entityData.get(DATA_ROLL); + } + + public float roll(float partialTicks) { + return Mth.rotLerp(partialTicks, rollO, roll()); + } + + public void setRoll(float roll) { + this.roll = roll; + } + + protected boolean isSimulatingHere() { + return isLocalInstanceAuthoritative() && !isPassenger(); + } + + public ControlInput input() { + return input; + } + + /** + * Applies the pilot's view change for one frame. Only called on the pilot's client, and only + * for kinds that steer with the mouse. + * + * @param dYaw change of the player's yaw since the last frame, in degrees + * @param dPitch change of the player's pitch since the last frame, in degrees + */ + public void applyPilotLook(float dYaw, float dPitch) { + } + + /** Whether the pilot steers with the mouse, as in helicopters and planes. */ + public boolean steersWithMouse() { + return false; + } + + /** Server side entry point for the pilot's control packet. */ + public void acceptPilotState(float throttle, float reverse, float roll, boolean hovering, byte keys) { + this.throttle = Mth.clamp(throttle, 0.0F, 1.0F); + this.reverse = Mth.clamp(reverse, 0.0F, 1.0F); + this.roll = AircraftRotation.wrap(roll); + input.read(keys); + entityData.set(DATA_THROTTLE, (float) this.throttle); + entityData.set(DATA_REVERSE, this.reverse); + entityData.set(DATA_ROLL, this.roll); + entityData.set(DATA_HOVERING, hovering); + entityData.set(DATA_INPUT, keys); + } + + public float reverse() { + return reverse; + } + + // Tick + + /** + * Makes this a workshop test flight: it takes no damage, is not saved with the world, and goes + * away once its pilot gets out, putting them back where they started. + */ + public void startTestFlight(Vec3 start) { + testFlightStart = start; + } + + @Override + public boolean shouldBeSaved() { + return testFlightStart == null && super.shouldBeSaved(); + } + + @Override + public void tick() { + if (testFlightOver) { + discard(); + return; + } + rollO = roll(); + if (getHurtTime() > 0) { + setHurtTime(getHurtTime() - 1); + } + super.tick(); + interpolation.interpolate(); + ticksAlive++; + lastSeats.clear(); + remoteReturns.clear(); + if (modeSwitchCooldown > 0) { + modeSwitchCooldown--; + } + + AircraftDefinition def = definition(); + if (def == null) { + // Content for this vehicle no longer exists. Keep the entity inert rather than crash. + setDeltaMovement(Vec3.ZERO); + return; + } + if (timeSinceHit > 0) { + timeSinceHit--; + } + if (alertCount > 0) { + alertCount--; + } + boolean groundedBefore = onGround(); + double fallBefore = getDeltaMovement().y; + tickDestroyed(); + + if (!level().isClientSide()) { + supplyFuel(def); + supplyAmmo(def); + repairOthers(def); + burnFuel(def); + if (def.maxFuel > 0 && fuel() < def.maxFuel && !isDestroyed()) { + inventory.pourFuelCans(ticksAlive); + } + tickNightVision(); + tickStation(); + } + tickRopes(def); + + if (isSimulatingHere()) { + if (!(getControllingPassenger() instanceof Player) || isDestroyed()) { + input.clear(); + } + if (isRepelling()) { + // {@code MCH_AircraftClientTickHandler}: the pilot holds a hover while the ropes are out. + input.throttleUp = false; + input.throttleDown = false; + input.left = false; + input.right = false; + } + if (!hasFuel()) { + // An empty tank: the throttle runs down by itself, as with the pilot's hand off it. + input.throttleUp = false; + } + if (entityData.get(DATA_SINKING)) { + sink(); + } else { + simulate(); + } + float[] kick = weapons != null ? weapons.recoilKick() : null; + if (kick != null) { + setXRot(getXRot() + kick[0]); + roll += kick[1]; + } + if (!level().isClientSide()) { + entityData.set(DATA_THROTTLE, (float) throttle); + entityData.set(DATA_REVERSE, reverse); + entityData.set(DATA_ROLL, roll); + entityData.set(DATA_INPUT, input.write()); + } + } else { + if (!isLocalInstanceAuthoritative()) { + input.read(entityData.get(DATA_INPUT)); + } + setDeltaMovement(Vec3.ZERO); + } + for (PartState part : parts) { + part.tick(); + } + if (!level().isClientSide()) { + updatePartsServer(def); + checkGunnerMode(def); + } + animation.tick(this, def); + tickAnimation(); + if (weapons != null) { + weapons.tick(); + if (!level().isClientSide()) { + entityData.set(DATA_FLARE, weapons.flares.isActive()); + warnIncomingMissiles(); + } + } + if (level().isClientSide()) { + damageSmoke(def); + VehicleParticles.tick(this, def); + } else { + tickImpacts(def, groundedBefore, fallBefore); + } + } + + /** + * Server side crashes. A wreck explodes when it lands; an intact vehicle takes damage when it + * meets the ground steeply tilted ({@code onUpdate_CollisionGroundDamage}), sits under water + * without floats, flies into terrain, or rams another vehicle. + */ + private void tickImpacts(AircraftDefinition def, boolean groundedBefore, double fallBefore) { + Vec3 velocity = lastTickPos != null ? position().subtract(lastTickPos) : Vec3.ZERO; + Vec3 previous = lastVelocity; + lastTickPos = position(); + lastVelocity = velocity; + if (collisionCooldown > 0) { + collisionCooldown--; + } + double speed = Math.max(velocity.length(), previous.length()); + + if (isDestroyed()) { + boolean falling = fallBefore < -0.2 || previous.y < -0.2; + boolean landed = onGround() && !groundedBefore; + boolean smashed = horizontalCollision && previous.horizontalDistance() > 0.5; + if (!exploded && (landed && falling || smashed)) { + crashExplosion(); + } + return; + } + + if (isNearGround(3)) { + float roll = Math.abs(Mth.wrapDegrees(roll())); + float pitch = Math.abs(Mth.wrapDegrees(getXRot())); + if (roll > crashTilt() || pitch > crashTilt()) { + float damage = roll + pitch; + damage *= damage < 90.0F ? 0.4F * (float) velocity.length() : 0.4F; + if (damage > 1.0F && random.nextInt(4) == 0) { + hurtServer((ServerLevel) level(), damageSources().inWall(), damage); + } + } + } + if (ticksAlive % 30 == 0 && !def.floats && level().getFluidState( + BlockPos.containing(getX() + 0.5, getY() + 1.5 + def.submergedDamageHeight, getZ() + 0.5)).is(FluidTags.WATER)) { + hurtServer((ServerLevel) level(), damageSources().inWall(), Math.max(def.maxHp / 10, 1)); + } + + if (takesGroundImpacts()) { + groundImpacts(def, previous); + } + + // Not in the original: flying into terrain or another vehicle at speed is a crash. + if (horizontalCollision && previous.horizontalDistance() > TERRAIN_CRASH_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), crashDamage(previous.horizontalDistance())); + } + if (speed > VEHICLE_CRASH_SPEED && collisionCooldown == 0) { + // A blocked move can leave the hulls up to a tick of travel apart; when the vehicle + // stopped short this tick, look that far for what stopped it. + boolean stopped = velocity.length() < previous.length() * 0.5; + double reach = stopped ? 0.5 + previous.length() : 0.5; + for (AircraftEntity other : solidNeighbours(hullBounds().inflate(reach))) { + if (!hullsTouch(other, reach)) { + continue; + } + double closing = velocity.subtract(other.lastVelocity).length(); + closing = Math.max(closing, previous.subtract(other.lastVelocity).length()); + if (closing > VEHICLE_CRASH_SPEED && other.collisionCooldown == 0) { + collisionCooldown = 20; + other.collisionCooldown = 20; + float damage = crashDamage(closing); + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), damage); + other.hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), damage); + break; + } + } + } + } + + /** {@code getGiveDamageRot}: how far a vehicle may lean near the ground before it scrapes. */ + protected float crashTilt() { + return 40.0F; + } + + /** Crash damage grows with the square of the impact speed, like the energy involved. */ + private static float crashDamage(double speed) { + return (float) (speed * speed * 60.0); + } + + /** Whether hitting walls and falling hurt this vehicle. Tanks and cars, which live on the ground. */ + protected boolean takesGroundImpacts() { + return false; + } + + /** + * A ground vehicle that hits a wall or lands after a long drop takes a share of its hit points + * by the speed or the height, see {@link GroundImpact}. The same height costs a car and a tank + * the same share before armour. + */ + private void groundImpacts(AircraftDefinition def, Vec3 previous) { + if (horizontalCollision && previous.horizontalDistance() > GroundImpact.WALL_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), GroundImpact.wall(previous.horizontalDistance()) * def.maxHp); + } + if (isInWater() || isPassenger()) { + airborneFromY = Double.NaN; + } else if (!onGround()) { + airborneFromY = Double.isNaN(airborneFromY) ? getY() : Math.max(airborneFromY, getY()); + } else if (!Double.isNaN(airborneFromY)) { + double drop = airborneFromY - getY(); + airborneFromY = Double.NaN; + if (drop > GroundImpact.SAFE_DROP) { + hurtServer((ServerLevel) level(), damageSources().fall(), GroundImpact.drop(drop) * def.maxHp); + } + } + } + + /** {@code MCH_MissileDetector}: the crew hears a warning while a missile homes on them. */ + private void warnIncomingMissiles() { + if (alertCount > 0 || isDestroyed() || weapons == null || !weapons.flares.isFitted() || ticksAlive % 5 != 0) { + return; + } + boolean tracked = !level().getEntitiesOfClass(ProjectileEntity.class, getBoundingBox().inflate(60.0), + p -> p.target() != null && (p.target() == this || hasPassenger(p.target()))).isEmpty(); + if (tracked) { + alertCount = 20; + ModSounds.playAt(this, "alert", 1.0F, 1.0F); + } + } + + /** One tick of control and physics on the simulating side. */ + protected abstract void simulate(); + + /** Visual state that every side updates, for example rotor angles. */ + protected void tickAnimation() { + } + + /** Ticks since this entity instance was created, used to delay controls right after spawning. */ + protected int ticksAlive() { + return ticksAlive; + } + + // Moving parts + + int partBits() { + return entityData.get(DATA_PARTS); + } + + void setPartBit(int bit, boolean on) { + int bits = entityData.get(DATA_PARTS); + entityData.set(DATA_PARTS, on ? bits | (1 << bit) : bits & ~(1 << bit)); + } + + public @Nullable PartState canopy() { + return canopy; + } + + public @Nullable PartState landingGear() { + return landingGear; + } + + public @Nullable PartState hatch() { + return hatch; + } + + public boolean isCanopyClosed() { + return canopy == null || canopy.isOff(); + } + + protected void openCanopy() { + if (canopy != null && modeSwitchCooldown <= 0) { + canopy.set(true, true); + modeSwitchCooldown = 20; + } + } + + protected void closeCanopy() { + if (canopy != null && modeSwitchCooldown <= 0 && canopy.status()) { + canopy.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean isLandingGearUp() { + return landingGear != null && landingGear.status(); + } + + /** Gear can be raised once the vehicle is at least ten blocks above anything solid. */ + public boolean canRaiseLandingGear() { + return landingGear != null && landingGear.angle() < 1.0F && !landingGear.status() && !hasBlockBelow(3, 10, true); + } + + public boolean canLowerLandingGear() { + return landingGear != null && landingGear.angle() >= 89.0F && landingGear.status(); + } + + public void toggleLandingGear() { + if (landingGear == null || modeSwitchCooldown > 0) { + return; + } + if (canRaiseLandingGear()) { + landingGear.set(true, true); + modeSwitchCooldown = 20; + } else if (canLowerLandingGear()) { + landingGear.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean canFoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOff(); + } + + public boolean canUnfoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOn(); + } + + /** The fold key: hatches by default, planes fold or sweep their wings instead. */ + public void toggleFold() { + if (canFoldHatch()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldHatch()) { + hatch.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Throttle below which the landing gear comes down by itself near the ground. */ + protected float autoLowerGearThrottle() { + return 0.8F; + } + + /** {@code updateParts}: automatic landing gear, plus whatever a kind adds. Server only. */ + protected void updatePartsServer(AircraftDefinition def) { + if (landingGear == null) { + return; + } + if (!landingGear.status() && landingGear.factor() <= 0.1F) { + // Amphibians retract the gear when they settle on water. + boolean onWater = isInWater() || isWaterBelow(20); + if (def.floats && onWater && (throttle <= 0.8 || onGround() || hasBlockBelow(3, 20, true))) { + landingGear.set(true, true); + } + } else if (landingGear.status() && landingGear.factor() >= 0.9F) { + boolean ground = hasBlockBelow(3, 10, true); + if (throttle < autoLowerGearThrottle() && ground) { + if (!def.floats || !isWaterBelow(150)) { + landingGear.set(false, true); + } + } else if (vtolMode() == 2 && ground) { + landingGear.set(false, true); + } + } + } + + /** 0 off, 1 tilting, 2 fully in VTOL mode. Only planes have one. */ + public int vtolMode() { + return 0; + } + + // Seats + + /** Seat (or rack) index of {@code entity}, or -1. */ + public int seatOf(Entity entity) { + int id = entity.getId(); + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == id) { + return i; + } + } + return EMPTY; + } + + public @Nullable Entity occupant(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] == EMPTY) { + return null; + } + int id = seatIds[seat]; + for (Entity passenger : getPassengers()) { + if (passenger.getId() == id) { + return passenger; + } + } + return null; + } + + public @Nullable SeatDefinition seat(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && seat >= 0 && seat < def.seatCount() ? def.seats.get(seat) : null; + } + + private boolean isSeatFree(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] != EMPTY) { + return false; + } + AircraftDefinition def = definition(); + if (def != null) { + for (int[] group : def.exclusiveSeats) { + if (contains(group, seat)) { + for (int other : group) { + if (other != seat && other >= 0 && other < seatIds.length && seatIds[other] != EMPTY) { + return false; + } + } + } + } + } + return true; + } + + private static boolean contains(int[] values, int value) { + for (int v : values) { + if (v == value) { + return true; + } + } + return false; + } + + /** First crew seat a player boarding by hand would take. Drones keep their pilot seat for the remote. */ + private int firstFreeCrewSeat() { + AircraftDefinition def = definition(); + if (def == null) { + return EMPTY; + } + for (int i = def.uav ? 1 : 0; i < def.seatCount(); i++) { + if (isSeatFree(i)) { + return i; + } + } + return EMPTY; + } + + private void assignSeat(Entity entity, int seat) { + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == entity.getId()) { + seatIds[i] = EMPTY; + if (weapons != null) { + weapons.onLeft(i); + } + } + } + if (seat >= 0 && seat < seatIds.length) { + seatIds[seat] = entity.getId(); + } + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onSeated(entity, seat); + weapons.resendAmmo(); + } + } + + private String encodeSeats() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < seatIds.length; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(seatIds[i]); + } + return sb.toString(); + } + + private void decodeSeats(String encoded) { + if (encoded.isEmpty()) { + Arrays.fill(seatIds, EMPTY); + return; + } + String[] parts = encoded.split(","); + if (parts.length != seatIds.length) { + seatIds = new int[parts.length]; + } + for (int i = 0; i < parts.length; i++) { + try { + seatIds[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + seatIds[i] = EMPTY; + } + } + } + + /** Moves {@code entity} to the next free crew seat, wrapping around. Server only. */ + public void switchSeat(Entity entity) { + AircraftDefinition def = definition(); + int current = seatOf(entity); + if (def == null || current < 0 || current >= def.seatCount() || current == 0 && station != null) { + // A remote pilot stays at the controls; they are not really aboard. + return; + } + int count = def.seatCount(); + seatIds[current] = EMPTY; + for (int step = 1; step < count; step++) { + int seat = (current + step) % count; + if (def.uav && seat == 0) { + continue; + } + if (isSeatFree(seat)) { + assignSeat(entity, seat); + if (seat == 0) { + closeCanopy(); + } + return; + } + } + assignSeat(entity, current); + } + + // UAV stations + + /** Whether a player at a station could take this drone's controls now. */ + public boolean canTakeRemotePilot() { + AircraftDefinition def = definition(); + return def != null && def.uav && !def.targetDrone && !isDestroyed() && isSeatFree(0); + } + + /** + * Seats {@code player}, who sits at {@code from}, at this drone's controls. The pilot is hidden + * and takes no harm while flying, since in the original they never left the station. Server only. + */ + public boolean takeRemotePilot(ServerPlayer player, UavStationEntity from) { + if (!canTakeRemotePilot()) { + return false; + } + pendingSeat = 0; + boolean seated = player.startRiding(this, true, true); + pendingSeat = EMPTY; + if (!seated) { + return false; + } + station = from.getUUID(); + stationReturn = from.returnPosition(); + entityData.set(DATA_STATION, from.getId()); + player.addTag(REMOTE_PILOT_TAG); + player.setInvisible(true); + player.setInvulnerable(true); + return true; + } + + /** Sends the remote pilot back to the station, if one is flying. Server only. */ + public void releaseRemotePilot() { + Entity pilot = occupant(0); + if (station != null && pilot != null) { + pilot.stopRiding(); + } + } + + /** The station this drone is flown from, as the client sees it. */ + public @Nullable UavStationEntity stationEntity() { + return level().getEntity(entityData.get(DATA_STATION)) instanceof UavStationEntity s ? s : null; + } + + /** Flat distance to the station this drone is flown from, or -1 without one. */ + public double stationDistance() { + UavStationEntity from = stationEntity(); + if (from == null) { + return -1.0; + } + double dx = getX() - from.getX(); + double dz = getZ() - from.getZ(); + return Math.sqrt(dx * dx + dz * dz); + } + + /** Whether {@code entity} flies this drone from a station rather than sitting in it. */ + public boolean isRemotePilot(Entity entity) { + return (station != null || entityData.get(DATA_STATION) != 0) && occupant(0) == entity; + } + + private void endRemoteControl(Entity pilot) { + UavStationEntity from = level() instanceof ServerLevel server && station != null + && server.getEntity(station) instanceof UavStationEntity s ? s : null; + Vec3 home = from != null ? from.returnPosition() : stationReturn; + if (home != null) { + remoteReturns.put(pilot.getId(), home); + } + clearRemotePilot(pilot); + if (from != null) { + from.setLinked(false); + } + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + } + + /** Undoes what {@link #takeRemotePilot} did to the player. */ + public static void clearRemotePilot(Entity pilot) { + if (pilot.removeTag(REMOTE_PILOT_TAG)) { + pilot.setInvisible(false); + pilot.setInvulnerable(false); + } + } + + /** + * {@code updateUAV}: the link breaks when the station is gone, and flying out of its range + * loses the drone, as in the original. + */ + private void tickStation() { + if (station == null || !(level() instanceof ServerLevel server)) { + return; + } + Entity pilot = occupant(0); + if (pilot == null) { + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + return; + } + UavStationEntity from = server.getEntity(station) instanceof UavStationEntity s && s.isAlive() ? s : null; + if (from == null) { + pilot.stopRiding(); + return; + } + entityData.set(DATA_STATION, from.getId()); + if (!from.inRange(this) && !isDestroyed()) { + if (pilot instanceof Player player) { + player.sendOverlayMessage(Component.translatable("message.mcheli.uav.lost")); + } + pilot.stopRiding(); + hurtServer(server, damageSources().fellOutOfWorld(), hp() + 10.0F); + } + } + + @Override + public @Nullable LivingEntity getControllingPassenger() { + // A wreck is nobody's to fly; the server lets it fall, so its crash is seen the same way by everyone. + return occupant(0) instanceof Player player && !isDestroyed() ? player : null; + } + + @Override + protected boolean canAddPassenger(Entity passenger) { + AircraftDefinition def = definition(); + if (def == null || !def.canRide) { + return false; + } + if (pendingSeat >= 0) { + return isSeatFree(pendingSeat); + } + // Coming back aboard after a reload, a remote pilot included. + Integer saved = savedSeats.get(passenger.getUUID()); + if (saved != null && isSeatFree(saved)) { + return true; + } + // Cargo only boards through a rack, never into a crew seat. + return !(passenger instanceof AircraftEntity || passenger instanceof ContainerEntity) && firstFreeCrewSeat() >= 0; + } + + @Override + protected void addPassenger(Entity passenger) { + super.addPassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = pendingSeat; + pendingSeat = EMPTY; + if (seat < 0) { + Integer saved = savedSeats.remove(passenger.getUUID()); + seat = saved != null && isSeatFree(saved) ? saved : firstFreeCrewSeat(); + } + assignSeat(passenger, seat); + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = seatOf(passenger); + if (seat >= 0) { + if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(passenger); + } + lastSeats.put(passenger.getId(), seat); + seatIds[seat] = EMPTY; + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onLeft(seat); + } + } + if (seat == 0) { + input.clear(); + if (station != null) { + endRemoteControl(passenger); + } + if (testFlightStart != null) { + remoteReturns.put(passenger.getId(), testFlightStart); + testFlightOver = true; + } + } + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + AircraftDefinition def = definition(); + int seat = seatOf(passenger); + if (def == null || seat < 0 || seat >= def.seatAndRackCount()) { + return super.getPassengerAttachmentPoint(passenger, dimensions, scale); + } + Vector3f p = seatPosition(def, seat, 1.0F); + return new Vec3(p.x, p.y, p.z); + } + + /** World offset of a seat from the vehicle origin. Turret seats turn with the pilot's view. */ + public Vector3f seatPosition(AircraftDefinition def, int seat, float partialTicks) { + Vector3 pos; + boolean turret; + if (seat < def.seatCount()) { + SeatDefinition s = def.seats.get(seat); + pos = s.position(); + turret = s.rotatingSeat(); + } else { + RackDefinition rack = def.rack(seat); + pos = rack.position(); + turret = rack.rotatingSeat(); + } + return toWorldOffset(turret ? onTurret(def, pos, partialTicks) : pos); + } + + /** Rotates a hull-space point around {@code TurretPosition} by the pilot's view yaw. */ + public Vector3 onTurret(AircraftDefinition def, Vector3 pos, float partialTicks) { + Vector3 t = def.turretPosition; + Vector3f v = AircraftRotation.toWorld(animation.seatYaw(0, partialTicks), 0.0F, 0.0F, + pos.x() - t.x(), pos.y() - t.y(), pos.z() - t.z()); + return new Vector3(v.x + t.x(), v.y + t.y(), v.z + t.z()); + } + + /** Rotates a hull-space point into the world frame, relative to the vehicle origin. */ + public Vector3f toWorldOffset(Vector3 local) { + return AircraftRotation.toWorld(getYRot(), getXRot(), roll(), local.x(), local.y(), local.z()); + } + + public Vec3 toWorld(Vector3 local) { + Vector3f v = toWorldOffset(local); + return position().add(v.x, v.y, v.z); + } + + @Override + protected void positionRider(Entity passenger, MoveFunction moveFunction) { + super.positionRider(passenger, moveFunction); + AircraftDefinition def = definition(); + if (def != null && passenger instanceof AircraftEntity cargo) { + RackDefinition rack = def.rack(seatOf(passenger)); + float yaw = rack != null ? rack.yaw() : 0.0F; + float pitch = rack != null ? rack.pitch() : 0.0F; + cargo.setYRot(getYRot() + yaw); + cargo.setXRot(getXRot() + pitch); + cargo.roll = roll(); + cargo.setDeltaMovement(Vec3.ZERO); + } else if (def != null && passenger instanceof ContainerEntity container) { + RackDefinition rack = def.rack(seatOf(passenger)); + container.setYRot(getYRot() + (rack != null ? rack.yaw() : 0.0F)); + container.setDeltaMovement(Vec3.ZERO); + } + } + + @Override + public Vec3 getDismountLocationForPassenger(LivingEntity passenger) { + Vec3 home = remoteReturns.remove(passenger.getId()); + if (home != null) { + return home; + } + AircraftDefinition def = definition(); + Integer seat = lastSeats.get(passenger.getId()); + if (def != null) { + Vector3 local; + if (def.unmountPosition != null) { + local = def.unmountPosition; + } else { + Vector3 seatPos = seat != null && seat < def.seatCount() ? def.seats.get(seat).position() : def.seats.getFirst().position(); + double x = seatPos.x() >= 0.0 ? seatPos.x() + 3.0 : seatPos.x() - 3.0; + local = new Vector3(x, 2.0, seatPos.z()); + } + Vec3 at = toWorld(local); + AABB box = passenger.getDimensions(Pose.STANDING).makeBoundingBox(at); + if (level().noCollision(passenger, box)) { + return at; + } + } + return super.getDismountLocationForPassenger(passenger); + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand, Vec3 location) { + ItemStack held = player.getItemInHand(hand); + if (held.getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (definition() != null && (held.getItem() instanceof FuelItem || held.getItem() instanceof WrenchItem)) { + if (level().isClientSide()) { + return InteractionResult.SUCCESS; + } + return held.getItem() instanceof FuelItem ? FuelItem.pour(held, this, player) : WrenchItem.useOn(held, this, player, hand); + } + InteractionResult result = super.interact(player, hand, location); + AircraftDefinition def = definition(); + if (result == InteractionResult.PASS && player.isSecondaryUseActive() && def != null && def.cargoRows() > 0 + && !isDestroyed() && player.getVehicle() != this) { + // Sneaking opens the cargo chest from outside, as in the original. + if (player instanceof ServerPlayer server) { + openCargo(server); + } + return InteractionResult.SUCCESS; + } + if (result != InteractionResult.PASS || player.isSecondaryUseActive()) { + return result; + } + if (def == null || !def.canRide || player.getVehicle() == this) { + return InteractionResult.PASS; + } + boolean boardingPilot = !def.uav && isSeatFree(0); + if (boardingPilot && canopy != null && isCanopyClosed()) { + // The first click opens the canopy, the next one climbs in. + if (!level().isClientSide()) { + openCanopy(); + } + return InteractionResult.SUCCESS; + } + if (boardingPilot && modeSwitchCooldown > 0) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + if (!player.startRiding(this)) { + return InteractionResult.PASS; + } + if (seatOf(player) == 0) { + closeCanopy(); + boardNearbyMobs(); + } + } + return InteractionResult.SUCCESS; + } + + // Racks + + /** Loads {@code cargo} into the given rack seat. Server only. */ + private boolean loadCargo(Entity cargo, int seat) { + if (!isSeatFree(seat) || cargo.isPassenger()) { + return false; + } + pendingSeat = seat; + boolean loaded = cargo.startRiding(this, true, true); + pendingSeat = EMPTY; + if (loaded) { + level().playSound(null, getX(), getY(), getZ(), SoundEvents.UI_BUTTON_CLICK, SoundSource.NEUTRAL, 1.0F, 1.0F); + } + return loaded; + } + + /** Whether {@code cargo} may ride rack {@code seat} of this vehicle. */ + public boolean acceptsCargo(AircraftEntity cargo, int seat) { + AircraftDefinition def = definition(); + AircraftDefinition cargoDef = cargo.definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + if (rack == null || cargoDef == null || cargo == this || isPassenger() || cargo.isPassenger()) { + return false; + } + // A vehicle that carries cargo itself cannot be loaded. + for (Entity p : cargo.getPassengers()) { + if (p instanceof AircraftEntity) { + return false; + } + } + if (rack.accepts(cargoDef.name(), cargoDef.kind())) { + return true; + } + for (RideRack rr : cargoDef.rideRacks) { + if (rr.carrier().equalsIgnoreCase(def.name()) && def.seatCount() + rr.rack() - 1 == seat) { + return true; + } + } + return false; + } + + /** Whether rack {@code seat} carries containers, {@code AddRack} naming {@code container}. */ + public boolean acceptsContainer(ContainerEntity container, int seat) { + AircraftDefinition def = definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + return rack != null && !isPassenger() && !container.isPassenger() + && rack.accepts(ContainerEntity.CARGO_NAME, ContainerEntity.CARGO_NAME); + } + + private boolean isNearRackEntry(AircraftEntity cargo, RackDefinition rack) { + Vec3 entry = toWorld(rack.entry()); + float r = rack.range(); + return Math.abs(cargo.getX() - entry.x) <= r && Math.abs(cargo.getY() - entry.y) <= r && Math.abs(cargo.getZ() - entry.z) <= r; + } + + /** Carrier pilot loads every nearby vehicle or container that fits a free rack. Server only. */ + public void loadNearbyCargo() { + AircraftDefinition def = definition(); + if (def == null || def.racks.isEmpty() || throttle > 0.3 || !hasBlockBelow(1, 3, true)) { + return; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (!isSeatFree(seat)) { + continue; + } + RackDefinition rack = def.rack(seat); + Vec3 entry = toWorld(rack.entry()); + AABB area = new AABB(entry, entry).inflate(rack.range()); + boolean loaded = false; + for (AircraftEntity cargo : level().getEntitiesOfClass(AircraftEntity.class, area)) { + if (acceptsCargo(cargo, seat) && loadCargo(cargo, seat)) { + loaded = true; + break; + } + } + if (!loaded) { + for (ContainerEntity container : level().getEntitiesOfClass(ContainerEntity.class, area)) { + if (acceptsContainer(container, seat) && loadCargo(container, seat)) { + break; + } + } + } + } + } + + /** Drops the last loaded piece of cargo at its rack's entry point. Server only. */ + public void unloadCargo() { + AircraftDefinition def = definition(); + if (def == null) { + return; + } + for (int seat = def.seatAndRackCount() - 1; seat >= def.seatCount(); seat--) { + Entity cargo = occupant(seat); + if (cargo == null) { + continue; + } + RackDefinition rack = def.rack(seat); + Vector3 drop = rack.entry(); + if (cargo instanceof AircraftEntity) { + // Aircraft roll out well clear of the carrier's tail or nose. + drop = drop.add(0.0, 0.0, drop.z() >= centreZ(def) ? 12.0 : -12.0); + } + Vec3 at = toWorld(drop); + cargo.stopRiding(); + cargo.snapTo(at.x, at.y, at.z, getYRot() + rack.yaw(), 0.0F); + // As in the original, with no ground in the 20 blocks below it goes down under a canopy. + if (!(cargo instanceof AircraftEntity) && !hasBlockBelow(3, 20, false)) { + ParachuteEntity.open(level(), cargo, ParachuteEntity.Kind.DROP); + } + return; + } + } + + /** Leaves the rack this vehicle rides on, at the rack entry point. Server only. */ + public void leaveRack() { + if (!(getVehicle() instanceof AircraftEntity carrier)) { + return; + } + AircraftDefinition carrierDef = carrier.definition(); + RackDefinition rack = carrierDef != null ? carrierDef.rack(carrier.seatOf(this)) : null; + Vec3 at = rack != null ? carrier.toWorld(rack.entry()) : position(); + stopRiding(); + snapTo(at.x, at.y, at.z, getYRot(), 0.0F); + } + + /** + * Pilot of a vehicle near a free rack of a carrier boards that rack. Server only. + * + * @return whether a rack was found + */ + public boolean boardNearbyRack() { + if (isPassenger()) { + return false; + } + for (AircraftEntity carrier : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(60.0))) { + AircraftDefinition def = carrier.definition(); + if (carrier == this || def == null) { + continue; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (carrier.isSeatFree(seat) && carrier.acceptsCargo(this, seat) && carrier.isNearRackEntry(this, def.rack(seat)) + && carrier.loadCargo(this, seat)) { + return true; + } + } + } + return false; + } + + private static double centreZ(AircraftDefinition def) { + if (def.boundingBoxes.isEmpty()) { + return 0.0; + } + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + for (var box : def.boundingBoxes) { + min = Math.min(min, box.offset().z() - box.width() / 2.0); + max = Math.max(max, box.offset().z() + box.width() / 2.0); + } + return (min + max) / 2.0; + } + + // Ejection + + /** Seats zero and one of vehicles with {@code EnableEjectionSeat} can bail out. */ + public boolean canEject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && def.ejectionSeat && seat >= 0 && seat < 2 && !(seat == 0 && def.uav); + } + + /** + * {@code ejectSeat}: throws the occupant clear of the aircraft in an ejection seat, whose + * parachute opens once it starts to fall. Each ejection takes a parachute from the parachute + * slots of the vehicle screen; without one the seat stays put. Creative players need none. + * Server only. + */ + public void eject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + if (def == null || !canEject(entity)) { + return; + } + boolean creative = entity instanceof Player player && player.getAbilities().instabuild; + if (!creative && !inventory.takeParachute()) { + if (entity instanceof ServerPlayer player) { + ModSounds.playTo(player, "ng", 1.0F, 1.0F); + player.sendOverlayMessage(Component.translatable("message.mcheli.no_parachute")); + } + return; + } + Vector3 seatPos = def.seats.get(seat).position(); + Vec3 out = toWorld(seatPos.add(0.0, 2.0, 0.0)); + Vector3f push = toWorldOffset(new Vector3(0.0, 2.0, 0.0)); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement().add(push.x + (random.nextFloat() - 0.5) * 0.1, push.y, + push.z + (random.nextFloat() - 0.5) * 0.1)); + entity.needsSync = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.EJECTION_SEAT); + if (canopy != null && !canopy.status()) { + canopy.set(true, false); + modeSwitchCooldown = 40; + } + ModSounds.playAt(entity, "eject_seat", 5.0F, 1.0F); + } + + // Search lights + + public boolean isSearchLightOn() { + AircraftDefinition def = definition(); + return def != null && !def.searchLights.isEmpty() && entityData.get(DATA_SEARCH_LIGHT); + } + + /** {@code canSwitchSearchLight}: the pilot or the first crew seat, in vehicles that have one. */ + public boolean canSwitchSearchLight(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && !def.searchLights.isEmpty() && seat >= 0 && seat <= 1; + } + + /** Server only. */ + public void toggleSearchLight(Entity entity) { + if (canSwitchSearchLight(entity)) { + entityData.set(DATA_SEARCH_LIGHT, !entityData.get(DATA_SEARCH_LIGHT)); + } + } + + /** + * Where a turning search light points: the first crew seat's view, else the pilot's, as + * {@code renderLight} took them. Null with nobody aboard to aim it. + */ + public @Nullable Entity searchLightAimer() { + Entity crew = occupant(1); + return crew != null ? crew : occupant(0); + } + + // Troops: parachute drops and ropes + + public boolean isRepelling() { + return entityData.get(DATA_REPELLING); + } + + /** How far the ropes hang below their hooks, as a negative number. */ + public float ropeLength() { + return ropeLength; + } + + /** Where rope {@code index} hangs from, in the world. */ + public Vec3 ropePosition(int index) { + AircraftDefinition def = definition(); + return def != null && index >= 0 && index < def.repellingHooks.size() ? toWorld(def.repellingHooks.get(index).position()) : position(); + } + + /** + * {@code onUpdate_Repelling} and {@code onUpdate_UnmountCrew}: the ropes run out towards the + * ground, then the mobs aboard slide down them one per hook interval; a parachute drop lets one + * passenger out per {@code MobDropOption} interval while the hatch is open. + */ + private void tickRopes(AircraftDefinition def) { + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + int clear = airBelow(50); + if (ropeLength > -50.0F && ropeLength > -clear) { + ropeLength -= level().isClientSide() ? 0.3F : 0.25F; + } + } else { + ropeLength = 0.0F; + } + } + if (level().isClientSide()) { + return; + } + if (isRepelling() && isDestroyed()) { + entityData.set(DATA_REPELLING, false); + } + if (!isRepelling()) { + repellingTicks = 0; + } else if (repellingTicks < 60) { + repellingTicks++; + } else { + for (int rope = 0; rope < def.repellingHooks.size(); rope++) { + if (ticksAlive % def.repellingHooks.get(rope).interval() != 0) { + continue; + } + for (int seat = 1; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + rappel(mob, rope); + break; + } + } + } + } + if (droppingTroops) { + if (hasBlockBelow(3, 10, false) || isDestroyed()) { + droppingTroops = false; + } else if ((hatch == null || hatch.isOn()) && ticksAlive % def.dropInterval == 0 && !dropNextTroop(def)) { + droppingTroops = false; + } + } + } + + /** Air blocks straight below, up to {@code max}. */ + private int airBelow(int max) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + int i = 0; + while (i < max && pos.getY() - i > level().getMinY() && level().getBlockState(pos.setY(blockPosition().getY() - i - 1)).isAir()) { + i++; + } + return i; + } + + /** + * {@code canStartRepelling}: a helicopter in hover mode, level and holding still. The original + * compared against where it was a few ticks before. + */ + private boolean canStartRepelling() { + return this instanceof HelicopterEntity && isHoveringMode() && Math.abs(getXRot()) < 3.0F && Math.abs(roll()) < 3.0F + && lastTickPos != null && position().distanceTo(lastTickPos) < 0.05; + } + + /** + * The pilot's drop key, {@code unmountCrew}: with ropes, lets them down from a steady hover + * or pulls them up again; with {@code EnableParachuting}, starts or stops dropping the mobs + * aboard under parachutes; otherwise, or close to the ground, simply lets the mobs out. + * Server only. + * + * @return false if nothing could be done, so the pilot hears it + */ + public boolean dropCrew() { + AircraftDefinition def = definition(); + if (def == null || isDestroyed()) { + return false; + } + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + entityData.set(DATA_REPELLING, false); + return true; + } + if (hasBlockBelow(3, 4, false)) { + return unloadMobs(); + } + if (!canStartRepelling()) { + return false; + } + entityData.set(DATA_REPELLING, true); + repellingTicks = 0; + return true; + } + if (droppingTroops) { + droppingTroops = false; + return true; + } + if (def.parachuting && !hasBlockBelow(3, 10, false)) { + droppingTroops = true; + if (hatch != null && hatch.isOff()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } + return true; + } + return unloadMobs(); + } + + /** Lets every mob in a crew seat out, as in {@code unmountCrew(false)}. */ + private boolean unloadMobs() { + AircraftDefinition def = definition(); + boolean any = false; + for (int seat = 1; def != null && seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + mob.stopRiding(); + any = true; + } + } + return any; + } + + /** The next mob behind the second seat leaves at the drop point under a parachute. */ + private boolean dropNextTroop(AircraftDefinition def) { + for (int seat = 2; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + parachuteOut(mob, def); + return true; + } + } + return false; + } + + private void parachuteOut(Entity entity, AircraftDefinition def) { + Vec3 out = toWorld(def.dropPosition); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement()); + entity.needsSync = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.DROP); + } + + /** + * A passenger's own drop key, {@code MCH_ClientSeatTickHandler}: slide down a rope once the + * ropes are out, or jump with a parachute from behind the second seat of a vehicle with + * {@code EnableParachuting} and room below. Server only. + * + * @return false if neither is possible right now + */ + public boolean jumpOut(Player player) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (def == null || seat <= 0 || seat >= def.seatCount() || isDestroyed()) { + return false; + } + if (isRepelling() && repellingTicks > 50 && !def.repellingHooks.isEmpty()) { + lastRope = (lastRope + 1) % def.repellingHooks.size(); + rappel(player, lastRope); + return true; + } + if (def.parachuting && seat >= 2 && !hasBlockBelow(3, 13, false)) { + parachuteOut(player, def); + return true; + } + return false; + } + + /** {@code unmountEntityRepelling}: two blocks under the hook, onto a rope rider that lets them down. */ + private void rappel(Entity entity, int rope) { + Vec3 at = ropePosition(rope).add(0.0, -2.0, 0.0); + entity.stopRiding(); + entity.teleportTo(at.x, at.y, at.z); + entity.setDeltaMovement(Vec3.ZERO); + entity.resetFallDistance(); + RappelEntity rider = new RappelEntity(ModEntities.RAPPEL.get(), level()); + rider.attach(this, rope); + rider.setPos(at); + level().addFreshEntity(rider); + entity.startRiding(rider, true, true); + lastRope = rope; + } + + /** + * {@code mountMobToSeats}: when the pilot climbs in, mobs standing close by take the free crew + * seats, so villagers and the like can be flown somewhere. Server only. + */ + private void boardNearbyMobs() { + AircraftDefinition def = definition(); + if (def == null || def.uav) { + return; + } + for (Mob mob : level().getEntitiesOfClass(Mob.class, getBoundingBox().inflate(3.0, 2.0, 3.0), m -> !m.isPassenger() && !m.isVehicle())) { + for (int seat = 1; seat < def.seatCount(); seat++) { + if (isSeatFree(seat)) { + pendingSeat = seat; + mob.startRiding(this, true, true); + pendingSeat = EMPTY; + break; + } + } + } + } + + // Terrain helpers + + /** True if any solid block is within {@code depth} blocks below the vehicle. */ + protected boolean isNearGround(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(0, -1, 0); + if (!level().getBlockState(pos).getCollisionShape(level(), pos).isEmpty()) { + return true; + } + } + return false; + } + + /** + * Port of {@code MCH_Lib.getBlockY}: scans a {@code size} x {@code size} column from the + * vehicle's block down {@code depth} layers (the first layer being the vehicle's own). + * + * @param solidOnly ignore blocks without collision; fluids still count when this is false + */ + public boolean hasBlockBelow(int size, int depth, boolean solidOnly) { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < depth; y++) { + for (int x = -size / 2; x <= size / 2; x++) { + for (int z = -size / 2; z <= size / 2; z++) { + pos.set(px + x, py - y, pz + z); + BlockState state = level().getBlockState(pos); + if (state.isAir()) { + continue; + } + if (!solidOnly || !state.getCollisionShape(level(), pos).isEmpty() || !state.getFluidState().isEmpty()) { + return true; + } + } + } + } + return false; + } + + /** True if the first non-air block within {@code depth} below is water. */ + protected boolean isWaterBelow(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + BlockState state = level().getBlockState(pos); + if (!state.isAir()) { + return !state.getFluidState().isEmpty(); + } + pos.move(0, -1, 0); + } + return false; + } + + /** {@code getBlockY(this, 3, -2, false)}: standing on something that is neither air nor water. */ + protected boolean isOnSolidGround() { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < 2; y++) { + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + BlockState state = level().getBlockState(pos.set(px + x, py - y, pz + z)); + if (!state.isAir()) { + return state.getFluidState().isEmpty(); + } + } + } + } + return false; + } + + /** + * Port of {@code getWaterDepth}: the share of the hit box that is in water, from 0 to 1, sampled + * in five horizontal slices. {@code FloatOffset} raises the slices, so a hull with an offset rides + * higher. The float physics only pushes up once this reaches 1, which sets the waterline. + * + *

The original drew the model {@value #LEGACY_MODEL_LIFT} above the bottom of the hit box + * (its {@code yOffset}, half the 0.7 default height); this port draws it at the bottom. The + * slices are lowered by the same amount so boats sit in the water as they did in 1.0.3. + */ + protected double waterDepth() { + if (entityData.get(DATA_SINKING)) { + return 0.0; + } + AircraftDefinition def = definition(); + float offset = (def != null ? def.floatOffset : 0.0F) - LEGACY_MODEL_LIFT; + AABB box = getBoundingBox(); + double height = box.maxY - box.minY; + double depth = 0.0; + for (int i = 0; i < 5; i++) { + double bottom = box.minY + height * i / 5.0 - 0.125 + offset; + double top = box.minY + height * (i + 1) / 5.0 - 0.125 + offset; + AABB slice = new AABB(box.minX, bottom, box.minZ, box.maxX, top, box.maxZ); + if (level().getBlockStates(slice).anyMatch(state -> state.getFluidState().is(FluidTags.WATER))) { + depth += 1.0 / 5.0; + } + } + return depth; + } + + /** {@code applyOnGroundPitch}: eases pitch towards {@code OnGroundPitch} and roll towards level. */ + protected void applyOnGroundPitch(float factor) { + AircraftDefinition def = definition(); + if (def != null) { + float ground = def.onGroundPitch; + setXRot((getXRot() - ground) * factor + ground); + } + roll *= factor; + } + + /** {@code StepHeight}: how tall a ledge the vehicle drives up without jumping. */ + @Override + public float maxUpStep() { + AircraftDefinition def = definition(); + return def != null ? def.stepHeight : 0.0F; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith(@Nullable Entity other) { + return true; + } + + @Override + public boolean isPushable() { + return false; + } + + // Weapons + + public @Nullable VehicleWeapons weapons() { + definition(); + return weapons; + } + + public void publishWeaponSelection() { + if (weapons != null && !level().isClientSide()) { + entityData.set(DATA_WEAPON_SELECTION, weapons.encodeSelection()); + } + } + + public void publishWeaponUse(int stat) { + entityData.set(DATA_WEAPON_USE, stat); + } + + public int syncedWeaponUse() { + return entityData.get(DATA_WEAPON_USE); + } + + /** + * Whether {@code set} fires along the user's view instead of the hull: the shot direction and + * the muzzle positions are worked out as if the vehicle faced where the user looks. Static + * mounts do this; see {@link GroundVehicleEntity#aimsWithUser}. + */ + public boolean aimsWithUser(WeaponSet set) { + return false; + } + + /** {@code isInfinityAmmo}: creative players never run dry, nor does anyone when the server config says so. */ + public boolean hasInfiniteAmmo(@Nullable Entity user) { + return ServerConfig.infiniteAmmo() || user instanceof Player player && player.getAbilities().instabuild; + } + + /** Where the pilot looks relative to the hull: the turret's traverse. */ + public float turretYaw() { + return animation.seatYaw(0, 1.0F); + } + + public float stealth() { + AircraftDefinition def = definition(); + return def != null ? def.stealth : 0.0F; + } + + /** The TV missile the crew is flying, if it still exists. */ + public @Nullable TvMissileEntity tvMissile() { + if (level().isClientSide()) { + return level().getEntity(entityData.get(DATA_TV_MISSILE)) instanceof TvMissileEntity m && !m.isRemoved() ? m : null; + } + return tvMissile != null && !tvMissile.isRemoved() ? tvMissile : null; + } + + public void setTvMissile(@Nullable TvMissileEntity missile) { + tvMissile = missile; + entityData.set(DATA_TV_MISSILE, missile != null ? missile.getId() : 0); + } + + /** Whether the flares are burning right now, for the crew's display. */ + public boolean isFlareActive() { + return entityData.get(DATA_FLARE); + } + + /** Someone is locking on to this vehicle; its crew hears the warning. Server only. */ + public void warnLocked() { + if (alertCount == 0 && !isDestroyed() && weapons != null && weapons.flares.isFitted()) { + alertCount = 10; + for (int seat = 0; seat < 2; seat++) { + if (occupant(seat) instanceof ServerPlayer player) { + ModSounds.playTo(player, "locked", 1.0F, 1.0F); + } + } + } + } + + // Hit boxes + + /** + * Clips a ray against the main hit box and every {@code BoundingBox}, remembering the damage + * factor of the box hit first, as the original's composite bounding box did. + */ + public Optional clipHitBoxes(Vec3 from, Vec3 to, double inflate) { + hitBoxFactor = 1.0F; + Optional best = clip(getBoundingBox().inflate(inflate), from, to); + double bestDistance = best.map(from::distanceToSqr).orElse(Double.MAX_VALUE); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + Optional hit = clip(hullBox(box).inflate(inflate), from, to); + if (hit.isPresent() && from.distanceToSqr(hit.get()) < bestDistance) { + best = hit; + bestDistance = from.distanceToSqr(hit.get()); + hitBoxFactor = box.damageFactor(); + } + } + } + return best; + } + + /** + * One {@code BoundingBox} in world space. Like the original's, it stays axis aligned: the + * centre turns with the hull, the box itself does not. + */ + private AABB hullBox(BoundingBoxDefinition box) { + Vec3 c = toWorld(box.offset()); + double hw = box.width() / 2.0; + double hh = box.height() / 2.0; + return new AABB(c.x - hw, c.y - hh, c.z - hw, c.x + hw, c.y + hh, c.z + hw); + } + + /** The whole hull: the main hit box, every {@code BoundingBox} and the generated boxes, in world space. */ + public List hullBoxes() { + AircraftDefinition def = definition(); + if (def == null || def.hull().isEmpty()) { + return List.of(getBoundingBox()); + } + List boxes = new ArrayList<>(def.hull().size() + 1); + boxes.add(getBoundingBox()); + for (BoundingBoxDefinition box : def.hull()) { + boxes.add(hullBox(box)); + } + return boxes; + } + + /** The smallest box around the whole hull. */ + private AABB hullBounds() { + AABB bounds = getBoundingBox(); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + bounds = bounds.minmax(hullBox(box)); + } + } + return bounds; + } + + /** Whether any part of this hull touches any part of {@code other}'s, grown by {@code inflate}. */ + private boolean hullsTouch(AircraftEntity other, double inflate) { + // Hulls generated from big models run to a thousand boxes, so skip ours that are nowhere near. + AABB around = other.hullBounds().inflate(inflate); + List theirs = null; + for (AABB mine : hullBoxes()) { + AABB grown = mine.inflate(inflate); + if (!grown.intersects(around)) { + continue; + } + if (theirs == null) { + theirs = other.hullBoxes(); + } + for (AABB box : theirs) { + if (grown.intersects(box)) { + return true; + } + } + } + return false; + } + + /** + * Vehicles whose hulls block this one: everything nearby except its own carrier or cargo, and + * vehicles either of the two could load onto a rack, which have to drive into each other. + */ + private List solidNeighbours(AABB area) { + return level().getEntitiesOfClass(AircraftEntity.class, area.inflate(HULL_REACH), other -> other != this && !other.isRemoved() + && other.definition() != null && !other.isPassenger() && !isPassengerOfSameVehicle(other) + && other.getVehicle() != this && getVehicle() != other && !canCarry(other) && !other.canCarry(this)); + } + + /** Whether a free rack of this vehicle would take {@code cargo}. */ + private boolean canCarry(AircraftEntity cargo) { + AircraftDefinition def = definition(); + if (def == null) { + return false; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (acceptsCargo(cargo, seat)) { + return true; + } + } + return false; + } + + /** + * Not in the original, where only the main box at a vehicle's feet collided: the whole hull + * blocks other vehicles. A move that would push this hull into another vehicle's is undone, + * sideways first and then completely, so a helicopter can also set down on a parked truck. + * Hulls that already overlapped, say after two vehicles were placed into each other, may move + * apart. + */ + @Override + public void move(MoverType type, Vec3 delta) { + if (type != MoverType.SELF || isPassenger() || definition() == null) { + super.move(type, delta); + return; + } + List near = solidNeighbours(hullBounds().expandTowards(delta)); + if (near.isEmpty()) { + super.move(type, delta); + return; + } + List overlapping = new ArrayList<>(); + for (AircraftEntity other : near) { + if (hullsTouch(other, 0.0)) { + overlapping.add(other); + } + } + Vec3 before = position(); + super.move(type, delta); + if (!entersHull(near, overlapping)) { + return; + } + Vec3 m = getDeltaMovement(); + setPos(before.x, getY(), before.z); + setDeltaMovement(0.0, m.y, 0.0); + if (entersHull(near, overlapping)) { + setPos(before); + setDeltaMovement(Vec3.ZERO); + } + } + + private boolean entersHull(List near, List overlapping) { + for (AircraftEntity other : near) { + if (!overlapping.contains(other) && hullsTouch(other, 0.0)) { + return true; + } + } + return false; + } + + /** A ray that starts inside a box hits it where it starts, as it does for vanilla entity picking. */ + private static Optional clip(AABB box, Vec3 from, Vec3 to) { + return box.contains(from) ? Optional.of(from) : box.clip(from, to); + } + + public float consumeHitBoxFactor() { + float f = hitBoxFactor; + hitBoxFactor = 1.0F; + return f; + } + + // Damage + + public boolean isDestroyed() { + return definition() != null && hp() <= 0; + } + + /** + * Port of the original's {@code attackEntityFrom}. Armour caps a hit at {@code ArmorMaxDamage}, + * scales it by {@code ArmorDamageFactor} and the struck box's factor, then subtracts + * {@code ArmorMinDamage}. A player punching an empty vehicle packs it up instead. + */ + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + float boxFactor = consumeHitBoxFactor(); + if (testFlightStart != null || isRemoved() || isInvulnerableToBase(source) || timeSinceHit > 0) { + return false; + } + if (source.is(DamageTypes.IN_FIRE) || source.is(DamageTypes.CACTUS)) { + return false; + } + boolean lava = source.is(DamageTypes.LAVA); + boolean onFire = source.is(DamageTypes.ON_FIRE); + if (lava) { + damage *= random.nextInt(8) + 2; + timeSinceHit = 2; + } + if (source.is(DamageTypeTags.IS_EXPLOSION)) { + timeSinceHit = 1; + } else if (source.getEntity() != null && hasPassenger(source.getEntity())) { + return false; + } + if (onFire) { + timeSinceHit = 10; + } + + boolean creative = false; + boolean sneaking = false; + boolean packUp = false; + if (source.getEntity() instanceof Player player) { + creative = player.getAbilities().instabuild; + sneaking = player.isShiftKeyDown(); + if (source.is(DamageTypes.PLAYER_ATTACK)) { + packUp = creative || !hasPlayerPassenger(); + } + ModSounds.playAt(this, "hit", 1.0F, 1.0F); + } else { + ModSounds.playAt(this, "helidmg", 1.0F, 0.9F + random.nextFloat() * 0.1F); + } + + if (!isDestroyed()) { + if (!packUp) { + AircraftDefinition def = definition(); + if (def != null && !lava && !onFire) { + damage = Math.min(damage, def.armorMaxDamage); + if (boxFactor <= 1.0F) { + damage *= boxFactor; + } + damage *= def.armorDamageFactor; + damage -= def.armorMinDamage; + if (damage <= 0.0F) { + return false; + } + if (boxFactor > 1.0F) { + damage *= boxFactor; + } + } + damage *= ServerConfig.vehicleDamageFactor(); + entityData.set(DATA_HP, Math.max(hp() - (int) damage, 0)); + } + setHurtDir(-getHurtDir()); + // No markHurt(): it sends the server's motion to every client, which snaps a vehicle + // its pilot is flying back a step and looks like knockback on every hit. + setHurtTime(10); + if (hp() <= 0 || packUp) { + if (!packUp) { + entityData.set(DATA_HP, 0); + destroyVehicle(); + timeSinceHit = 20; + if (isAfloat() || isHeavy() && isNearGround(3)) { + startCookOff(source.getEntity()); + } else if (source.is(DamageTypes.IN_WALL) || source.is(DamageTypes.FLY_INTO_WALL) || source.is(DamageTypes.FALL)) { + crashExplosion(); + } else { + Explosions.explode(level, getX(), getY(), getZ(), + new Explosions.Params(null, source.getEntity(), 2.0F, 2.0F, true, true, 5, false, null)); + } + } else { + // In creative the item only drops when sneaking, as with the original's defaults. + if (!creative || sneaking) { + spawnAtLocation(level, getPickResult() != null ? getPickResult() : ItemStack.EMPTY); + } + discard(); + } + } + } else if (packUp && creative) { + discard(); + } + return true; + } + + private boolean hasPlayerPassenger() { + for (Entity passenger : getPassengers()) { + if (passenger instanceof Player) { + return true; + } + } + return false; + } + + /** {@code destroyAircraft}: the wreck tumbles, burns and disappears after 30 seconds. Server only. */ + private void destroyVehicle() { + AircraftDefinition def = definition(); + despawnCount = def != null && def.targetDrone ? 50 : WRECK_TICKS; + setHovering(false); + if (def != null && def.uav) { + Entity remote = occupant(0); + if (remote != null) { + remote.stopRiding(); + } + } + for (int seat = 0; seat < 2; seat++) { + Entity crew = occupant(seat); + if (crew != null && canEject(crew)) { + eject(crew); + } + } + } + + /** {@code explosionByCrash}: a wreck hitting the ground goes up in a fireball sized by its fuel. */ + private void crashExplosion() { + if (!(level() instanceof ServerLevel level)) { + return; + } + if (isHeavy()) { + startCookOff(null); + return; + } + exploded = true; + AircraftDefinition def = definition(); + float size = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + Vec3 at = def != null && def.fuelTankPosition != null ? toWorld(def.fuelTankPosition) : position(); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, null, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + if (!isInWater()) { + Explosions.crashFireball(level, at.x, at.y, at.z, size); + } + } + + /** + * Airliners, bombers and transports ({@code MaxFuel} of {@value HEAVY_FUEL} or more): like a + * boat, they go up in three blasts rather than one fireball. + */ + private boolean isHeavy() { + AircraftDefinition def = definition(); + return def != null && def.maxFuel >= HEAVY_FUEL; + } + + /** A floating vehicle sitting on the water, such as a patrol boat or a float plane at rest. */ + private boolean isAfloat() { + AircraftDefinition def = definition(); + return def != null && def.floats && waterDepth() > 0.0; + } + + /** + * A boat destroyed on the water, or a heavy aircraft wrecked on the ground, does not go up in + * one crash fireball; its fuel and magazine cook off in three blasts over about five seconds, + * then a hull in the water sinks. Not in the original, which only had the crash explosion. + */ + private void startCookOff(@Nullable Entity cause) { + exploded = true; + cookOffBlasts = 3; + cookOffDelay = 1; + cookOffCause = cause; + } + + private void tickCookOff(ServerLevel level) { + if (cookOffBlasts <= 0 || --cookOffDelay > 0) { + return; + } + AircraftDefinition def = definition(); + int blast = 4 - cookOffBlasts--; + float fuel = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + // The fuel tank first, then whatever the magazine still holds, then the hull goes up. + float size = switch (blast) { + case 1 -> fuel * 0.6F; + case 2 -> Math.max(munitionSize(), fuel * 0.4F); + default -> fuel; + }; + size = Mth.clamp(size, 1.0F, 15.0F); + Vec3 at = cookOffPoint(def); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, cookOffCause, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + // The blasts happen at the waterline, but the fire is above it, so show the full fireball. + Explosions.crashFireball(level, at.x, at.y, at.z, size); + if (cookOffBlasts > 0) { + // Three blasts inside five seconds: about 1.5 to 2.5 seconds apart. + cookOffDelay = 30 + random.nextInt(20); + } else { + // A boat or a flying boat goes under; a heavy aircraft on land stays where it burns. + if (isInWater() || waterDepth() > 0.0) { + entityData.set(DATA_SINKING, true); + } + cookOffCause = null; + } + } + + /** + * A cooked-off wreck going under, in place of the vehicle's own physics: it settles slowly, + * stern first with the bow raised and listing to one side, drifts to a stop and comes to rest + * on the bottom. Out of the water it simply falls. + */ + private void sink() { + Vec3 m = getDeltaMovement(); + double my = isInWater() ? Math.max(m.y - 0.003, -SINK_SPEED) : m.y - 0.04; + Vec3 motion = new Vec3(m.x * 0.9, my, m.z * 0.9); + move(MoverType.SELF, motion); + setDeltaMovement(onGround() ? motion.multiply(0.5, 0.0, 0.5) : motion); + if (!onGround()) { + // Which way it lists only has to be the same on every side, so take it from the id. + float list = (getId() & 1) == 0 ? SINK_LIST : -SINK_LIST; + setXRot(Mth.approach(getXRot(), -SINK_PITCH, 0.12F)); + roll = Mth.approach(roll, list, 0.15F); + } + } + + /** + * How big the ammunition aboard goes up: each weapon's {@code Explosion} scaled by how full its + * magazine and reserve still are. Guns and empty racks add nothing. + */ + private float munitionSize() { + if (weapons == null) { + return 0.0F; + } + float size = 0.0F; + for (WeaponSet set : weapons.sets()) { + int rounds = set.ammo() + set.restAmmo(); + if (rounds > 0 && set.definition().explosion > 0) { + size += set.definition().explosion * Math.min(1.0F, (float) rounds / Math.max(set.maxAmmo(), 1)); + } + } + return size; + } + + /** A random spot on the hull: one of its hit boxes, or the middle of the body. */ + private Vec3 cookOffPoint(@Nullable AircraftDefinition def) { + if (def == null || def.boundingBoxes.isEmpty() || random.nextInt(def.boundingBoxes.size() + 1) == 0) { + return position().add(0.0, def != null ? def.bodyHeight / 2.0 : 0.5, 0.0); + } + return toWorld(def.boundingBoxes.get(random.nextInt(def.boundingBoxes.size())).offset()); + } + + /** Destroyed state each tick: the wreck spins, sets its crew alight and explodes on impact. */ + private void tickDestroyed() { + if (!isDestroyed()) { + return; + } + input.clear(); + throttle *= 0.98; + crashSpin(); + if (!(level() instanceof ServerLevel level)) { + return; + } + tickCookOff(level); + if (ticksAlive % 20 == 0) { + AircraftDefinition def = definition(); + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null && !(seat == 0 && def != null && def.uav)) { + crew.igniteForSeconds(5.0F); + } + } + } + if (despawnCount > 0 && --despawnCount <= 1) { + ejectPassengers(); + discard(); + } + } + + /** + * Not in the original: a shot down plane rolls into a nose-down spiral and a helicopter spins + * with some wobble, until it is close to the ground. Tanks and cars just stop. + */ + private void crashSpin() { + if (!(this instanceof HelicopterEntity) && !(this instanceof PlaneEntity) || isNearGround(3) || isInWater()) { + return; + } + int direction = CrashSpin.direction(getId()); + int ticks = spinTicks++; + if (this instanceof HelicopterEntity) { + setYRot(getYRot() + CrashSpin.helicopterYawRate(ticks, direction)); + setXRot(Mth.approach(getXRot(), CrashSpin.helicopterWobblePitch(ticks, getId()), 2.0F)); + roll = Mth.approach(roll, CrashSpin.helicopterWobbleRoll(ticks, getId()), 3.0F); + } else { + setYRot(getYRot() + CrashSpin.planeYawRate(ticks, direction)); + setXRot(CrashSpin.planePitch(getXRot())); + roll = AircraftRotation.wrap(roll + CrashSpin.planeRollRate(ticks, direction)); + } + } + + /** {@code onUpdate_Particle2}: smoke from a damaged hull, thicker the lower its hit points. */ + private void damageSmoke(AircraftDefinition def) { + float ratio = (float) hp() / def.maxHp; + if (ratio > 0.5F) { + return; + } + int sources = 1 + def.boundingBoxes.size(); + int pick = random.nextInt(sources); + Vec3 at = pick == 0 ? position().add(0.0, def.bodyHeight / 2.0, 0.0) : toWorld(def.boundingBoxes.get(pick - 1).offset()); + if (ratio < random.nextFloat() / 2.0F) { + level().addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, at.x, at.y, at.z, + (random.nextDouble() - 0.5) * 0.3, random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.3); + } + if (isDestroyed()) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, at.x, at.y, at.z, 0.0, 0.07, 0.0); + if (random.nextInt(3) == 0) { + level().addParticle(ParticleTypes.FLAME, at.x + random.nextGaussian() * 0.5, at.y, at.z + random.nextGaussian() * 0.5, 0.0, 0.05, 0.0); + } + wreckFire(def); + } + } + + /** + * Not in the original: the wreck keeps burning until it disappears, with a fire glow that + * lights up the smoke at night and dies down over the last few seconds. Nothing burns under + * water. Client only. + */ + private void wreckFire(AircraftDefinition def) { + wreckTicks++; + if (isInWater()) { + return; + } + float left = Math.clamp((float) (WRECK_TICKS - wreckTicks) / WRECK_FADE_TICKS, 0.15F, 1.0F); + Vec3 centre = def.enginePosition != null ? toWorld(def.enginePosition) : position().add(0.0, def.bodyHeight / 2.0, 0.0); + if (wreckTicks % 8 == 0) { + // Each glow outlives the gap to the next, so the fire never goes dark in between. + float size = Math.clamp(def.bodyWidth * 0.8F, 2.0F, 8.0F) * left; + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size, 24), true, + centre.x, centre.y, centre.z, 0.0, 0.02, 0.0); + } + if (random.nextFloat() < left) { + double spread = def.bodyWidth * 0.4; + level().addParticle(ParticleTypes.FLAME, centre.x + random.nextGaussian() * spread, centre.y, + centre.z + random.nextGaussian() * spread, 0.0, 0.04, 0.0); + } + if (random.nextInt(10) == 0) { + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 20 + random.nextInt(20)), true, centre.x, centre.y, centre.z, + (random.nextDouble() - 0.5) * 0.15, 0.1 + random.nextDouble() * 0.15, (random.nextDouble() - 0.5) * 0.15); + } + } + + /** A vehicle that is destroyed, packed up or killed spills what it carried; one that unloads keeps it. */ + @Override + public void remove(RemovalReason reason) { + if (reason.shouldDestroy() && !level().isClientSide() && !isRemoved()) { + inventory.dropAll(); + } + super.remove(reason); + } + + @Override + protected Item getDropItem() { + AircraftDefinition def = definition(); + return def != null ? ModItems.forKind(def.kind()) : Items.AIR; + } + + @Override + public @Nullable ItemStack getPickResult() { + AircraftDefinition def = definition(); + return def != null ? AircraftItem.stackOf(def) : null; + } + + // Dimensions and syncing + + @Override + public EntityDimensions getDimensions(Pose pose) { + AircraftDefinition def = definition(); + return def != null ? EntityDimensions.scalable(def.bodyWidth, def.bodyHeight) : super.getDimensions(pose); + } + + @Override + public void onSyncedDataUpdated(EntityDataAccessor accessor) { + super.onSyncedDataUpdated(accessor); + if (DATA_TYPE.equals(accessor)) { + refreshDimensions(); + definition(); + } else if (DATA_SEATS.equals(accessor) && level().isClientSide()) { + decodeSeats(entityData.get(DATA_SEATS)); + } else if (DATA_WEAPON_SELECTION.equals(accessor) && level().isClientSide() && weapons() != null) { + weapons.decodeSelection(entityData.get(DATA_WEAPON_SELECTION), ClientAccess.localPlayer()); + } + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putString("Type", typeName()); + output.putString("Texture", texture()); + output.putInt("Hp", hp()); + output.putInt("Fuel", fuel()); + output.putFloat("Roll", roll); + output.putBoolean("Hovering", isHoveringMode()); + output.putBoolean("GunnerMode", isGunnerMode()); + output.putInt("Parts", partBits()); + output.putInt("DespawnCount", despawnCount); + output.putInt("CookOff", cookOffBlasts); + output.putBoolean("Sinking", entityData.get(DATA_SINKING)); + output.putBoolean("SearchLight", entityData.get(DATA_SEARCH_LIGHT)); + if (weapons != null) { + output.putIntArray("WeaponsAmmo", weapons.saveAmmo()); + } + inventory.save(output); + if (station != null && stationReturn != null) { + output.store("Station", UUIDUtil.CODEC, station); + output.store("StationReturn", Vec3.CODEC, stationReturn); + } + ValueOutput.TypedOutputList seats = output.list("Seats", Codec.STRING); + for (int i = 0; i < seatIds.length; i++) { + Entity e = occupant(i); + seats.add(e != null ? e.getUUID().toString() : ""); + } + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + entityData.set(DATA_TYPE, input.getStringOr("Type", "")); + entityData.set(DATA_TEXTURE, input.getStringOr("Texture", "")); + entityData.set(DATA_PARTS, input.getIntOr("Parts", 0)); + AircraftDefinition def = definition(); + entityData.set(DATA_HP, input.getIntOr("Hp", def != null ? def.maxHp : 1)); + // 1.0.0 and the earlier betas had no fuel; their vehicles come back with a full tank. + entityData.set(DATA_FUEL, input.getIntOr("Fuel", def != null ? def.maxFuel : 0)); + roll = input.getFloatOr("Roll", 0.0F); + setHovering(input.getBooleanOr("Hovering", false)); + entityData.set(DATA_GUNNER_MODE, input.getBooleanOr("GunnerMode", false)); + despawnCount = input.getIntOr("DespawnCount", 0); + if (hp() <= 0 && despawnCount <= 0) { + despawnCount = WRECK_TICKS; + } + cookOffBlasts = input.getIntOr("CookOff", 0); + cookOffDelay = cookOffBlasts > 0 ? 20 : 0; + entityData.set(DATA_SINKING, input.getBooleanOr("Sinking", false)); + entityData.set(DATA_SEARCH_LIGHT, input.getBooleanOr("SearchLight", false)); + exploded |= cookOffBlasts > 0 || entityData.get(DATA_SINKING); + input.getIntArray("WeaponsAmmo").ifPresent(ammo -> { + if (weapons != null) { + weapons.loadAmmo(ammo); + } else { + savedAmmo = ammo; + } + }); + inventory.load(input); + station = input.read("Station", UUIDUtil.CODEC).orElse(null); + stationReturn = input.read("StationReturn", Vec3.CODEC).orElse(null); + savedSeats.clear(); + List seats = input.listOrEmpty("Seats", Codec.STRING).stream().toList(); + for (int i = 0; i < seats.size(); i++) { + if (!seats.get(i).isEmpty()) { + try { + savedSeats.put(UUID.fromString(seats.get(i)), i); + } catch (IllegalArgumentException ignored) { + // A hand edited save; that occupant simply takes the first free seat. + } + } + } + for (PartState part : parts) { + part.force(part.status()); + } + refreshDimensions(); + } + + /** Raw pilot input, filled in by the client controller every tick. */ + public static final class ControlInput { + public boolean throttleUp; + public boolean throttleDown; + public boolean left; + public boolean right; + public boolean brake; + /** The pilot is looking around instead of steering; planes and tanks yaw with A/D then. */ + public boolean freeLook; + + public void clear() { + throttleUp = false; + throttleDown = false; + left = false; + right = false; + brake = false; + freeLook = false; + } + + public byte write() { + return (byte) ((throttleUp ? 1 : 0) | (throttleDown ? 2 : 0) | (left ? 4 : 0) | (right ? 8 : 0) + | (brake ? 16 : 0) | (freeLook ? 32 : 0)); + } + + public void read(byte bits) { + throttleUp = (bits & 1) != 0; + throttleDown = (bits & 2) != 0; + left = (bits & 4) != 0; + right = (bits & 8) != 0; + brake = (bits & 16) != 0; + freeLook = (bits & 32) != 0; + } + } +} From ffd7cd21a46b77f0aa47696a077820453a45e11e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:17 +0800 Subject: [PATCH 0237/1461] Add entity/AnimationState.java to the 26.2 shared sources --- .../mcheli/entity/AnimationState.java | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java new file mode 100644 index 00000000..eda5ed7b --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java @@ -0,0 +1,202 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.RackDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; + +/** + * Purely visual state that every side advances once per tick: wheels, tracks, spinning parts and + * where each crew member is looking relative to the hull. + * + *

Ported from {@code updatePartWheel}, {@code updatePartCrawlerTrack} and + * {@code onUpdate_PartRotation}. Nothing in here affects physics, so it is never synced; each + * client derives it from the synced throttle, input and crew rotations. + */ +public final class AnimationState { + /** Road wheel roll in degrees. */ + public float wheelRoll; + public float prevWheelRoll; + /** Steering from -1 (full right) to 1 (full left). */ + public float steer; + public float prevSteer; + /** Track roller angle per side, index 1 is positive X. */ + public final float[] roller = new float[2]; + public final float[] prevRoller = new float[2]; + /** Track run per side, in link spacings from 0 to 1. */ + public final float[] track = new float[2]; + public final float[] prevTrack = new float[2]; + private final float[] trackSpeed = new float[2]; + public float[] rotatingParts = new float[0]; + public float[] prevRotatingParts = new float[0]; + public float throttle; + public float prevThrottle; + /** Per seat and rack: view yaw and pitch of the occupant relative to the hull. */ + public float[] seatYaw = new float[0]; + public float[] prevSeatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public float[] prevSeatPitch = new float[0]; + + void resize(AircraftDefinition def) { + rotatingParts = new float[def.rotatingParts.size()]; + prevRotatingParts = new float[def.rotatingParts.size()]; + int seats = def.seatAndRackCount(); + seatYaw = new float[seats]; + prevSeatYaw = new float[seats]; + seatPitch = new float[seats]; + prevSeatPitch = new float[seats]; + for (int i = def.seatCount(); i < seats; i++) { + RackDefinition rack = def.rack(i); + seatYaw[i] = prevSeatYaw[i] = rack.yaw(); + seatPitch[i] = prevSeatPitch[i] = rack.pitch(); + } + } + + void tick(AircraftEntity vehicle, AircraftDefinition def) { + AircraftEntity.ControlInput input = vehicle.input(); + float visibleThrottle = vehicle.visibleThrottle(); + float reverse = vehicle.visibleReverse(); + prevThrottle = throttle; + throttle = visibleThrottle; + + tickWheels(def, input, visibleThrottle, reverse); + tickTracks(def, input, visibleThrottle, reverse); + tickRotatingParts(def, vehicle.getControllingPassenger() != null); + tickCrewAim(vehicle, def); + } + + private void tickWheels(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + prevWheelRoll = wheelRoll; + prevSteer = steer; + if (def.enableBack && reverse > 0.01F && throttle <= 0.0) { + throttle = -reverse * 15.0F; + } + if (input.left && !input.right) { + steer = Math.min(steer + 0.1F, 1.0F); + } else if (input.right && !input.left) { + steer = Math.max(steer - 0.1F, -1.0F); + } else { + steer *= 0.9F; + } + wheelRoll += (float) (throttle * def.partWheelRot); + if (wheelRoll >= 360.0F) { + wheelRoll -= 360.0F; + prevWheelRoll -= 360.0F; + } else if (wheelRoll < 0.0F) { + wheelRoll += 360.0F; + prevWheelRoll += 360.0F; + } + } + + private void tickTracks(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + System.arraycopy(roller, 0, prevRoller, 0, 2); + System.arraycopy(track, 0, prevTrack, 0, 2); + double pivot = def.pivotTurnThrottle <= 0.0F ? 1.0 : def.pivotTurnThrottle * 0.1F; + boolean left = input.left; + boolean right = input.right; + int dir = 1; + if (def.enableBack && reverse > 0.0F && throttle <= 0.0) { + throttle = -reverse * 5.0F; + // Reversing swaps which track leads a turn. + if (left != right) { + boolean t = left; + left = right; + right = t; + dir = -1; + } + } + if (left && !right) { + double t = 0.2 * dir; + trackSpeed[0] += (float) t; + trackSpeed[1] -= (float) (pivot * t); + } else if (right && !left) { + double t = 0.2 * dir; + trackSpeed[0] -= (float) (pivot * t); + trackSpeed[1] += (float) t; + } else { + float t = (float) Mth.clamp(throttle, -0.2, 0.2); + trackSpeed[0] += t; + trackSpeed[1] += t; + } + + for (int i = 0; i < 2; i++) { + trackSpeed[i] = Mth.clamp(trackSpeed[i], -0.72F, 0.72F); + roller[i] += trackSpeed[i] * def.trackRollerRot; + if (roller[i] >= 360.0F) { + roller[i] -= 360.0F; + prevRoller[i] -= 360.0F; + } else if (roller[i] < 0.0F) { + roller[i] += 360.0F; + prevRoller[i] += 360.0F; + } + track[i] -= trackSpeed[i]; + while (track[i] >= 1.0F) { + track[i]--; + prevTrack[i]--; + } + while (track[i] < 0.0F) { + track[i]++; + } + while (prevTrack[i] < 0.0F) { + prevTrack[i]++; + } + trackSpeed[i] *= 0.75F; + } + } + + private void tickRotatingParts(AircraftDefinition def, boolean piloted) { + for (int i = 0; i < rotatingParts.length && i < def.rotatingParts.size(); i++) { + RotatingPart part = def.rotatingParts.get(i); + prevRotatingParts[i] = rotatingParts[i]; + if (part.always() || piloted) { + float r = rotatingParts[i] + part.speed(); + if (r < 0.0F) { + r += 360.0F; + prevRotatingParts[i] += 360.0F; + } else if (r >= 360.0F) { + r -= 360.0F; + prevRotatingParts[i] -= 360.0F; + } + rotatingParts[i] = r; + } + } + } + + private void tickCrewAim(AircraftEntity vehicle, AircraftDefinition def) { + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + float hullRoll = vehicle.roll(); + for (int i = 0; i < seatYaw.length && i < def.seatCount(); i++) { + prevSeatYaw[i] = seatYaw[i]; + prevSeatPitch[i] = seatPitch[i]; + Entity crew = vehicle.occupant(i); + if (crew == null) { + continue; + } + float yaw = Mth.wrapDegrees(crew.getYRot() - hullYaw); + double rad = Math.toRadians(yaw); + // The hull tilt along the crew member's view direction, as in the original tank code. + float tilt = (float) (hullPitch * Math.cos(rad) + hullRoll * Math.sin(rad)); + float pitch = crew.getXRot() - tilt; + // Keep the previous value on the same side of the seam so interpolation never spins. + prevSeatYaw[i] = yaw + Mth.wrapDegrees(prevSeatYaw[i] - yaw); + seatYaw[i] = yaw; + seatPitch[i] = pitch; + } + } + + public float seatYaw(int seat, float partialTicks) { + if (seat < 0 || seat >= seatYaw.length) { + return 0.0F; + } + return prevSeatYaw[seat] + (seatYaw[seat] - prevSeatYaw[seat]) * partialTicks; + } + + public float seatPitch(int seat, float partialTicks) { + if (seat < 0 || seat >= seatPitch.length) { + return 0.0F; + } + return prevSeatPitch[seat] + (seatPitch[seat] - prevSeatPitch[seat]) * partialTicks; + } +} From d0d0fd08712c5fa87a19c99b89a0e40a3f1a1c01 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:17 +0800 Subject: [PATCH 0238/1461] Add entity/ChainEntity.java to the 26.2 shared sources --- .../mcheli/entity/ChainEntity.java | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java new file mode 100644 index 00000000..4aaab44d --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java @@ -0,0 +1,193 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * A tow chain, {@code MCH_EntityChain}: it hangs above the vehicle that pulls and drags the towed + * one along, letting it drift up to the chain's length away and never more than two blocks past + * that. It goes when either end does. + */ +public class ChainEntity extends Entity { + public static final int MIN_LENGTH = 3; + public static final int MAX_LENGTH = 15; + /** Ticks a chain loaded from disk waits for both ends to load before it gives up. */ + private static final int SEARCH_TICKS = 50; + + private static final EntityDataAccessor DATA_TOW = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TOWED = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + + private @Nullable UUID towId; + private @Nullable UUID towedId; + private @Nullable Entity tow; + private @Nullable Entity towed; + private int length = 4; + private int searchTicks = SEARCH_TICKS; + + public ChainEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + /** Chains {@code towed} to {@code tow}, at the distance they are apart now. Server only. */ + public static ChainEntity connect(Level level, Entity towed, Entity tow) { + ChainEntity chain = new ChainEntity(ModEntities.CHAIN.get(), level); + chain.setPos(tow.position().add(towed.position()).scale(0.5)); + chain.length = Mth.clamp((int) towed.distanceTo(tow), MIN_LENGTH, MAX_LENGTH); + chain.attach(towed, tow); + level.addFreshEntity(chain); + ModSounds.playAt(chain.tow, "chain_ct", 1.0F, 1.0F); + return chain; + } + + /** Whether {@code entity} can pull the other end: only vehicles can. */ + public static boolean canTow(Entity entity) { + return entity instanceof AircraftEntity; + } + + /** + * Only a vehicle pulls. Hooked the other way round, a container would hold a helicopter down, + * so the ends swap whichever order they were clicked in (and chains saved that way are fixed + * when they load). + */ + private void attach(Entity towed, Entity tow) { + if (!canTow(tow) && canTow(towed)) { + Entity swap = tow; + tow = towed; + towed = swap; + } + this.towed = towed; + this.tow = tow; + this.towedId = towed.getUUID(); + this.towId = tow.getUUID(); + entityData.set(DATA_TOWED, towed.getId()); + entityData.set(DATA_TOW, tow.getId()); + searchTicks = 0; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_TOW, -1); + builder.define(DATA_TOWED, -1); + } + + /** The end that pulls; on a client, as far as it has loaded. */ + public @Nullable Entity tow() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOW)) : tow; + } + + /** The end that is pulled. */ + public @Nullable Entity towed() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOWED)) : towed; + } + + /** Whether {@code entity} is either end of this chain. */ + public boolean holds(Entity entity) { + return entity == tow() || entity == towed(); + } + + /** Takes the chain off, with the sound of it coming loose. Server only. */ + public void disconnect() { + ModSounds.playAt(this, "chain_ct", 1.0F, 1.0F); + discard(); + } + + /** The chain on {@code entity}, at either end, if there is one within reach. */ + public static @Nullable ChainEntity on(Entity entity) { + for (ChainEntity chain : entity.level().getEntitiesOfClass(ChainEntity.class, entity.getBoundingBox().inflate(25.0))) { + if (chain.holds(entity)) { + return chain; + } + } + return null; + } + + @Override + public void tick() { + super.tick(); + if (!(level() instanceof ServerLevel server)) { + return; + } + if (searchTicks > 0) { + searchTicks--; + Entity foundTowed = towedId != null ? server.getEntity(towedId) : null; + Entity foundTow = towId != null ? server.getEntity(towId) : null; + if (foundTowed != null && foundTow != null) { + attach(foundTowed, foundTow); + } + return; + } + if (tow == null || towed == null || tow.isRemoved() || towed.isRemoved() || tow == towed) { + disconnect(); + return; + } + towed.resetFallDistance(); + setPos(tow.getX(), tow.getY() + 2.0, tow.getZ()); + pull(tow, towed); + } + + /** {@code updateTowingEntityPosRot}: turns the towed end to face along the chain and drags it in. */ + private void pull(Entity tow, Entity towed) { + Vec3 d = towed.position().subtract(tow.position()); + double dist = d.length(); + if (dist <= length) { + return; + } + towed.setYRot((float) Math.toDegrees(Math.atan2(d.z, d.x)) + 90.0F); + double slack = dist - length; + towed.setDeltaMovement(towed.getDeltaMovement().subtract(d.scale(0.001 / slack))); + double max = length + 2.0; + if (dist > max) { + Vec3 at = tow.position().add(d.scale(max / dist)); + towed.setPos(at.x, at.y, at.z); + } + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + towId = input.read("Tow", UUIDUtil.CODEC).orElse(null); + towedId = input.read("Towed", UUIDUtil.CODEC).orElse(null); + length = Mth.clamp(input.getIntOr("Length", 4), MIN_LENGTH, MAX_LENGTH); + searchTicks = SEARCH_TICKS; + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + if (towId != null && towedId != null) { + output.store("Tow", UUIDUtil.CODEC, towId); + output.store("Towed", UUIDUtil.CODEC, towedId); + } + output.putInt("Length", length); + } +} From 66289ba01b3cbdf05240030c178f8d4dab6341fe Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:17 +0800 Subject: [PATCH 0239/1461] Add entity/ClientAccess.java to the 26.2 shared sources --- .../mcheli/entity/ClientAccess.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java new file mode 100644 index 00000000..28f14851 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * The few client facts common code needs, such as who the local player is. The client installs + * the real supplier at startup; a dedicated server keeps the empty default and never loads a + * client class through here. + */ +public final class ClientAccess { + private static Supplier<@Nullable Entity> localPlayer = () -> null; + private static PackReceiver packs = new PackReceiver() { + @Override + public void onPackList(PackListPacket packet) { + } + + @Override + public void onPackData(PackDataPacket packet) { + } + + @Override + public void onPackSound(PackSoundPacket packet) { + } + }; + + private ClientAccess() { + } + + public static void install(Supplier<@Nullable Entity> player, PackReceiver receiver) { + localPlayer = player; + packs = receiver; + } + + public static @Nullable Entity localPlayer() { + return localPlayer.get(); + } + + /** Where the server's content pack packets go. Called on the client's main thread. */ + public static PackReceiver packs() { + return packs; + } + + /** The client half of the content pack sync. */ + public interface PackReceiver { + void onPackList(PackListPacket packet); + + void onPackData(PackDataPacket packet); + + void onPackSound(PackSoundPacket packet); + } +} From ee7c35584db8a761fa5c2da94db55e5b84c741c8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:18 +0800 Subject: [PATCH 0240/1461] Hold and run the interpolation handler in ContainerEntity on 26.2 --- .../mcheli/entity/ContainerEntity.java | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java new file mode 100644 index 00000000..76880875 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java @@ -0,0 +1,152 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A cargo container, {@code MCH_EntityContainer}: a crate with a large chest's worth of room that + * sits on the ground, floats, can be pushed around, and rides the racks of the vehicles that carry + * {@code container}. Dropped from a rack high enough it comes down under a parachute. + */ +public class ContainerEntity extends Entity { + /** What racks call it in their {@code AddRack} lines. */ + public static final String CARGO_NAME = "container"; + private static final int SLOTS = 54; + /** Punches add up; the original broke it past 40 with 20 per point of damage. */ + private static final int BREAK_DAMAGE = 40; + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private final SimpleContainer items = new SimpleContainer(SLOTS); + private int damageTaken; + + public ContainerEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide() || isPassenger()) { + return; + } + if (damageTaken > 0) { + damageTaken--; + } + Vec3 m = getDeltaMovement(); + double my = m.y; + if (isInWater() || isInLava()) { + // Bobs up to the surface rather than sinking, as the original's boat physics did. + my = (my < 0.0 ? my / 2.0 : my) + (isEyeInFluid(FluidTags.WATER) ? 0.04 : 0.007); + } else { + my -= 0.04; + } + double mx = m.x; + double mz = m.z; + if (onGround()) { + mx *= 0.9; + mz *= 0.9; + } + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand, Vec3 location) { + if (player.getItemInHand(hand).getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (player.isSecondaryUseActive()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer serverPlayer) { + serverPlayer.openMenu(new SimpleMenuProvider((id, inventory, p) -> ChestMenu.sixRows(id, inventory, items), + Component.translatable("entity.mcheli.container"))); + } + return InteractionResult.SUCCESS; + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved() || !(source.getEntity() instanceof Player player)) { + // Only players take it apart; explosions and stray rounds leave the cargo alone. + return false; + } + ModSounds.playAt(this, "hit", 1.0F, 1.3F); + damageTaken += (int) (damage * 20.0F); + boolean creative = player.getAbilities().instabuild; + if (creative || damageTaken > BREAK_DAMAGE) { + if (!creative) { + spawnAtLocation(level, ModItems.CONTAINER.get()); + } + Containers.dropContents(level, this, items); + discard(); + } + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean isPushable() { + return !isPassenger(); + } + + @Override + public boolean canBeCollidedWith(@Nullable Entity other) { + return !isPassenger(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.CONTAINER.get()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + items.clearContent(); + ContainerHelper.loadAllItems(input, items.getItems()); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + ContainerHelper.saveAllItems(output, items.getItems(), false); + } +} From b0bed7393aeffff52e9e4ece0a29520cfe5a4ac8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:18 +0800 Subject: [PATCH 0241/1461] Add entity/EngineSoundSource.java to the 26.2 shared sources --- .../mcheli/entity/EngineSoundSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java new file mode 100644 index 00000000..6b25a388 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.entity; + +/** + * A vehicle with an engine loop. The client sound player polls these values every tick; the + * definition's {@code SoundVolume} and {@code SoundPitch} are applied on top. + */ +public interface EngineSoundSource { + /** Loudness before the definition's multiplier. Zero silences the loop. */ + float engineVolume(); + + /** Pitch before the definition's multiplier. */ + float enginePitch(); +} From 43c24a33e3f28703b0b55bbd2aba04a925a0680e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:19 +0800 Subject: [PATCH 0242/1461] Hold and run the interpolation handler in GltdEntity on 26.2 --- .../jnhprojects/mcheli/entity/GltdEntity.java | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java new file mode 100644 index 00000000..aa13dba7 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java @@ -0,0 +1,213 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.weapon.CasStrike; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The ground laser target designator, {@code MCH_EntityGLTD}: a tripod a player sits behind. Its + * sight looks up to 70 degrees either side of where it was set up, zooms and has night vision, + * and marking a spot calls in an A-10 for three gun runs over it. + */ +public class GltdEntity extends Entity { + /** How far the sight turns from the tripod's heading, either way, and up or down. */ + public static final float VIEW_LIMIT = 70.0F; + /** The CAS weapon the A-10s fire; the original hard coded it too. */ + private static final String WEAPON = "a10gau8"; + /** Added to the weapon's own {@code Delay} between two calls. */ + private static final int EXTRA_WAIT = 150; + private static final double SIGHT_RANGE = 300.0; + + private static final EntityDataAccessor DATA_WAIT = SynchedEntityData.defineId(GltdEntity.class, EntityDataSerializers.INT); + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private final CasStrike strike = new CasStrike(WEAPON); + private boolean nightVision; + + public GltdEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WAIT, 0); + } + + /** Ticks until the next A-10 can be called; the sight shows it. */ + public int waitTicks() { + return entityData.get(DATA_WAIT); + } + + /** The eye of the sight, a little in front of and above the tripod. */ + public Vec3 sightPosition(float partialTicks) { + float yaw = getYRot(partialTicks) * Mth.DEG_TO_RAD; + Vec3 pos = getPosition(partialTicks); + return new Vec3(pos.x - Mth.sin(yaw) * 0.6, pos.y + 0.7, pos.z + Mth.cos(yaw) * 0.6); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.04, 0.0); + if (onGround()) { + m = m.scale(0.5); + } + move(MoverType.SELF, m); + setDeltaMovement(m.x * 0.99, m.y * 0.95, m.z * 0.99); + + if (waitTicks() > 0) { + entityData.set(DATA_WAIT, waitTicks() - 1); + } + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (def != null && level() instanceof ServerLevel server) { + strike.tick(server, def); + } + Entity viewer = getFirstPassenger(); + if (viewer == null) { + nightVision = false; + } else if (nightVision) { + NightVision.topUp(viewer); + } + } + + /** + * {@code useCurrentWeapon}: marks the block {@code player} looks at through the sight and calls + * the A-10 on it. Server only. + * + * @return whether the strike was called; the sight may be waiting, or see nothing in range + */ + public boolean callStrike(ServerPlayer player) { + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (getFirstPassenger() != player || waitTicks() > 0 || def == null || !(level() instanceof ServerLevel server)) { + return false; + } + Vec3 from = sightPosition(1.0F); + Vec3 to = from.add(player.getLookAngle().scale(SIGHT_RANGE)); + BlockHitResult hit = server.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + strike.start(hit.getLocation(), player.getYRot(), this, player); + entityData.set(DATA_WAIT, def.delay + EXTRA_WAIT); + ModSounds.playAt(server, getX(), getY(), getZ(), "gltd", 0.5F, 1.0F); + if (def.sound != null) { + ModSounds.playTo(player, def.sound, 1.0F, 1.0F); + } + return true; + } + + public void setNightVision(Player player, boolean on) { + if (getFirstPassenger() != player) { + return; + } + nightVision = on; + if (!on) { + NightVision.end(player); + } + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand, Vec3 location) { + if (player.isSecondaryUseActive() || isVehicle()) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + player.setYRot(getYRot()); + player.setXRot(0.0F); + player.startRiding(this); + } + return InteractionResult.SUCCESS; + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (!level().isClientSide() && nightVision) { + NightVision.end(passenger); + nightVision = false; + } + } + + /** The viewer sits behind the tripod. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, -0.5).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + /** The original clamped the viewer's head the same way every tick. */ + @Override + public void onPassengerTurned(Entity passenger) { + float yaw = getYRot() + Mth.clamp(Mth.wrapDegrees(passenger.getYRot() - getYRot()), -VIEW_LIMIT, VIEW_LIMIT); + passenger.setYRot(yaw); + passenger.setYHeadRot(yaw); + passenger.setXRot(Mth.clamp(passenger.getXRot(), -VIEW_LIMIT, VIEW_LIMIT)); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved()) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + if (!creative) { + spawnAtLocation(level, ModItems.GLTD.get()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), "hit", 1.0F, 1.0F); + discard(); + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.GLTD.get()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } +} From 5f02120a9c6c6100ea175e992e3b22f7a529c559 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:19 +0800 Subject: [PATCH 0243/1461] Add entity/GroundVehicleEntity.java to the 26.2 shared sources --- .../mcheli/entity/GroundVehicleEntity.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java new file mode 100644 index 00000000..30290141 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java @@ -0,0 +1,130 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Static guns and launchers, ported from {@code MCH_EntityVehicle}. + * + *

The mount itself does not turn; the gunner looks around and the {@code AddPart} tree turns + * with the view. {@code CanMove} lets W/S push the mount and A/D turn it, for the few that set it. + */ +public class GroundVehicleEntity extends AircraftEntity implements EngineSoundSource { + private static final double MIN_SPEED = 0.07; + private double currentSpeed = MIN_SPEED; + + public GroundVehicleEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable VehicleDefinition lookup(String name) { + return MCHeli.content().vehicle(name); + } + + @Override + public @Nullable VehicleDefinition definition() { + return (VehicleDefinition) super.definition(); + } + + /** + * {@code MCH_EntityVehicle.useCurrentWeapon}: the mount stays put while its {@code AddPart} + * tree turns with the gunner's view, so a weapon fires where the gunner looks. Only weapons + * with both yaw limits set aim like turrets instead. The original checked the first + * {@code AddWeapon} line of that weapon, and so does this. + */ + @Override + public boolean aimsWithUser(WeaponSet set) { + WeaponMount mount = set.mounts().getFirst(); + return mount.minYaw() == 0.0F || mount.maxYaw() == 0.0F; + } + + @Override + protected void simulate() { + VehicleDefinition def = definition(); + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + + if (getControllingPassenger() == null) { + throttle = Math.max(throttle - 0.00125, 0.0); + } else if (def.canMove) { + double x = 0.0; + double z = 0.0; + double yaw = Math.toRadians(getYRot()); + if (input.throttleUp) { + x += Math.sin(yaw); + z += Math.cos(yaw); + } + if (input.throttleDown) { + x -= Math.sin(yaw); + z -= Math.cos(yaw); + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + double d = Math.sqrt(x * x + z * z); + if (d > 1.0E-6) { + mx -= x / d * 0.03; + mz += z / d * 0.03; + } + } + if (isInWater()) { + setXRot(getXRot() * 0.9F); + } + + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public float engineVolume() { + return visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return visibleThrottle() * 0.5F; + } +} From fd063e659f128e3c93857e9e0b7473aab126bb77 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:20 +0800 Subject: [PATCH 0244/1461] Add entity/HelicopterEntity.java to the 26.2 shared sources --- .../mcheli/entity/HelicopterEntity.java | 553 ++++++++++++++++++ 1 file changed, 553 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java new file mode 100644 index 00000000..957bf9ef --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java @@ -0,0 +1,553 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A helicopter, ported from {@code MCH_EntityHeli}. + * + *

The flight model is the original one: throttle produces lift that weakens as the aircraft + * tilts, and pitching the nose down converts throttle into forward speed through a cubic curve. + * Hover mode holds full throttle, levels the aircraft and lets W/A/S/D slide it around instead. + * + *

Two deliberate differences from 1.7.10: + *

    + *
  • The original ran roll damping once per rendered frame, so it depended on frame rate. It + * now runs once per tick with constants tuned to match the feel at 60 FPS.
  • + *
  • Blades fold and unfold one after another at the same pace on both sides, from the + * synced fold state, instead of each client stepping its own copy.
  • + *
+ */ +public class HelicopterEntity extends AircraftEntity implements EngineSoundSource { + /** + * Rotation per mouse count. The original used {@code 0.06 * partialTicks} with the partial tick + * low-pass filtered to roughly 0.5. + */ + private static final float LOOK_SCALE = 0.03F; + /** Vanilla turns the view 0.15 degrees per mouse count, so this recovers the raw count. */ + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final float ROLL_DAMPING = 0.06F; + private static final float ROLL_DAMPING_HOVER = 0.1F; + private static final float ROLL_RATE = 1.8F; + private static final double MIN_SPEED = 0.07; + + private static final EntityDataAccessor DATA_FOLD = SynchedEntityData.defineId(HelicopterEntity.class, EntityDataSerializers.BYTE); + // DATA_FOLD values, as the original numbered them. + private static final byte FOLDED = 0; + private static final byte FOLDING = 1; + private static final byte UNFOLDED = 2; + private static final byte UNFOLDING = 3; + /** {@code MCH_Blade}: degrees a blade swings per tick while folding. */ + private static final float FOLD_SPEED = 2.0F; + private static final int FOLD_COOLDOWN = 40; + + private double currentSpeed = MIN_SPEED; + private float rotorAngle; + public float rotorAngleO; + /** Per rotor, per blade: where each blade points while they fold, and whether that rotor is folded. */ + private float[][] blades = new float[0][]; + private float[][] bladesO = new float[0][]; + private boolean[] rotorFolded = new boolean[0]; + private int foldCooldown; + private byte lastFold = -1; + + public HelicopterEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable HelicopterDefinition lookup(String name) { + return MCHeli.content().helicopter(name); + } + + @Override + public @Nullable HelicopterDefinition definition() { + return (HelicopterDefinition) super.definition(); + } + + @Override + public boolean steersWithMouse() { + return true; + } + + @Override + public float engineVolume() { + HelicopterDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return 0.2F + visibleThrottle() * 0.2F; + } + + /** Hovering or slow helicopters drop their gear sooner than ones passing over at speed. */ + @Override + protected float autoLowerGearThrottle() { + HelicopterDefinition def = definition(); + double dx = getX() - xo; + double dy = getY() - yo; + double dz = getZ() - zo; + float slow = def != null ? def.speed / 3.5F : 0.0F; + return dx * dx + dy * dy + dz * dz <= slow ? 0.8F : 0.3F; + } + + public float rotorAngle(float partialTicks) { + float prev = rotorAngleO; + if (rotorAngle - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - rotorAngle < -180.0F) { + prev += 360.0F; + } + return prev + (rotorAngle - prev) * partialTicks; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_FOLD, UNFOLDED); + } + + // Blade folding + + private byte foldState() { + return entityData.get(DATA_FOLD); + } + + private boolean hasFoldingRotors() { + HelicopterDefinition def = definition(); + return def != null && def.foldableBlades && !def.rotors.isEmpty(); + } + + /** {@code isFoldBlades}: folded, or on the way. */ + public boolean isFoldedBlades() { + return hasFoldingRotors() && foldState() == FOLDED; + } + + /** {@code canUseBlades}: the blades are out and none is still swinging into place. */ + public boolean canUseBlades() { + if (foldState() != UNFOLDED) { + return false; + } + for (int r = 0; r < rotorFolded.length; r++) { + if (rotorFolded[r]) { + return false; + } + } + return true; + } + + /** {@code canSwitchFoldBlades}: with the engine stopped and the last switch finished. */ + private boolean canSwitchFoldBlades() { + byte state = foldState(); + return hasFoldingRotors() && visibleThrottle() <= 0.01F && foldCooldown == 0 && (state == UNFOLDED || state == FOLDED); + } + + /** The fold key folds the blades of helicopters that can, and works the hatches otherwise. */ + @Override + public void toggleFold() { + if (canSwitchFoldBlades()) { + entityData.set(DATA_FOLD, foldState() == FOLDED ? UNFOLDING : FOLDING); + foldCooldown = FOLD_COOLDOWN; + return; + } + super.toggleFold(); + } + + /** + * {@code onUpdate_Rotor} with {@code MCH_Rotor.update} and {@code MCH_Blade}: spinning blades + * follow the rotor; folding ones swing one by one onto the tail, each a few degrees further + * than the one before, and swing out again until they meet the spinning rotor's angle. + */ + private void tickBlades(HelicopterDefinition def) { + int rotors = def.rotors.size(); + if (blades.length != rotors) { + blades = new float[rotors][]; + bladesO = new float[rotors][]; + rotorFolded = new boolean[rotors]; + for (int r = 0; r < rotors; r++) { + int count = Math.max(def.rotors.get(r).blades(), 1); + blades[r] = new float[count]; + bladesO[r] = new float[count]; + } + if (foldState() == FOLDED || foldState() == FOLDING) { + forceFold(def); + } + } + byte state = foldState(); + if (state != lastFold) { + lastFold = state; + foldCooldown = FOLD_COOLDOWN; + } else if (foldCooldown > 0) { + foldCooldown--; + } + boolean target = state == FOLDING || state == FOLDED; + boolean settled = true; + for (int r = 0; r < rotors; r++) { + RotorDefinition rotor = def.rotors.get(r); + boolean wanted = target && rotor.foldable(); + boolean done = true; + for (int b = 0; b < blades[r].length; b++) { + float base = b * Math.max(rotor.bladeStep(), 1); + float fold = 5.0F + b * 3.0F; + bladesO[r][b] = blades[r][b]; + if (!rotorFolded[r]) { + if (!wanted) { + blades[r][b] = wrap(rotorAngle + base); + done = false; + } else if (!foldBlade(r, b, fold)) { + done = false; + } + } else if (wanted) { + done = false; + } else if (!unfoldBlade(r, b, base)) { + done = false; + } + } + if (done) { + rotorFolded[r] = wanted; + } + if (rotorFolded[r] != wanted) { + settled = false; + } + } + if (settled && !level().isClientSide()) { + if (state == FOLDING) { + entityData.set(DATA_FOLD, FOLDED); + } else if (state == UNFOLDING) { + entityData.set(DATA_FOLD, UNFOLDED); + } + } + } + + /** {@code MCH_Blade.folding}: towards the nearer side of the tail. */ + private boolean foldBlade(int r, int b, float fold) { + float angle = blades[r][b]; + if (angle > fold && angle < 360.0F - fold) { + blades[r][b] = wrap(angle < 180.0F ? angle - FOLD_SPEED : angle + FOLD_SPEED); + return false; + } + return true; + } + + /** {@code MCH_Blade.unfolding}: back round until the blade meets its place on the turning rotor. */ + private boolean unfoldBlade(int r, int b, float base) { + float target = wrap(rotorAngle + base); + float before = blades[r][b]; + if (target <= 180.0F) { + blades[r][b] = wrap(before + FOLD_SPEED); + if (blades[r][b] >= target && before <= target) { + blades[r][b] = target; + return true; + } + } else { + blades[r][b] = wrap(before - FOLD_SPEED); + if (blades[r][b] <= target && before >= target) { + blades[r][b] = target; + return true; + } + } + return false; + } + + /** {@code forceFold}: a helicopter loaded with its blades folded starts with them folded. */ + private void forceFold(HelicopterDefinition def) { + for (int r = 0; r < blades.length; r++) { + if (!def.rotors.get(r).foldable()) { + continue; + } + for (int b = 0; b < blades[r].length; b++) { + float fold = 5.0F + b * 3.0F; + float angle = wrap(rotorAngle + b * Math.max(def.rotors.get(r).bladeStep(), 1)); + if (angle > fold && angle < 360.0F - fold) { + angle = angle < 180.0F ? fold : 360.0F - fold; + } + blades[r][b] = angle; + bladesO[r][b] = angle; + } + rotorFolded[r] = true; + } + } + + private static float wrap(float degrees) { + float d = degrees % 360.0F; + return d < 0.0F ? d + 360.0F : d; + } + + /** Where blade {@code blade} of rotor {@code rotor} points, for drawing. */ + public float bladeAngle(int rotor, int blade, int step, float partialTicks) { + if (rotor >= blades.length || blade >= blades[rotor].length) { + return rotorAngle(partialTicks) + blade * step; + } + float prev = bladesO[rotor][blade]; + float now = blades[rotor][blade]; + if (now - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - now < -180.0F) { + prev += 360.0F; + } + return prev + (now - prev) * partialTicks; + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + super.addAdditionalSaveData(output); + output.putBoolean("FoldBlade", foldState() == FOLDED || foldState() == FOLDING); + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + super.readAdditionalSaveData(input); + entityData.set(DATA_FOLD, input.getBooleanOr("FoldBlade", false) ? FOLDED : UNFOLDED); + } + + /** Pilot toggles hover mode. Only allowed while fairly level and not in gunner mode, as in the original. */ + public void toggleHovering() { + if (isHoveringMode()) { + setHovering(false); + } else if (!isGunnerMode() && isLevelEnough() && canUseBlades()) { + setHovering(true); + } + } + + private boolean isLevelEnough() { + return Math.abs(Mth.wrapDegrees(roll())) < 40.0F && Math.abs(Mth.wrapDegrees(getXRot())) < 40.0F; + } + + @Override + public boolean canSwitchGunnerMode() { + return super.canSwitchGunnerMode() && isLevelEnough() && canUseBlades(); + } + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + HelicopterDefinition def = definition(); + if (def == null) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + + float yaw = 0.0F; + float pitch = 0.0F; + float rollDelta = 0.0F; + if (!isNearGround(2)) { + yaw = clamp(x, 40.0F * def.mobilityYaw) * LOOK_SCALE; + } + // Standing on its skids or wheels a helicopter stays level; the mouse only tilts it in the air. + if (!isHovering() && !onGround()) { + pitch = clamp(y, 40.0F * def.mobilityPitch) * LOOK_SCALE; + rollDelta = clamp(x, 40.0F * def.mobilityRoll) * LOOK_SCALE; + } + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + // Keep yaw continuous so the pilot's own yaw never jumps across the +-180 seam. + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + @Override + protected void simulate() { + HelicopterDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + + updateThrottle(def, piloted); + updateAttitude(def); + updateMotion(def); + } + + /** {@code onUpdate_Control} and the hover/non-hover branches. */ + private void updateThrottle(HelicopterDefinition def, boolean piloted) { + if (piloted) { + Vec3 motion = getDeltaMovement(); + if (!canUseBlades()) { + // Folded blades give no lift; on the ground A/D taxi the helicopter round slowly. + throttle = Math.max(0.0, throttle - 0.02); + if (onGround() && isFoldedBlades() && !isDestroyed()) { + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + } + return; + } + if (!isHovering()) { + if (input.throttleUp) { + throttle = Math.min(1.0, throttle + 0.02 * def.throttleUpDown); + } else if (input.throttleDown) { + throttle = Math.max(0.0, throttle - (1.0 / 70.0) * def.throttleUpDown); + } + motion = strafe(motion, false, input.left, input.right, 0.02 * def.speed); + } else { + throttle = Math.min(1.0, throttle + 1.0 / 30.0); + motion = strafe(motion, input.throttleUp, input.left, input.right, 0.01 * def.speed); + if (input.throttleDown) { + motion = push(motion, getYRot() - 180.0F, 0.01 * def.speed); + } + } + setDeltaMovement(motion); + } else { + throttle = Math.max(0.0, throttle - 0.00125); + if (isHoveringMode() && !level().isClientSide()) { + setHovering(false); + } + } + } + + private Vec3 strafe(Vec3 motion, boolean forward, boolean left, boolean right, double amount) { + if (forward) { + motion = push(motion, getYRot(), amount); + } + if (left && !right) { + motion = push(motion, getYRot() - 90.0F, amount); + } else if (right && !left) { + motion = push(motion, getYRot() + 90.0F, amount); + } + return motion; + } + + /** Adds {@code amount} of horizontal motion towards {@code yaw}, Minecraft style (0 = south). */ + private static Vec3 push(Vec3 motion, float yaw, double amount) { + double rad = Math.toRadians(yaw); + return motion.add(-Math.sin(rad) * amount, 0.0, Math.cos(rad) * amount); + } + + /** Per-tick version of {@code onUpdateAngles}: roll levels out, A/D bank in the air. */ + private void updateAttitude(HelicopterDefinition def) { + float damping = 1.0F - (isHovering() ? ROLL_DAMPING_HOVER : ROLL_DAMPING); + if (Math.abs(roll) > 0.1F && Math.abs(roll) < 65.0F) { + roll *= damping; + } + if (getControllingPassenger() != null && !isNearGround(3)) { + if (input.left && !input.right) { + roll -= ROLL_RATE; + } else if (input.right && !input.left) { + roll += ROLL_RATE; + } + } + if (isHovering() && Math.abs(getXRot()) < 70.0F) { + setXRot(getXRot() * 0.95F); + } + if (def.limitRotation) { + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + /** {@code onUpdate_Server}: gravity, lift, forward thrust, speed limit and ground friction. */ + private void updateMotion(HelicopterDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + float groundPitch = def.onGroundPitch; + + if (!isHovering()) { + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + double yawRad = Math.toRadians(getYRot()); + float pitch = getXRot(); + if (isNearGround(3)) { + pitch -= groundPitch; + } + double thrust = 0.1 * currentSpeed * (pitch * pitch * pitch / 30000.0F) * throttle; + mx += Math.sin(yawRad) * -thrust; + mz += Math.cos(yawRad) * thrust; + + double tilt = (Math.abs(getXRot()) + Math.abs(roll)) * 0.6; + double lift = tilt <= 50.0 ? 1.0 - tilt / 50.0 : 0.0; + my += (lift * 0.025 + 0.03) * throttle; + } else { + levelOut(groundPitch); + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my *= 0.7; + } + my += 0.007; + } + } + } else { + if (random.nextInt(50) == 0) { + mx += (random.nextDouble() - 0.5) / 30.0; + } + if (random.nextInt(50) == 0) { + my += (random.nextDouble() - 0.5) / 50.0; + } + if (random.nextInt(50) == 0) { + mz += (random.nextDouble() - 0.5) / 30.0; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + double limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + levelOut(groundPitch); + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + private void levelOut(float groundPitch) { + if (Math.abs(getXRot()) < 40.0F) { + setXRot((getXRot() - groundPitch) * 0.9F + groundPitch); + } + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + } + + @Override + protected void tickAnimation() { + HelicopterDefinition def = definition(); + rotorAngleO = rotorAngle; + float spool = 1.0F - visibleThrottle(); + rotorAngle = (rotorAngle + (1.0F - spool * spool * spool) * def.rotorSpeed) % 360.0F; + tickBlades(def); + } +} From b351866f885cef149ea3af20019d308a69c24b04 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:20 +0800 Subject: [PATCH 0245/1461] Add entity/NightVision.java to the 26.2 shared sources --- .../mcheli/entity/NightVision.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java new file mode 100644 index 00000000..f52c4227 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; + +/** + * Night vision through a sight, {@code MCH_Camera.updateViewer}: a short effect kept topped up while + * the viewer has it switched on, and taken away again when they switch it off or leave. + */ +public final class NightVision { + private static final int TICKS = 300; + + private NightVision() { + } + + /** Called every tick for each viewer. It never runs below ten seconds, where vanilla starts to flicker the view. */ + public static void topUp(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current == null || current.getDuration() < TICKS - 40) { + living.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, TICKS, 0, true, false, false)); + } + } + } + + /** Removes ours, leaving a longer one from a potion alone. */ + public static void end(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current != null && current.getDuration() <= TICKS) { + living.removeEffect(MobEffects.NIGHT_VISION); + } + } + } +} From 77a56f706cc02ecbfbfca0f11aeb21561f0a0f9a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:21 +0800 Subject: [PATCH 0246/1461] Hold and run the interpolation handler in ParachuteEntity on 26.2 --- .../mcheli/entity/ParachuteEntity.java | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java new file mode 100644 index 00000000..f7a7db39 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java @@ -0,0 +1,188 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A parachute with someone hanging under it, {@code MCH_EntityParachute}. It sinks at about two + * blocks a second, the rider steers it with the forward key, and it folds away a moment after it + * touches the ground or the water. + */ +public class ParachuteEntity extends Entity { + /** What the parachute looks like and how it opens, the original's model type. */ + public enum Kind { + /** From the parachute item: open from the start. */ + ITEM, + /** An ejection seat: the rider falls in the seat and the canopy opens once it drops. */ + EJECTION_SEAT, + /** Troops dropped from a rack or a cargo door: open from the start, no seat. */ + DROP + } + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(ParachuteEntity.class, EntityDataSerializers.INT); + private static final double MAX_SPEED = 0.35; + private static final double MIN_STEER = 0.07; + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private double steer = MIN_STEER; + private int onGroundCount; + /** Who boards on the next server tick; boarding in the same tick as the spawn can be lost. */ + private @Nullable Entity user; + + public ParachuteEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Opens a parachute above {@code user}, carrying over their speed, and puts them under it. + * Server only. + */ + public static ParachuteEntity open(Level level, Entity user, Kind kind) { + ParachuteEntity parachute = new ParachuteEntity(ModEntities.PARACHUTE.get(), level); + double lift = kind == Kind.ITEM ? 3.5 : 0.0; + parachute.setPos(user.getX(), user.getY() + lift, user.getZ()); + parachute.setYRot(user.getYRot()); + parachute.setDeltaMovement(user.getDeltaMovement()); + parachute.entityData.set(DATA_KIND, kind.ordinal()); + parachute.user = user; + user.resetFallDistance(); + level.addFreshEntity(parachute); + return parachute; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, Kind.ITEM.ordinal()); + } + + public Kind kind() { + Kind[] kinds = Kind.values(); + int i = entityData.get(DATA_KIND); + return i >= 0 && i < kinds.length ? kinds[i] : Kind.ITEM; + } + + /** An ejection seat only opens its canopy once it has started to fall. */ + public boolean isOpen() { + return kind() != Kind.EJECTION_SEAT || getDeltaMovement().y < -0.1; + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide()) { + return; + } + if (user != null) { + if (!user.isPassenger() && user.isAlive()) { + user.startRiding(this, true, true); + setYRot(user.getYRot()); + } + user = null; + } + Entity rider = getFirstPassenger(); + if (rider == null && tickCount > 20 || isInWater()) { + ejectPassengers(); + discard(); + return; + } + if (rider != null) { + rider.resetFallDistance(); + } + simulate(rider); + } + + /** {@code onUpdateServer}. */ + private void simulate(@Nullable Entity rider) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevSpeed = Math.sqrt(mx * mx + mz * mz); + boolean seatLaunch = kind() == Kind.EJECTION_SEAT && tickCount < 20; + my -= onGround() || seatLaunch ? 0.01 : 0.03; + + if (isOpen()) { + if (rider instanceof ServerPlayer player && player.getLastClientInput().forward()) { + double yaw = Math.toRadians(rider.getYRot()); + mx += -Math.sin(yaw) * steer * 0.05; + mz += Math.cos(yaw) * steer * 0.05; + } + double speed = Math.sqrt(mx * mx + mz * mz); + if (speed > MAX_SPEED) { + mx *= MAX_SPEED / speed; + mz *= MAX_SPEED / speed; + speed = MAX_SPEED; + } + // Holding course builds up speed; letting go settles back to a slow drift. + if (speed > prevSpeed) { + steer = Math.min(steer + (MAX_SPEED - steer) / 35.0, MAX_SPEED); + } else { + steer = Math.max(steer - (steer - MIN_STEER) / 35.0, MIN_STEER); + } + } + + if (onGround() && ++onGroundCount > 5) { + ejectPassengers(); + discard(); + return; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + my *= seatLaunch ? 0.95 : 0.9; + double drag = isOpen() ? 0.95 : 0.99; + setDeltaMovement(mx * drag, my, mz * drag); + if (rider != null) { + setYRot(rider.getYRot()); + } + } + + /** + * The server moves the parachute; clients ease towards each position it sends over three + * ticks, as for the vehicles. Without this a slow descent steps from one update to the next. + */ + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + entityData.set(DATA_KIND, input.getIntOr("Kind", Kind.ITEM.ordinal())); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putInt("Kind", kind().ordinal()); + } +} From 7ac92124a579812f932a7d4690f04b2a032693a1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:21 +0800 Subject: [PATCH 0247/1461] Add entity/PartState.java to the 26.2 shared sources --- .../jnhprojects/mcheli/entity/PartState.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/PartState.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/PartState.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/PartState.java new file mode 100644 index 00000000..54755d56 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/PartState.java @@ -0,0 +1,122 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; + +/** + * Open/closed state of a group of moving parts (canopy, landing gear, hatches, wings, VTOL nozzles). + * Ported from {@code MCH_Parts}. + * + *

The target state is one bit of a synched integer, so only the server changes it and every + * side sees the same value. The angle runs from 0 to {@link #max} at {@link #speed} degrees per tick + * and is advanced locally on every side, which keeps the animation smooth without sending angles. + */ +public final class PartState { + private final AircraftEntity owner; + private final int bit; + private final float max; + private final float speed; + private final Sounds sounds; + + private float angle; + private float prevAngle; + + PartState(AircraftEntity owner, int bit, float speed, Sounds sounds) { + this.owner = owner; + this.bit = bit; + this.max = 90.0F; + this.speed = speed; + this.sounds = sounds; + } + + /** Target state: {@code true} means open, folded, retracted or tilted, depending on the part. */ + public boolean status() { + return (owner.partBits() & (1 << bit)) != 0; + } + + /** Fully back in the default position. */ + public boolean isOff() { + return !status() && angle <= 0.02F; + } + + /** Fully in the switched position. */ + public boolean isOn() { + return status() && angle >= max - 0.02F; + } + + public float angle() { + return angle; + } + + public float angle(float partialTicks) { + return prevAngle + (angle - prevAngle) * partialTicks; + } + + /** Progress from 0 to 1. */ + public float factor() { + return angle / max; + } + + public float factor(float partialTicks) { + return angle(partialTicks) / max; + } + + /** Changes the target state. Server only; clients follow through synched data. */ + void set(boolean on, boolean playSound) { + if (owner.level().isClientSide() || status() == on) { + return; + } + owner.setPartBit(bit, on); + if (playSound) { + play(sounds.switching); + play(on ? sounds.startOn : sounds.startOff); + } + tick(); + } + + /** Jumps straight to {@code on} without animating, used for parts that start deployed. */ + void force(boolean on) { + if (!owner.level().isClientSide()) { + owner.setPartBit(bit, on); + } + angle = prevAngle = on ? max : 0.0F; + } + + void tick() { + prevAngle = angle; + if (status()) { + if (angle < max) { + angle = Math.min(angle + speed, max); + if (angle >= max) { + play(sounds.endOn); + } + } + } else if (angle > 0.0F) { + angle = Math.max(angle - speed, 0.0F); + if (angle <= 0.0F) { + play(sounds.endOff); + } + } + } + + private void play(Cue cue) { + if (cue != null && !owner.level().isClientSide()) { + ModSounds.playAt(owner, cue.sound(), 1.0F, cue.pitch()); + } + } + + /** One sound from {@code sounds.json} with its pitch. */ + record Cue(String sound, float pitch) { + } + + /** What plays when the part starts and finishes moving each way; {@code null} entries stay silent. */ + record Sounds(Cue startOn, Cue endOn, Cue startOff, Cue endOff, Cue switching) { + private static final Cue CLICK = new Cue("plane_cc", 1.0F); + private static final Cue CLICK_LOW = new Cue("plane_cc", 0.5F); + + static final Sounds HATCH = new Sounds(null, CLICK, null, CLICK, new Cue("plane_cv", 0.5F)); + static final Sounds CANOPY = new Sounds(null, CLICK, null, CLICK, null); + static final Sounds LANDING_GEAR = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.75F)); + static final Sounds NOZZLE = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.5F)); + static final Sounds WING = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, null); + } +} From f91c2f07ca644aebddf3db81c20cf5bae021f98f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:22 +0800 Subject: [PATCH 0248/1461] Add entity/PlaneEntity.java to the 26.2 shared sources --- .../mcheli/entity/PlaneEntity.java | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java new file mode 100644 index 00000000..b80171b8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java @@ -0,0 +1,516 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A fixed wing aircraft, ported from {@code MCP_EntityPlane}. + * + *

Throttle pushes the plane along its nose, tilted ten degrees up, and the vertical part of + * that push is what keeps it in the air: below full throttle it sinks. VTOL capable planes tilt + * their nozzles (and tilt rotors) towards straight down, which turns the push into lift. + * + *

The original applied its attitude rules once per rendered frame with the partial tick + * filtered to about 0.5. Here they run three times per tick with that value, which is what a + * 60 FPS client did, so the handling is the same without depending on the frame rate. + */ +public class PlaneEntity extends AircraftEntity implements EngineSoundSource { + private static final float LOOK_SCALE = 0.03F; + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private @Nullable PartState nozzle; + private @Nullable PartState wing; + + private double currentSpeed = MIN_SPEED; + private float keyRoll; + private float rotorAngle; + public float rotorAngleO; + private float soundLevel; + + public PlaneEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable PlaneDefinition lookup(String name) { + return MCHeli.content().plane(name); + } + + @Override + public @Nullable PlaneDefinition definition() { + return (PlaneDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + PlaneDefinition def = (PlaneDefinition) definition; + nozzle = def.hasNozzleState() ? addPart(PART_NOZZLE, 1.5F, PartState.Sounds.NOZZLE) : null; + wing = def.wings.isEmpty() ? null : addPart(PART_WING, 2.5F, PartState.Sounds.WING); + if (nozzle != null && def.defaultVtol && !nozzle.status()) { + nozzle.force(true); + } + } + + @Override + public boolean steersWithMouse() { + return true; + } + + public @Nullable PartState nozzle() { + return nozzle; + } + + public @Nullable PartState wing() { + return wing; + } + + public float nozzleAngle() { + return nozzle != null ? nozzle.angle() : 0.0F; + } + + @Override + public int vtolMode() { + if (nozzle == null) { + return 0; + } + if (!nozzle.status()) { + return nozzle.angle() <= 0.005F ? 0 : 1; + } + return nozzle.angle() >= 89.995F ? 2 : 1; + } + + public float rotorAngle(float partialTicks) { + return rotorAngleO + (rotorAngle - rotorAngleO) * partialTicks; + } + + // Pilot actions, server side + + public boolean canSwitchVtol() { + PlaneDefinition def = definition(); + if (def == null || !def.vtol || nozzle == null || modeSwitchCooldown > 0 || vtolMode() == 1) { + return false; + } + return Math.abs(roll) <= 30.0F && !(onGround() && def.defaultVtol); + } + + public void toggleVtol() { + if (canSwitchVtol()) { + modeSwitchCooldown = 20; + setVtol(!nozzle.status()); + } + } + + private void setVtol(boolean on) { + PlaneDefinition def = definition(); + if (nozzle == null || def == null || (def.defaultVtol && onGround() && !on)) { + return; + } + nozzle.set(on, true); + } + + public boolean canFoldWing() { + PlaneDefinition def = definition(); + if (wing == null || def == null || modeSwitchCooldown > 0) { + return false; + } + if (def.variableSweepWing) { + // Sweep back only when fast and well clear of the ground, spread again only when slow. + if (!onGround() && !hasBlockBelow(3, 20, true)) { + if (throttle < 0.7F) { + return false; + } + } else if (throttle > 0.1F) { + return false; + } + } else { + if (!onGround() && !hasBlockBelow(3, 3, true)) { + return false; + } + if (throttle > 0.01F) { + return false; + } + } + return wing.isOff(); + } + + public boolean canUnfoldWing() { + return wing != null && modeSwitchCooldown <= 0 && wing.isOn(); + } + + @Override + public void toggleFold() { + PlaneDefinition def = definition(); + if (def == null) { + return; + } + if (!def.hatches.isEmpty()) { + super.toggleFold(); + } else if (canFoldWing()) { + wing.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldWing()) { + wing.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Folded wings ground the plane; swept wings only below 20% throttle. */ + private boolean canUseWing(PlaneDefinition def) { + if (wing == null) { + return true; + } + if (def.variableSweepWing) { + return throttle >= 0.2 || wing.isOff(); + } + return wing.isOff(); + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void updatePartsServer(AircraftDefinition definition) { + super.updatePartsServer(definition); + PlaneDefinition def = (PlaneDefinition) definition; + // Swept wings spread out again when the plane slows down or gets close to the ground. + if (wing != null && def.variableSweepWing && wing.isOn() && throttle >= 0.2F + && (throttle < 0.5 || hasBlockBelow(1, 10, true))) { + wing.set(false, true); + } + if (nozzle != null && def.defaultVtol && onGround() && vtolMode() == 0) { + nozzle.set(true, true); + } + } + + // Mouse + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + PlaneDefinition def = definition(); + if (def == null || ticksAlive() < 30 || isPassenger() || hasBlockBelow(3, 2, true)) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + boolean vtol = vtolMode() > 0; + float yawFactor = (vtol ? def.vtolYaw : 1.0F) * 0.8F; + float pitchFactor = (vtol ? def.vtolPitch : 1.0F) * 0.8F; + float rollInput = vtolMode() == 0 ? x * 0.5F : x; + + float yaw = clamp(x, 40.0F * def.mobilityYaw) * yawFactor * LOOK_SCALE; + float pitch = clamp(y, 40.0F * def.mobilityPitch) * pitchFactor * LOOK_SCALE; + float rollDelta = clamp(rollInput, 40.0F * def.mobilityRoll) * yawFactor * LOOK_SCALE; + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + // Simulation + + @Override + protected void simulate() { + PlaneDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + if (def.targetDrone && !piloted) { + input.throttleUp = true; + } + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + } + updateMotion(def); + } + + /** + * Gunner mode needs a plane in level flight with throttle up and nothing solid within five + * blocks below, so the autopilot has room to circle. + */ + @Override + public boolean canSwitchGunnerMode() { + if (!super.canSwitchGunnerMode()) { + return false; + } + float r = Math.abs(Mth.wrapDegrees(roll())); + float p = Math.abs(Mth.wrapDegrees(getXRot())); + return r <= 40.0F && p <= 40.0F && visibleThrottle() > 0.6F && !hasBlockBelow(3, 5, false); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlNotHovering}. */ + private void updateThrottle(PlaneDefinition def, boolean piloted) { + reverse *= 0.8F; + boolean canFly = piloted && isCanopyClosed() && canUseWing(def); + if (canFly && isGunnerMode()) { + // The autopilot holds the throttle the pilot left it at. + return; + } + if (canFly || def.targetDrone) { + float upDown = def.throttleUpDown; + boolean turning = input.left != input.right; + boolean up = input.throttleUp; + if (turning && throttle < def.pivotTurnThrottle && !up && !input.throttleDown) { + up = true; + upDown *= 2.0F; + } + if (up) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * def.throttleUpDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}. In gunner mode the autopilot levels out and circles by {@code AutoPilotRot}. */ + private void updateAttitude(PlaneDefinition def, float pt) { + boolean gunner = isGunnerMode(); + if (gunner) { + setXRot(getXRot() * 0.95F); + setYRot(getYRot() + def.autoPilotRot * 0.2F); + if (Math.abs(roll) > 20.0F) { + roll *= 0.95F; + } + } + boolean flying = !hasBlockBelow(3, 3, true); + if (!flying || input.freeLook || gunner || (def.floats && waterDepth() > 0.0)) { + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.6F * yawRate * pt); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.6F * yawRate * pt); + } + } else { + // A/D bank the plane; in VTOL mode the keys do nothing in the air. + float rate = vtolMode() != 0 ? 0.0F : 0.2F; + if (input.left && !input.right) { + keyRoll -= rate * pt; + } else if (input.right && !input.left) { + keyRoll += rate * pt; + } + roll += keyRoll * 0.5F * def.mobilityRoll; + } + keyRoll *= 1.0F - 0.1F * pt; + if (!flying && Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.97F); + } + if (nozzleAngle() > 0.001F) { + setXRot(getXRot() * (1.0F - 0.03F * pt)); + roll *= 1.0F - 0.1F * pt; + } + roll = AircraftRotation.wrap(roll); + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), def.minRotationPitch, def.maxRotationPitch)); + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + private float maxSpeed(PlaneDefinition def) { + float extra = 0.0F; + if (def.variableSweepWing) { + if (wing != null) { + extra = (def.sweepWingSpeed - def.speed) * wing.factor(); + } else if (hatch != null) { + extra = (def.sweepWingSpeed - def.speed) * hatch.factor(); + } + } + return def.speed + extra; + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(PlaneDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + // The gunner mode autopilot holds its altitude. + boolean levelOff = isGunnerMode(); + + if (depth == 0.0) { + if (def.targetDrone) { + levelOff = targetDroneAutopilot(def) || levelOff; + } + if (!levelOff) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else { + my *= 0.8; + } + } else { + setXRot(getXRot() * 0.8F); + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + } + + double push = throttle / 10.0; + float nozzleAngle = nozzleAngle(); + Vec3 v; + if (nozzleAngle > 0.001F) { + setXRot(getXRot() * 0.95F); + v = direction(getYRot(), getXRot() - nozzleAngle); + if (nozzleAngle >= 90.0F) { + v = new Vec3(v.x * 0.8, v.y, v.z * 0.8); + } + } else { + v = direction(getYRot(), getXRot() - 10.0F); + } + if (!levelOff) { + my += v.y * push / (nozzleAngle <= 0.01F ? 2.0 : 8.0); + } + + boolean canMove = def.canMoveOnGround || !isOnSolidGround(); + if (canMove) { + if (def.enableBack && reverse > 0.0F) { + mx -= v.x * reverse; + mz -= v.z * reverse; + } else { + mx += v.x * push; + mz += v.z * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = maxSpeed(def); + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + if (Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.8F); + } + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** Target drones circle at a safe height on their own. Returns whether to hold altitude. */ + private boolean targetDroneAutopilot(PlaneDefinition def) { + if (hasBlockBelow(3, 40, true)) { + if (!hasBlockBelow(3, 5, true)) { + setYRot(getYRot() + def.autoPilotRot * 2.0F); + if (getXRot() > -20.0F) { + setXRot(getXRot() - 0.5F); + } + } + return false; + } + setYRot(getYRot() + def.autoPilotRot); + setXRot(getXRot() * 0.95F); + if (!level().isClientSide() && canRaiseLandingGear() && modeSwitchCooldown <= 0) { + toggleLandingGear(); + } + return true; + } + + /** {@code MCH_Lib.Rot2Vec3}: unit vector along a yaw and pitch, Minecraft conventions. */ + private static Vec3 direction(float yaw, float pitch) { + double y = Math.toRadians(yaw); + double p = Math.toRadians(pitch); + return new Vec3(-Math.sin(y) * Math.cos(p), -Math.sin(p), Math.cos(y) * Math.cos(p)); + } + + // Animation and sound + + @Override + protected void tickAnimation() { + PlaneDefinition def = definition(); + float throttle = visibleThrottle(); + rotorAngleO = rotorAngle; + rotorAngle += throttle * def.rotorSpeed; + if (rotorAngle > 360.0F) { + rotorAngle -= 360.0F; + rotorAngleO -= 360.0F; + } else if (rotorAngle < 0.0F) { + rotorAngle += 360.0F; + rotorAngleO += 360.0F; + } + + float target = throttle; + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + PlaneDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return 0.6F + visibleThrottle() * 0.4F; + } +} From 86143d48c7ea2aa2645289a894bce255313b168b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:22 +0800 Subject: [PATCH 0249/1461] Hold and run the interpolation handler in RappelEntity on 26.2 --- .../mcheli/entity/RappelEntity.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java new file mode 100644 index 00000000..e5781817 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java @@ -0,0 +1,106 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Carries someone down a helicopter's rope, {@code MCH_EntityHide}. It stays under its hook while + * the helicopter drifts, slides down at a steady pace and lets its rider off at the ground or at + * the end of the rope. Pulling the ropes in drops whoever is still on them. + */ +public class RappelEntity extends Entity { + private static final EntityDataAccessor DATA_VEHICLE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_ROPE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + + public RappelEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_VEHICLE, 0); + builder.define(DATA_ROPE, -1); + } + + public void attach(AircraftEntity vehicle, int rope) { + entityData.set(DATA_VEHICLE, vehicle.getId()); + entityData.set(DATA_ROPE, rope); + } + + private @Nullable AircraftEntity vehicle() { + return level().getEntity(entityData.get(DATA_VEHICLE)) instanceof AircraftEntity a && a.isAlive() ? a : null; + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + AircraftEntity vehicle = vehicle(); + Entity rider = getFirstPassenger(); + if (rider != null) { + rider.resetFallDistance(); + } + if (level().isClientSide()) { + return; + } + if (vehicle == null || !vehicle.isRepelling() || rider == null) { + arrive(); + return; + } + Vec3 hook = vehicle.ropePosition(entityData.get(DATA_ROPE)); + Vec3 m = getDeltaMovement(); + double fall = (m.y - (onGround() ? 0.01 : 0.03)) * 0.9; + setPos(hook.x, getY(), hook.z); + if (onGround()) { + arrive(); + return; + } + move(MoverType.SELF, new Vec3(0.0, fall, 0.0)); + setDeltaMovement(0.0, fall, 0.0); + if (Math.abs(getY() - hook.y) > Math.abs(vehicle.ropeLength()) + 5.0F) { + arrive(); + } + } + + private void arrive() { + ejectPassengers(); + discard(); + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } +} From 3152c7784c2f4fb1830292754524b419659a8d66 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:22 +0800 Subject: [PATCH 0250/1461] Add entity/TankEntity.java to the 26.2 shared sources --- .../jnhprojects/mcheli/entity/TankEntity.java | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java new file mode 100644 index 00000000..e729c7f5 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java @@ -0,0 +1,267 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.physics.GroundImpact; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Tanks and cars, ported from {@code MCH_EntityTank}. + * + *

The drive model is the plane's minus lift: throttle pushes along the hull, A/D turn it, + * S brakes and then reverses when {@code EnableBack} is set, and Space is a hand brake. The hull + * does not follow the mouse. Instead a {@link TerrainFollower} reads the terrain under the + * wheels and the hull eases towards that pitch and roll, while the crew look around freely and + * the turret follows their view. + */ +public class TankEntity extends AircraftEntity implements EngineSoundSource { + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private final TerrainFollower terrain = new TerrainFollower(this); + private double currentSpeed = MIN_SPEED; + /** + * Squared horizontal distance covered by last tick's move. The original read + * {@code posX - prevPosX} here, but vanilla resets {@code xo} before {@link #simulate()} runs, + * which left vehicles with {@code PivotTurnThrottle} unable to steer. + */ + private double lastStepSq; + private float soundLevel; + private float turnSound; + + public TankEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable TankDefinition lookup(String name) { + return MCHeli.content().tank(name); + } + + @Override + public @Nullable TankDefinition definition() { + return (TankDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + TankDefinition def = (TankDefinition) definition; + terrain.create(def.groundWheels, new Vector3(0.0, -0.35, def.weightedCenterZ)); + } + + /** Walls and long drops hurt tanks and cars, scaled by speed and height. */ + @Override + protected boolean takesGroundImpacts() { + return true; + } + + /** Tanks climb and tilt on slopes all the time; only rolling over damages them. */ + @Override + protected float crashTilt() { + return 91.0F; + } + + @Override + protected void simulate() { + TankDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), -90.0F, 90.0F)); + roll = Mth.clamp(roll, -90.0F, 90.0F); + } + } + updateMotion(def, piloted); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlSub}. */ + private void updateThrottle(TankDefinition def, boolean piloted) { + reverse *= 0.8F; + float upDown = def.throttleUpDown; + if (input.brake) { + reverse *= 0.5F; + throttle = Math.max(throttle - 0.02 * upDown, 0.0); + } + if ((piloted && isCanopyClosed()) || def.targetDrone) { + if (input.throttleUp || (def.targetDrone && !piloted)) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * upDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}: ease onto the terrain, steer with A/D. */ + private void updateAttitude(TankDefinition def, float pt) { + setXRot(getXRot() + (terrain.targetPitch - getXRot()) * pt); + roll += (terrain.targetRoll - roll) * pt; + + boolean flying = !hasBlockBelow(3, 3, true); + if (flying && !(def.floats && waterDepth() > 0.0)) { + return; + } + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + float pivot = def.pivotTurnThrottle; + double dist = lastStepSq; + // With PivotTurnThrottle set the vehicle has to be rolling before it can turn, like a car. + if (pivot <= 0.0F || throttle >= pivot || reverse >= pivot / 10.0F || dist > reverse * 0.01) { + float speedScale = pivot <= 0.0F ? 1.0F : (float) Math.sqrt(Math.min(dist, 1.0)); + float direction = !input.throttleUp && input.throttleDown && throttle < pivot + 0.05 ? -1.0F : 1.0F; + float step = 0.6F * yawRate * pt * direction * speedScale; + if (input.left && !input.right) { + setYRot(getYRot() - step); + } else if (input.right && !input.left) { + setYRot(getYRot() + step); + } + } + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(TankDefinition def, boolean piloted) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + + if (depth == 0.0) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double push = throttle / 10.0; + double yawRad = Math.toRadians(getYRot()); + double pitchRad = Math.toRadians(getXRot() - 10.0F); + double vx = -Math.sin(yawRad) * Math.cos(pitchRad); + double vy = -Math.sin(pitchRad); + double vz = Math.cos(yawRad) * Math.cos(pitchRad); + my += vy * push / 8.0; + + if (def.canMoveOnGround || !isOnSolidGround()) { + if (def.enableBack && reverse > 0.0F) { + mx -= vx * reverse; + mz -= vz * reverse; + } else { + mx += vx * push; + mz += vz * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + } + + if (!terrain.isEmpty()) { + Vec3 slide = terrain.move(mx, mz); + mx += slide.x; + mz += slide.z; + if (!piloted) { + // Nobody is easing the hull in per frame, so take the attitude straight away. + setXRot(terrain.targetPitch); + roll = terrain.targetRoll; + } + } + + double startX = getX(); + double startZ = getZ(); + move(MoverType.SELF, new Vec3(mx, my, mz)); + double stepX = getX() - startX; + double stepZ = getZ() - startZ; + lastStepSq = stepX * stepX + stepZ * stepZ; + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** The original's {@code applyOnGroundPitch} is a no-op for tanks; the terrain decides. */ + @Override + protected void applyOnGroundPitch(float factor) { + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void tickAnimation() { + float target = visibleThrottle(); + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + // Turning on the spot still revs the engine. + if (!input.left && !input.right && !input.throttleDown) { + turnSound *= 0.8F; + } else { + turnSound = Math.min(turnSound + 0.1F, 0.75F); + } + target = Math.max(target, turnSound); + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + TankDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return Math.max(0.5F + visibleThrottle() * 0.5F, 0.5F + turnSound * 0.5F); + } +} From 943432fd35f868d2871247512ab55d5f718f6a15 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:23 +0800 Subject: [PATCH 0251/1461] Add entity/TerrainFollower.java to the 26.2 shared sources --- .../mcheli/entity/TerrainFollower.java | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java new file mode 100644 index 00000000..92e8e59e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java @@ -0,0 +1,225 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * Works out the pitch and roll of a ground vehicle from where its wheels touch the terrain. + * Ported from {@code MCH_WheelManager}. + * + *

The original spawned an invisible entity per wheel and let each one fall onto the ground + * under its mount point. Here every wheel is a one block collision box moved with vanilla's + * collision code, which gives the same result without extra entities to sync. Each left/right + * pair spans a plane with the centre of gravity; the weighted average of their normals is the + * ground normal the hull leans towards. + */ +final class TerrainFollower { + private static final double STEP_HEIGHT = 1.5; + private static final double MAX_DRIFT = 2.0; + private static final double SNAP_DISTANCE = 8.0; + + private final AircraftEntity vehicle; + private Probe[] probes = new Probe[0]; + private Vector3 centre = Vector3.ZERO; + private double wheelBase = 1.0; + float targetPitch; + float targetRoll; + + TerrainFollower(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + /** + * @param wheels positive X wheel positions, front to back; the mirrored wheel is added for each + * @param centre centre of gravity in hull space + */ + void create(List wheels, Vector3 centre) { + this.centre = centre; + probes = new Probe[wheels.size() * 2]; + double minZ = Double.MAX_VALUE; + double maxZ = -Double.MAX_VALUE; + for (int i = 0; i < probes.length; i++) { + Vector3 w = wheels.get(i / 2); + Vector3 local = new Vector3(i % 2 == 0 ? w.x() : -w.x(), w.y(), w.z()); + Probe probe = new Probe(local, local.z() >= centre.z()); + Vec3 at = vehicle.toWorld(local); + probe.setPos(at.x, at.y + 1.0, at.z); + probes[i] = probe; + minZ = Math.min(minZ, local.z()); + maxZ = Math.max(maxZ, local.z()); + } + wheelBase = Math.max(maxZ - minZ, 1.0E-3); + } + + boolean isEmpty() { + return probes.length == 0; + } + + /** + * Drags the wheels along with the hull, lets them settle on the terrain and derives the target + * attitude from them. + * + * @return extra horizontal motion from sliding down steep slopes + */ + Vec3 move(double mx, double mz) { + for (Probe probe : probes) { + Vec3 target = vehicle.toWorld(probe.local); + if (target.distanceToSqr(probe.x, probe.y, probe.z) > SNAP_DISTANCE * SNAP_DISTANCE) { + // Teleported, or created before the vehicle was placed: start over under the hull. + probe.setPos(target.x, target.y + 1.0, target.z); + } + double dx = target.x - probe.x + mx; + double dy = (target.y - probe.y) * 0.15; + double dz = target.z - probe.z + mz; + probe.move(dx, dy, dz); + probe.move(0.0, -0.1, 0.0); + } + + int pairs = probes.length / 2; + // If neither half touches the ground, the outermost pair of that half carries the hull, + // which makes the vehicle tip over a crest instead of floating level. + int rear = pairs - 1; + for (int i = 0; i < pairs; i++) { + if (!probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + rear = -1; + break; + } + } + if (rear >= 0) { + probes[rear * 2].onGround = true; + probes[rear * 2 + 1].onGround = true; + } + int front = 0; + for (int i = pairs - 1; i >= 0; i--) { + if (probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + front = -1; + break; + } + } + if (front >= 0 && pairs > 0) { + probes[front * 2].onGround = true; + probes[front * 2 + 1].onGround = true; + } + + Vec3 wc = vehicle.toWorld(centre).subtract(vehicle.position()); + double cx = vehicle.getX() + wc.x; + double cy = vehicle.getY() + centre.y(); + double cz = vehicle.getZ() + wc.z; + Vec3 normal = Vec3.ZERO; + for (int i = 0; i < pairs; i++) { + Probe w1 = probes[i * 2]; + Probe w2 = probes[i * 2 + 1]; + Vec3 v1 = new Vec3(w1.x - cx, w1.y - cy, w1.z - cz); + Vec3 v2 = new Vec3(w2.x - cx, w2.y - cy, w2.z - cz); + Vec3 n = (w1.local.z() >= 0.0 ? v2.cross(v1) : v1.cross(v2)).normalize(); + double weight = w1.onGround || w2.onGround ? Math.abs(w1.local.z() / wheelBase) : 0.0; + normal = normal.add(n.scale(weight)); + } + normal = normal.normalize(); + + Vec3 slide = Vec3.ZERO; + if (normal.y > 0.01 && normal.y < 0.7) { + slide = new Vec3(normal.x / 50.0, 0.0, normal.z / 50.0); + } + + // Into the hull's frame, then read pitch and roll off the normal. + double yaw = Math.toRadians(vehicle.getYRot()); + double c = Math.cos(yaw); + double s = Math.sin(yaw); + double lx = normal.x * c + normal.z * s; + double lz = normal.z * c - normal.x * s; + float pitch = (float) (90.0 - Math.toDegrees(Math.atan2(normal.y, lz))); + float roll = (float) -(90.0 - Math.toDegrees(Math.atan2(normal.y, lx))); + if (normal.lengthSqr() < 1.0E-6) { + pitch = vehicle.getXRot(); + roll = vehicle.roll; + } + + var def = vehicle.definition(); + float pitchStep = def != null ? def.onGroundPitchFactor : 0.0F; + float rollStep = def != null ? def.onGroundRollFactor : 0.0F; + targetPitch = Mth.clamp(pitch, vehicle.getXRot() - pitchStep, vehicle.getXRot() + pitchStep); + targetRoll = Mth.clamp(roll, vehicle.roll - rollStep, vehicle.roll + rollStep); + + // Wheels that fell behind (stuck on a ledge) are pulled back under the hull. + for (Probe probe : probes) { + Vec3 v = vehicle.position().add(new Vec3(AircraftRotation.toWorld(vehicle.getYRot(), targetPitch, targetRoll, + probe.local.x(), probe.local.y(), probe.local.z()))); + boolean pulled = false; + double x = probe.x; + double z = probe.z; + if (x > v.x + MAX_DRIFT || x < v.x - MAX_DRIFT) { + x = Mth.clamp(x, v.x - MAX_DRIFT, v.x + MAX_DRIFT); + pulled = true; + } + if (z > v.z + MAX_DRIFT || z < v.z - MAX_DRIFT) { + z = Mth.clamp(z, v.z - MAX_DRIFT, v.z + MAX_DRIFT); + pulled = true; + } + if (pulled) { + probe.setPos(x, v.y + STEP_HEIGHT / 2.0, z); + } + } + return slide; + } + + /** One wheel: a one block box that falls onto the terrain and climbs steps up to 1.5 high. */ + private final class Probe { + final Vector3 local; + /** In front of the centre of gravity. */ + final boolean front; + double x; + double y; + double z; + boolean onGround; + + Probe(Vector3 local, boolean front) { + this.local = local; + this.front = front; + } + + void setPos(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + private AABB box() { + return new AABB(x - 0.5, y, z - 0.5, x + 0.5, y + 1.0, z + 0.5); + } + + void move(double dx, double dy, double dz) { + AABB box = box(); + Vec3 wanted = new Vec3(dx, dy, dz); + Vec3 moved = collide(wanted, box); + boolean blockedSideways = moved.x != dx || moved.z != dz; + boolean wasGrounded = onGround || (moved.y != dy && dy < 0.0); + if (blockedSideways && wasGrounded) { + // Same step-up rule as the original wheel entity: climb, move, settle back down. + Vec3 up = collide(new Vec3(0.0, STEP_HEIGHT, 0.0), box); + AABB raised = box.move(up); + Vec3 across = collide(new Vec3(dx, 0.0, dz), raised); + AABB shifted = raised.move(across); + Vec3 down = collide(new Vec3(0.0, -STEP_HEIGHT, 0.0), shifted); + Vec3 stepped = up.add(across).add(down); + if (stepped.horizontalDistanceSqr() > moved.horizontalDistanceSqr()) { + moved = stepped; + } + } + x += moved.x; + y += moved.y; + z += moved.z; + onGround = dy < 0.0 && moved.y != dy; + } + + private Vec3 collide(Vec3 movement, AABB box) { + return Entity.collideBoundingBox(vehicle, movement, box, vehicle.level(), List.of()); + } + } +} From 6936684c2dd39a33e00c8fc70bb5091b0e3f0448 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:23 +0800 Subject: [PATCH 0252/1461] Hold and run the interpolation handler in UavStationEntity on 26.2 --- .../mcheli/entity/UavStationEntity.java | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java new file mode 100644 index 00000000..221c9563 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java @@ -0,0 +1,339 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * The UAV station and the portable UAV controller, {@code MCH_EntityUavStation}. A player sits at + * it and puts a UAV in its slot: the drone appears at the station's launch offset and the player + * flies it from there. The portable controller only launches small UAVs and its lid opens and + * closes with Shift and right-click. + * + *

The original kept the player at the station and had the drone read its pilot from there. + * Here the pilot takes the drone's reserved seat for as long as they fly it, hidden and unhurt, + * so the drone uses the same controls, cameras, HUD and weapons as every other vehicle. Leaving + * the drone, or losing it, puts the pilot back beside the station. + */ +public class UavStationEntity extends Entity { + public static final int STATION = 1; + public static final int PORTABLE = 2; + /** Further out than this, measured flat, the link breaks and the drone is lost, as in the original. */ + public static final double RANGE = 123.0; + /** The launch offset goes up to this far from the station along each axis. */ + public static final int MAX_OFFSET = 50; + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_OPEN = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_OFFSET = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BLOCK_POS); + private static final EntityDataAccessor DATA_LINKED = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private final SimpleContainer slot = new SimpleContainer(1); + private @Nullable UUID lastUav; + /** The lid of the portable controller, 0 shut to 1 open. */ + private float cover; + private float previousCover; + + public UavStationEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, (byte) STATION); + builder.define(DATA_OPEN, true); + builder.define(DATA_OFFSET, new BlockPos(12, 2, 12)); + builder.define(DATA_LINKED, false); + } + + public int kind() { + return entityData.get(DATA_KIND); + } + + public void setKind(int kind) { + entityData.set(DATA_KIND, (byte) (kind == PORTABLE ? PORTABLE : STATION)); + } + + public boolean isOpen() { + return entityData.get(DATA_OPEN); + } + + /** Where a launched drone appears, relative to the station. */ + public BlockPos offset() { + return entityData.get(DATA_OFFSET); + } + + public void setOffset(BlockPos offset) { + entityData.set(DATA_OFFSET, new BlockPos(Mth.clamp(offset.getX(), -MAX_OFFSET, MAX_OFFSET), + Mth.clamp(offset.getY(), -MAX_OFFSET, MAX_OFFSET), Mth.clamp(offset.getZ(), -MAX_OFFSET, MAX_OFFSET))); + } + + /** Whether a drone is flown from here right now; the screen lights up. */ + public boolean isLinked() { + return entityData.get(DATA_LINKED); + } + + public void setLinked(boolean linked) { + entityData.set(DATA_LINKED, linked); + } + + public SimpleContainer slot() { + return slot; + } + + public float cover(float partialTicks) { + return Mth.lerp(partialTicks, previousCover, cover); + } + + /** + * {@code initUavPostion}: the first launch offset is twelve blocks out on the diagonal the + * station faces, two up. + */ + public void initOffset() { + int quarter = (int) (Mth.positiveModulo(getYRot() + 45.0F, 360.0F) / 90.0F) % 4; + int x = quarter == 0 || quarter == 3 ? 12 : -12; + int z = quarter == 0 || quarter == 1 ? 12 : -12; + setOffset(new BlockPos(x, 2, z)); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + previousCover = cover; + cover = Mth.clamp(cover + (isOpen() ? 0.1F : -0.1F), 0.0F, 1.0F); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.03, 0.0); + move(MoverType.SELF, new Vec3(0.0, m.y, 0.0)); + setDeltaMovement(0.0, onGround() ? 0.0 : m.y * 0.96, 0.0); + + Entity user = getFirstPassenger(); + if (user instanceof ServerPlayer player && !slot.getItem(0).isEmpty()) { + launch(player, slot.getItem(0)); + } + } + + /** What the slot holds, if it is a vehicle at all. */ + public static @Nullable AircraftDefinition definitionOf(ItemStack stack) { + String name = AircraftItem.vehicleOf(stack); + return name != null ? MCHeli.content().any(name) : null; + } + + /** Whether this station can put up the vehicle in {@code stack}. */ + public boolean accepts(@Nullable AircraftDefinition def) { + return def != null && def.uav && (kind() != PORTABLE || def.smallUav); + } + + /** {@code handleItem}: puts up the drone in the slot and hands it to {@code player}. */ + private void launch(ServerPlayer player, ItemStack stack) { + AircraftDefinition def = definitionOf(stack); + if (!accepts(def) || !(level() instanceof ServerLevel server)) { + return; + } + if (ServerConfig.isDisabled(def.name())) { + player.sendOverlayMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName())); + return; + } + AircraftEntity uav = ModEntities.typeFor(def).create(server, EntitySpawnReason.TRIGGERED); + if (uav == null) { + return; + } + BlockPos o = offset(); + double y = Math.max(getY() + o.getY(), server.getMinY() + 2.0); + uav.setType(def); + uav.snapTo(getX() + o.getX(), y, getZ() + o.getZ(), getYRot() - 180.0F, 0.0F); + if (!server.noCollision(uav, uav.getBoundingBox().deflate(0.1))) { + player.sendOverlayMessage(Component.translatable("message.mcheli.uav.blocked")); + slot.setItem(0, ItemStack.EMPTY); + Containers.dropItemStack(server, player.getX(), player.getY(), player.getZ(), stack); + return; + } + slot.setItem(0, ItemStack.EMPTY); + uav.setFuelLevel(def.targetDrone ? uav.maxFuel() : (int) (uav.maxFuel() * 0.05F)); + server.addFreshEntity(uav); + if (!def.targetDrone) { + player.closeContainer(); + control(player, uav); + } + } + + /** {@code controlLastAircraft}: takes the last drone flown from here again. */ + public boolean continueControl(ServerPlayer player) { + AircraftEntity uav = lastUav(); + if (uav == null || getFirstPassenger() != player || !uav.canTakeRemotePilot()) { + return false; + } + player.closeContainer(); + return control(player, uav); + } + + public @Nullable AircraftEntity lastUav() { + if (lastUav == null || !(level() instanceof ServerLevel server)) { + return null; + } + Entity e = server.getEntity(lastUav); + return e instanceof AircraftEntity uav && uav.isAlive() && !uav.isDestroyed() && inRange(uav) ? uav : null; + } + + public boolean inRange(Entity uav) { + double dx = uav.getX() - getX(); + double dz = uav.getZ() - getZ(); + return dx * dx + dz * dz <= RANGE * RANGE; + } + + private boolean control(ServerPlayer player, AircraftEntity uav) { + player.stopRiding(); + if (!uav.takeRemotePilot(player, this)) { + return false; + } + lastUav = uav.getUUID(); + setLinked(true); + player.setYRot(uav.getYRot()); + return true; + } + + /** Where a pilot comes back to when the drone is left or lost: the seat in front of the station. */ + public Vec3 returnPosition() { + float yaw = getYRot() * Mth.DEG_TO_RAD; + return new Vec3(getX() - Mth.sin(yaw) * 0.9, getY(), getZ() + Mth.cos(yaw) * 0.9); + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand, Vec3 location) { + if (isVehicle()) { + return InteractionResult.PASS; + } + if (kind() == PORTABLE && player.isSecondaryUseActive()) { + if (!level().isClientSide()) { + entityData.set(DATA_OPEN, !isOpen()); + } + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive() || kind() == PORTABLE && !isOpen()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer server) { + player.setYRot(getYRot() - 180.0F); + if (player.startRiding(this)) { + server.openMenu(new SimpleMenuProvider((id, inventory, p) -> new UavStationMenu(id, inventory, this), + Component.translatable(kind() == PORTABLE ? "item.mcheli.uav_station2" : "item.mcheli.uav_station"))); + } + } + return InteractionResult.SUCCESS; + } + + /** The original sat the player down in front of the console, lower for the portable one. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + double y = kind() == PORTABLE ? -0.6 : 0.35; + return new Vec3(0.0, y, 0.9).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved() || damage <= 0.0F) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + AircraftEntity uav = lastUav(); + if (uav != null) { + uav.releaseRemotePilot(); + } + Containers.dropContents(level, blockPosition(), slot); + if (!creative) { + spawnAtLocation(level, item()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), source.getEntity() instanceof Player ? "hit" : "helidmg", 1.0F, 1.0F); + discard(); + return true; + } + + private Item item() { + return kind() == PORTABLE ? ModItems.UAV_CONTROLLER.get() : ModItems.UAV_STATION.get(); + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith(@Nullable Entity other) { + return true; + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(item()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + setKind(input.getIntOr("Kind", STATION)); + entityData.set(DATA_OPEN, input.getBooleanOr("Open", true)); + setOffset(new BlockPos(input.getIntOr("UavX", 12), input.getIntOr("UavY", 2), input.getIntOr("UavZ", 12))); + lastUav = input.read("LastUav", UUIDUtil.CODEC).orElse(null); + slot.setItem(0, input.read("Item", ItemStack.CODEC).orElse(ItemStack.EMPTY)); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putInt("Kind", kind()); + output.putBoolean("Open", isOpen()); + BlockPos o = offset(); + output.putInt("UavX", o.getX()); + output.putInt("UavY", o.getY()); + output.putInt("UavZ", o.getZ()); + if (lastUav != null) { + output.store("LastUav", UUIDUtil.CODEC, lastUav); + } + if (!slot.getItem(0).isEmpty()) { + output.store("Item", ItemStack.CODEC, slot.getItem(0)); + } + } +} From 004434883e4e39f5dd437ddc9de3e5a69fe3c0c5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:24 +0800 Subject: [PATCH 0253/1461] Add entity/VehicleInventory.java to the 26.2 shared sources --- .../mcheli/entity/VehicleInventory.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java new file mode 100644 index 00000000..1fdc978e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java @@ -0,0 +1,145 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.supply.SupplyRules; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +/** + * What a vehicle carries besides its crew, {@code MCH_AircraftInventory} and the container half of + * {@code W_EntityContainer}: the equipment slots of the vehicle screen (three fuel cans and a + * parachute per ejection seat) and the cargo chest of vehicles with an {@code InventorySize}. + * + *

The cargo chest always has room for the largest size, 54 slots, and the screen shows as many + * rows as the definition asks for. Items in rows a content reload took away are kept rather than + * lost, and still drop when the vehicle is destroyed. + */ +public final class VehicleInventory { + public static final int FUEL_SLOTS = 3; + public static final int PARACHUTE_SLOTS = 2; + public static final int EQUIPMENT_SLOTS = FUEL_SLOTS + PARACHUTE_SLOTS; + public static final int CARGO_SLOTS = 54; + /** How far from the vehicle's centre a player outside may keep a screen open, past its width. */ + private static final double REACH = 12.0; + + private final AircraftEntity vehicle; + private final SimpleContainer equipment = new GuardedContainer(EQUIPMENT_SLOTS); + private final SimpleContainer cargo = new GuardedContainer(CARGO_SLOTS); + + VehicleInventory(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + public SimpleContainer equipment() { + return equipment; + } + + public SimpleContainer cargo() { + return cargo; + } + + /** {@code isUseableByPlayer}: the crew, or someone standing next to the vehicle. */ + public boolean stillValid(Player player) { + if (vehicle.isRemoved() || vehicle.definition() == null) { + return false; + } + if (player.getVehicle() == vehicle) { + return true; + } + double reach = REACH + vehicle.definition().bodyWidth / 2.0; + return player.distanceToSqr(vehicle) <= reach * reach; + } + + // Fuel + + /** + * {@code updateFuel}: every half second, while the vehicle stands on something, each fuel can + * in a fuel slot pours up to 100 units into the tank. Server only. + * + * @return whether any fuel went in + */ + boolean pourFuelCans(int ticks) { + if (ticks % SupplyRules.FUEL_SLOT_INTERVAL != 0 || !vehicle.canSupply()) { + return false; + } + boolean poured = false; + for (int slot = 0; slot < FUEL_SLOTS; slot++) { + ItemStack can = equipment.getItem(slot); + if (!(can.getItem() instanceof FuelItem)) { + continue; + } + int amount = SupplyRules.fuelFromCan(vehicle.maxFuel() - vehicle.fuel(), FuelItem.contents(can)); + if (amount > 0) { + vehicle.refuel(amount); + can.setDamageValue(can.getDamageValue() + amount); + poured = true; + } + } + if (poured) { + equipment.setChanged(); + } + return poured; + } + + // Parachutes + + /** Parachute slots the vehicle screen offers: one per ejection seat. */ + public static int parachuteSlots(AircraftDefinition def) { + return Math.min(def.ejectionSeats(), PARACHUTE_SLOTS); + } + + /** + * {@code consumeParachute}: takes one parachute out of the parachute slots for a crew member + * who bails out. + * + * @return false when there is none left + */ + boolean takeParachute() { + for (int slot = FUEL_SLOTS; slot < EQUIPMENT_SLOTS; slot++) { + if (equipment.getItem(slot).getItem() instanceof ParachuteItem) { + equipment.removeItem(slot, 1); + return true; + } + } + return false; + } + + // Saving and dropping + + void save(ValueOutput output) { + ContainerHelper.saveAllItems(output.child("Equipment"), equipment.getItems(), false); + ContainerHelper.saveAllItems(output.child("Cargo"), cargo.getItems(), false); + } + + void load(ValueInput input) { + equipment.clearContent(); + cargo.clearContent(); + input.child("Equipment").ifPresent(in -> ContainerHelper.loadAllItems(in, equipment.getItems())); + input.child("Cargo").ifPresent(in -> ContainerHelper.loadAllItems(in, cargo.getItems())); + } + + /** {@code setDead}: everything aboard falls out where the vehicle was. Server only. */ + void dropAll() { + Containers.dropContents(vehicle.level(), vehicle, equipment); + Containers.dropContents(vehicle.level(), vehicle, cargo); + } + + /** A container that only stays open for players {@link #stillValid(Player)} allows. */ + private final class GuardedContainer extends SimpleContainer { + GuardedContainer(int size) { + super(size); + } + + @Override + public boolean stillValid(Player player) { + return VehicleInventory.this.stillValid(player); + } + } +} From b7a2aaa1940623444a71e33f7681cc818a9a2675 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:24 +0800 Subject: [PATCH 0254/1461] Add entity/VehicleParticles.java to the 26.2 shared sources --- .../mcheli/entity/VehicleParticles.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java new file mode 100644 index 00000000..46ed88a8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java @@ -0,0 +1,180 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.ParticleSplash; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; + +/** + * The spray, dust and exhaust a vehicle throws up, all client side: + *

    + *
  • {@code onUpdate_ParticleSandCloud}: a helicopter's downwash raises a ring of dust off the + * ground or water below it; boats and amphibians with {@code EnableSeaSurfaceParticle} raise + * one off the sea while under way.
  • + *
  • {@code onUpdate_ParticleSplash}: each {@code AddParticleSplash} point sprays while it moves + * through water.
  • + *
  • {@code onUpdate_ParticleNozzle}: jet nozzles smoke with the throttle, more when the blast + * hits the ground, as on a Harrier in the hover.
  • + *
  • {@code onUpdate_ParticleLandingGear}: a plane rolling fast kicks up bits of the ground.
  • + *
+ */ +final class VehicleParticles { + private static final int WHITE = 0xFFFFFF; + + private VehicleParticles() { + } + + static void tick(AircraftEntity vehicle, AircraftDefinition def) { + if (vehicle.isDestroyed()) { + return; + } + switch (vehicle) { + case HelicopterEntity heli -> sandCloud(heli, def, false); + case PlaneEntity plane -> { + if (def instanceof PlaneDefinition planeDef) { + nozzles(plane, planeDef); + } + landingGear(plane); + splashes(plane, def); + sandCloud(plane, def, true); + } + case TankEntity tank -> { + splashes(tank, def); + sandCloud(tank, def, true); + } + default -> { + } + } + } + + private static void sandCloud(AircraftEntity vehicle, AircraftDefinition def, boolean seaOnly) { + if (seaOnly && !def.seaSurfaceParticles || def.particlesScale <= 0.01F) { + return; + } + Level level = vehicle.level(); + RandomSource random = vehicle.getRandom(); + float scale = def.particlesScale * 3.0F * (seaOnly ? 2.0F : 1.0F); + double throttle = Math.min(vehicle.visibleThrottle() * 2.0, 1.0); + // A boat needs some water under it before it sprays; the original counted water blocks. + int count = seaOnly ? (int) (scale * 7.0F) : 0; + int range = (int) (scale * 10.0F) + 1; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + int bx = Mth.floor(vehicle.getX() + 0.5); + int by = Mth.floor(vehicle.getY() + 0.5); + int bz = Mth.floor(vehicle.getZ() + 0.5); + int y = 0; + double groundY = 0.0; + boolean found = false; + for (; y < range && !found; y++) { + for (int x = -1; x <= 1 && !found; x++) { + for (int z = -1; z <= 1 && !found; z++) { + BlockState state = level.getBlockState(pos.set(bx + x, by - y, bz + z)); + if (state.isAir()) { + continue; + } + if (seaOnly && state.getFluidState().is(FluidTags.WATER)) { + count--; + } + if (count <= 0) { + groundY = vehicle.getY() + 1.0 + scale / 5.0F - y; + found = true; + } + } + } + } + if (!found) { + return; + } + double density = (range - y + 1) / (5.0 * scale) / 2.0; + for (int k = 0; k < (int) (throttle * 6.0 * density); k++) { + float angle = random.nextFloat() * Mth.TWO_PI; + double dx = Mth.cos(angle); + double dz = Mth.sin(angle); + level.addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, scale * 5.0F, (int) (10.0F * scale) + 1), true, + vehicle.getX() + dx * scale * 3.0, groundY + (random.nextDouble() - 0.5) * scale, vehicle.getZ() + dz * scale * 3.0, + scale * dx * 0.3, scale * -0.02, scale * dz * 0.3); + } + } + + private static void splashes(AircraftEntity vehicle, AircraftDefinition def) { + if (def.splashes.isEmpty()) { + return; + } + double mx = vehicle.getX() - vehicle.xo; + double mz = vehicle.getZ() - vehicle.zo; + double dist = Math.min(mx * mx + mz * mz, 1.0); + RandomSource random = vehicle.getRandom(); + for (ParticleSplash splash : def.splashes) { + for (int i = 0; i < splash.count(); i++) { + if (dist > 0.03 + random.nextFloat() * 0.1) { + splash(vehicle, splash, -mx * splash.acceleration(), -mz * splash.acceleration(), splash.size() * (0.5 + dist * 0.5)); + } + } + } + } + + private static void splash(AircraftEntity vehicle, ParticleSplash splash, double mx, double mz, double size) { + RandomSource random = vehicle.getRandom(); + Vec3 at = vehicle.toWorld(splash.position()).add(random.nextDouble() - 0.5, (random.nextDouble() - 0.5) * 0.5, random.nextDouble() - 0.5); + BlockPos block = BlockPos.containing(at.x + 0.5, at.y, at.z + 0.5); + if (!vehicle.level().getFluidState(block).is(FluidTags.WATER)) { + return; + } + int age = splash.age() + (int) (random.nextFloat() * 0.5F * splash.age()); + vehicle.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, (float) size * (random.nextFloat() * 0.2F + 0.8F), age), true, + at.x, at.y, at.z, mx + (random.nextFloat() - 0.5) * 0.7, splash.motionY(), mz + (random.nextFloat() - 0.5) * 0.7); + } + + private static void nozzles(PlaneEntity plane, PlaneDefinition def) { + float throttle = plane.visibleThrottle(); + if (def.nozzles.isEmpty() || throttle <= 0.1F) { + return; + } + RandomSource random = plane.getRandom(); + PartState nozzle = plane.nozzle(); + float tilt = nozzle != null ? nozzle.angle() * Mth.DEG_TO_RAD : 0.0F; + // Out of the back, turned down as the nozzles swing towards the hover. + Vector3f blast = plane.toWorldOffset(new Vector3(0.0, -Mth.sin(tilt), -Mth.cos(tilt))); + for (MovingPart part : def.nozzles) { + if (random.nextFloat() > throttle * 1.5F) { + continue; + } + Vec3 at = plane.toWorld(part.position()).add(blast.x, blast.y, blast.z); + BlockPos ahead = BlockPos.containing(at.x + blast.x * 3.0, at.y + blast.y * 3.0, at.z + blast.z * 3.0); + float spread = plane.level().getBlockState(ahead).isAir() ? 0.7F : 2.0F; + plane.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, 5.0F * def.particlesScale, 30), true, at.x, at.y, at.z, + blast.x + (random.nextFloat() - 0.5F) * spread, blast.y, blast.z + (random.nextFloat() - 0.5F) * spread); + } + } + + private static void landingGear(PlaneEntity plane) { + double dx = plane.getX() - plane.xo; + double dz = plane.getZ() - plane.zo; + if (!plane.onGround() || dx * dx + dz * dz <= 0.01) { + return; + } + BlockPos below = BlockPos.containing(plane.getX(), plane.getY() - 0.5, plane.getZ()); + BlockState state = plane.level().getBlockState(below); + if (state.getRenderShape() == RenderShape.INVISIBLE) { + return; + } + RandomSource random = plane.getRandom(); + float width = plane.getBbWidth(); + plane.level().addParticle(new BlockParticleOption(ParticleTypes.BLOCK, state), + plane.getX() + (random.nextFloat() - 0.5) * width, plane.getBoundingBox().minY + 0.1, plane.getZ() + (random.nextFloat() - 0.5) * width, + -dx * 4.0, 1.5, -dz * 4.0); + } +} From 977a06a7a35fee28672a3f6275c557dfa9bc62bd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:25 +0800 Subject: [PATCH 0255/1461] Add entity/projectile/A10Entity.java to the 26.2 shared sources --- .../mcheli/entity/projectile/A10Entity.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java new file mode 100644 index 00000000..e72e952e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java @@ -0,0 +1,160 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import org.jspecify.annotations.Nullable; + +/** + * The A-10 that answers a close air support call, {@code MCH_EntityA10}. It dives in for a second, + * strafes the target with forty GAU-8 rounds and climbs away. + */ +public class A10Entity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_DIRECTION = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.INT); + private static final int LIFETIME = 70; + + private int despawnCount; + private int shotCount; + public int power = 32; + public int explosionPower = 1; + public float acceleration = 4.0F; + private @Nullable Entity shooter; + private @Nullable Entity user; + + public A10Entity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_DIRECTION, 0); + } + + /** + * @param direction 0 heading south (+Z), then clockwise in steps of 90 degrees + */ + public void setup(String weapon, int direction, double mx, double mz, @Nullable Entity shooter, @Nullable Entity user) { + entityData.set(DATA_WEAPON, weapon); + entityData.set(DATA_DIRECTION, direction); + setYRot(90 * direction); + yRotO = getYRot(); + setDeltaMovement(mx, 0.0, mz); + this.shooter = shooter; + this.user = user; + } + + public int direction() { + return entityData.get(DATA_DIRECTION); + } + + /** Hidden during the first second, while it is still out of sight. */ + public boolean isVisible() { + return despawnCount > 20; + } + + @Override + public void tick() { + super.tick(); + despawnCount++; + if (!level().isClientSide()) { + if (despawnCount > LIFETIME) { + discard(); + return; + } + if (shotCount < 40) { + for (int i = 0; i < 2; i++) { + fireGau8(shotCount); + shotCount++; + } + if (shotCount == 38) { + ModSounds.playAt(this, "gau-8_snd", 150.0F, 1.0F); + } + } + } + if (despawnCount <= 20) { + setDeltaMovement(getDeltaMovement().x, -0.3, getDeltaMovement().z); + } else { + setPos(getX() + getDeltaMovement().x, getY() + getDeltaMovement().y, getZ() + getDeltaMovement().z); + setDeltaMovement(getDeltaMovement().add(0.0, 0.02, 0.0)); + } + } + + private void fireGau8(int count) { + int dir = direction(); + double x = getX(); + double z = getZ(); + double tx = random.nextDouble() - 0.5; + double ty = -2.6; + double tz = random.nextDouble() - 0.5; + switch (dir) { + case 0 -> { + tz += 10.0; + z += count * 0.6; + } + case 1 -> { + tx -= 10.0; + x -= count * 0.6; + } + case 2 -> { + tz -= 10.0; + z -= count * 0.6; + } + default -> { + tx += 10.0; + x += count * 0.6; + } + } + BulletEntity round = ModEntities.BULLET.get().create(level(), EntitySpawnReason.TRIGGERED); + if (round == null) { + return; + } + round.launch(x, getY(), z, tx, ty, tz, 90 * dir, 30.0F, acceleration); + round.setWeapon(entityData.get(DATA_WEAPON)); + round.explosionPower = shotCount % 4 == 0 ? explosionPower : 0; + round.power = power; + round.setOwners(shooter, user); + level().addFreshEntity(round); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 512.0 * 512.0; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From 86e0497336549d0345577393cd9f454a1c3df52e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:25 +0800 Subject: [PATCH 0256/1461] Add entity/projectile/AAMissileEntity.java to the 26.2 shared sources --- .../entity/projectile/AAMissileEntity.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java new file mode 100644 index 00000000..a9d41520 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** An air to air missile homing on its locked target, {@code MCH_EntityAAMissile}. */ +public class AAMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 3422500.0; + + public AAMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (age() > 4 && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize * 0.5F); + } + if (level().isClientSide() || w == null || isRemoved()) { + return; + } + Entity t = target(); + if (user() == null || t == null || t.isRemoved()) { + discard(); + return; + } + double d = distanceToSqr(t); + if (d > MAX_DISTANCE_SQ) { + discard(); + } else if (age() > w.rigidityTime) { + // The original compared the squared distance with the plain fuse distance. + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + setPos((t.getX() + getX()) / 2.0, (t.getY() + getY()) / 2.0, (t.getZ() + getZ()) / 2.0); + impact(new Impact(position(), t, null, Direction.UP), 1.0F); + } else { + guideTo(t.getX(), t.getY(), t.getZ()); + } + } + } + + @Override + public String defaultModel() { + return "aamissile"; + } +} From 8e32ca3c671491abb9d7ab1ab1eda6a86ba2f25a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:26 +0800 Subject: [PATCH 0257/1461] Add entity/projectile/ASMissileEntity.java to the 26.2 shared sources --- .../entity/projectile/ASMissileEntity.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java new file mode 100644 index 00000000..e5f46c96 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java @@ -0,0 +1,91 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * An air to surface missile flying to the block the gunner aimed at, {@code MCH_EntityASMissile}. + * Powered ones climb for half a second first; gliding bombs only steer sideways. + */ +public class ASMissileEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + + public ASMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public float gravity() { + return isBomblet() ? -0.03F : super.gravity(); + } + + @Override + public float gravityInWater() { + return isBomblet() ? -0.03F : super.gravityInWater(); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && !isBomblet()) { + trail(w.trajectoryParticle, 3, 10.0F * w.smokeSize * 0.5F); + } + if (w != null && !level().isClientSide() && !isBomblet() && !isRemoved()) { + BlockPos aim = BlockPos.containing(targetX, targetY, targetZ); + if (!level().getBlockState(aim).isAir()) { + double dist = Math.sqrt(distanceToSqr(targetX, targetY, targetZ)); + if (dist < w.proximityFuseDist) { + if (w.bomblet > 0) { + for (int i = 0; i < w.bomblet; i++) { + sprinkleBomblet(); + } + } else { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + } + discard(); + } else if (gravity() == 0.0F) { + double up = age() < 10 ? 20.0 : 0.0; + aimAlong(targetX - getX(), targetY + up - getY(), targetZ - getZ()); + } else { + double dx = targetX - getX(); + double dy = (targetY - getY()) * 0.3; + double dz = targetZ - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + Vec3 m = getDeltaMovement(); + setDeltaMovement(dx * acceleration / d, m.y, dz * acceleration / d); + } + } + } + faceMotion(); + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + ASMissileEntity child = spawnChild(ModEntities.AS_MISSILE.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x * 0.5 + (random.nextFloat() - 0.5F) * spread, + m.y * 0.5 / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z * 0.5 + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From ecf3b82524b675533f7257f184cab88d1aeb1b12 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:26 +0800 Subject: [PATCH 0258/1461] Add entity/projectile/ATMissileEntity.java to the 26.2 shared sources --- .../entity/projectile/ATMissileEntity.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java new file mode 100644 index 00000000..d7031ecf --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java @@ -0,0 +1,84 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** + * An anti tank missile, {@code MCH_EntityATMissile}. In the default mode it flies straight at the + * locked target; in top attack mode it climbs 150 blocks above the shooter and dives. + */ +public class ATMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 2250000.0; + + /** 0 direct, 1 top attack. */ + public int guidanceType; + + public ATMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && tickCount >= w.trajectoryParticleStartTick) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + if (!level().isClientSide() && !isRemoved()) { + Entity t = target(); + if (user() != null && t != null && !t.isRemoved() && w != null) { + steer(w, t); + } else { + discard(); + } + } + faceMotion(); + } + + private void steer(WeaponDefinition w, Entity t) { + double x = t.getX() - getX(); + double y = t.getY() - getY(); + double z = t.getZ() - getZ(); + double d = x * x + y * y + z * z; + if (d > MAX_DISTANCE_SQ) { + discard(); + return; + } + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + return; + } + int rigidity = w.rigidityTime; + if (age() <= rigidity) { + return; + } + float af = age() < rigidity + w.trajectoryParticleStartTick ? 0.5F : 1.0F; + if (guidanceType == 1) { + Entity shooter = user(); + if (age() <= rigidity + 20) { + guideTo(t.getX(), shooter.getY() + 150.0, t.getZ()); + } else if (age() <= rigidity + 30) { + guideTo(t.getX(), shooter.getY(), t.getZ()); + } else { + if (age() == rigidity + 35) { + power = (int) (power * 1.2F); + if (explosionPower > 0) { + explosionPower++; + } + } + guideTo(t.getX(), t.getY(), t.getZ()); + } + } else { + d = Math.sqrt(d); + setDeltaMovement(x * acceleration / d * af, y * acceleration / d * af, z * acceleration / d * af); + } + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From eea5f1bafe7580ab21d6d2fd5fd5549df7fc2cd8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:27 +0800 Subject: [PATCH 0259/1461] Add entity/projectile/BombEntity.java to the 26.2 shared sources --- .../mcheli/entity/projectile/BombEntity.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java new file mode 100644 index 00000000..c758d75b --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** A free fall bomb, {@code MCH_EntityBomb}. Cluster bombs split into bomblets. */ +public class BombEntity extends ProjectileEntity { + public BombEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (!level().isClientSide() && w != null && !isRemoved()) { + Vec3 m = getDeltaMovement(); + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + float fuse = w.proximityFuseDist; + if (fuse > 0.1F && age() % 10 == 0) { + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(fuse), this::canHit)) { + if (entity instanceof LivingEntity) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + BombEntity child = spawnChild(ModEntities.BOMB.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + /** A cluster bomb that has not split yet is not drawn when it splits right away. */ + public boolean isVisible() { + WeaponDefinition w = weapon(); + return w == null || isBomblet() || w.bomblet <= 0 || w.bombletSTime > 0; + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From 2ba4b7fd3a2610fed03716026b6d8ff847935fe6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:27 +0800 Subject: [PATCH 0260/1461] Add entity/projectile/BulletEntity.java to the 26.2 shared sources --- .../entity/projectile/BulletEntity.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java new file mode 100644 index 00000000..b2b418ac --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** A gun round, {@code MCH_EntityBullet}. High explosive rounds may carry a proximity fuse. */ +public class BulletEntity extends ProjectileEntity { + public BulletEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return true; + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (isRemoved() || level().isClientSide() || age() <= 1 || w == null || explosionPower <= 0) { + return; + } + float fuse = w.proximityFuseDist; + if (fuse > 0.1F) { + fuse++; + float range = fuse + Math.abs(w.acceleration); + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(range), this::canHit)) { + if (entity.distanceToSqr(this) < fuse * fuse) { + setPos((entity.getX() + getX()) / 2.0, (entity.getY() + getY()) / 2.0, (entity.getZ() + getZ()) / 2.0); + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + + @Override + public String defaultModel() { + return "bullet"; + } +} From 348299fe0ed058c98685fbe82587049f65b478a6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:28 +0800 Subject: [PATCH 0261/1461] Add entity/projectile/DispensedItemEntity.java to the 26.2 shared sources --- .../projectile/DispensedItemEntity.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java new file mode 100644 index 00000000..69a2f674 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java @@ -0,0 +1,121 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseFireBlock; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A canister that uses an item on every block around where it lands, {@code MCH_EntityDispensedItem}: + * water bombers put out fires, crop dusters spread bone meal or seeds, and so on. + */ +public class DispensedItemEntity extends ProjectileEntity { + public DispensedItemEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize); + } + if (!level().isClientSide() && w != null) { + Vec3 m = getDeltaMovement(); + if (acceleration < 1.0E-4) { + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + } + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + } + tickBomblets(); + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + return; + } + WeaponDefinition w = weapon(); + Item item = w != null ? LegacyItems.item(w.dispenseItem, w.dispenseDamage) : null; + if (w != null && item != null && user() instanceof ServerPlayer player && !player.isRemoved()) { + BlockPos center = hit.block() != null ? hit.block() : blockPosition(); + int range = w.dispenseRange - 1; + for (int x = -range; x <= range; x++) { + for (int y = -range; y <= range; y++) { + for (int z = -range; z <= range; z++) { + int dist = x * x + y * y + z * z; + if (dist <= range * range && (dist <= 0.5 * range * range || random.nextInt(2) == 0)) { + useOn(server, center.offset(x, y, z), item); + } + } + } + } + } + discard(); + } + + private void useOn(ServerLevel server, BlockPos pos, Item item) { + if (pos.getY() < server.getMinY() || pos.getY() >= server.getMaxY()) { + return; + } + BlockState state = server.getBlockState(pos); + if (state.isAir()) { + return; + } + if (item == Items.WATER_BUCKET) { + // A water drop douses fire and quenches lava instead of placing water. + FluidState fluid = server.getFluidState(pos); + if (state.getBlock() instanceof BaseFireBlock) { + server.removeBlock(pos, false); + } else if (fluid.is(FluidTags.LAVA)) { + server.setBlockAndUpdate(pos, fluid.isSource() ? Blocks.OBSIDIAN.defaultBlockState() : Blocks.COBBLESTONE.defaultBlockState()); + } + return; + } + // Used from above with nobody holding it, the way a dispenser block would. + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(pos).add(0.0, 0.5, 0.0), Direction.UP, pos, false); + item.useOn(new UseOnContext(server, null, InteractionHand.MAIN_HAND, new ItemStack(item), hit)); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + DispensedItemEntity child = spawnChild(ModEntities.DISPENSED_ITEM.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From 8310449b8286ae88f5a6475348f47b95399107e2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:28 +0800 Subject: [PATCH 0262/1461] Add entity/projectile/FlareEntity.java to the 26.2 shared sources --- .../mcheli/entity/projectile/FlareEntity.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java new file mode 100644 index 00000000..a564a1fc --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java @@ -0,0 +1,161 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A burning flare, or with a fuse a smoke grenade that bursts into a cloud. Port of + * {@code MCH_EntityFlare}. Flares only look the part: decoying happens in the dispenser. + */ +public class FlareEntity extends Entity { + private static final EntityDataAccessor DATA_FUSE = SynchedEntityData.defineId(FlareEntity.class, EntityDataSerializers.INT); + + private double gravity = -0.013; + private double airResistance = 0.992; + private float size = 6.0F; + + public FlareEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_FUSE, 0); + } + + /** + * @param fuse ticks until a smoke grenade bursts, 0 for a flare + * @param slow the long hanging type 4 flare + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float size, int fuse, boolean slow) { + snapTo(x, y, z, random.nextFloat() * 360.0F, random.nextFloat() * 360.0F); + setDeltaMovement(mx, my, mz); + this.size = size; + entityData.set(DATA_FUSE, fuse); + if (slow) { + gravity *= 0.6; + airResistance = 0.995; + } + } + + private int fuse() { + return entityData.get(DATA_FUSE); + } + + /** A smoke grenade rather than a burning flare. */ + public boolean isSmoke() { + return fuse() > 0; + } + + @Override + public void tick() { + if (fuse() > 0 && tickCount >= fuse()) { + // Removal bursts the grenade on the client. + discard(); + return; + } + boolean server = !level().isClientSide(); + if (server && (!level().isLoaded(blockPosition()) || tickCount > 300)) { + discard(); + return; + } + super.tick(); + Vec3 m = getDeltaMovement(); + if (server) { + HitResult hit = level().clip(new ClipContext(position(), position().add(m), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.MISS) { + discard(); + return; + } + } else { + trail(); + } + setPos(getX() + m.x, getY() + m.y, getZ() + m.z); + setDeltaMovement(m.x * airResistance, m.y + gravity, m.z * airResistance); + if (server && (isInWater() || onGround())) { + discard(); + } + } + + private void trail() { + double x = (getX() - xo) / 2.0; + double y = (getY() - yo) / 2.0; + double z = (getZ() - zo) / 2.0; + for (int i = 0; i < 2; i++) { + if (size < 5.0F && random.nextInt(2) == 0) { + continue; + } + level().addAlwaysVisibleParticle(fuse() > 0 ? ParticleTypes.CLOUD : ParticleTypes.LARGE_SMOKE, true, + xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + if (fuse() == 0) { + level().addAlwaysVisibleParticle(ParticleTypes.FLAME, true, xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + } + } + } + + /** A smoke grenade bursting into a cloud that hangs around for ten seconds or more. */ + private void burst() { + if (!level().isClientSide()) { + return; + } + for (int i = 0; i < 20; i++) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + getX() + (random.nextDouble() - 0.5) * 10.0, + getY() + (random.nextDouble() - 0.5) * 10.0, + getZ() + (random.nextDouble() - 0.5) * 10.0, + (random.nextDouble() - 0.5) * 0.045, 0.005, (random.nextDouble() - 0.5) * 0.045); + } + } + + @Override + public void remove(RemovalReason reason) { + if (level().isClientSide() && fuse() > 0) { + burst(); + entityData.set(DATA_FUSE, 0); + } + super.remove(reason); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isInvulnerable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From f7491ee59873455b88963f46f29a77e31cb06ef5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:29 +0800 Subject: [PATCH 0263/1461] Add entity/projectile/MarkerRocketEntity.java to the 26.2 shared sources --- .../entity/projectile/MarkerRocketEntity.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java new file mode 100644 index 00000000..eb723310 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java @@ -0,0 +1,139 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; + +/** + * A marking rocket, {@code MCH_EntityMarkerRocket}. Where it lands under open sky it pops red + * smoke, and five seconds later a stick of bombs falls on the mark. + */ +public class MarkerRocketEntity extends ProjectileEntity { + private static final EntityDataAccessor DATA_STATUS = SynchedEntityData.defineId(MarkerRocketEntity.class, EntityDataSerializers.BYTE); + + /** 0 inert, 1 flying, 2 marking. */ + private int countDown; + + public MarkerRocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_STATUS, (byte) 0); + } + + public void setStatus(int status) { + if (!level().isClientSide()) { + entityData.set(DATA_STATUS, (byte) status); + } + } + + public int status() { + return entityData.get(DATA_STATUS); + } + + @Override + public void tick() { + int status = status(); + if (level().isClientSide()) { + if (weapon() == null) { + super.tick(); + } + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && status != 0) { + if (status == 1) { + super.tick(); + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } else { + float gb = random.nextFloat() * 0.3F; + int rgb = (int) ((random.nextFloat() * 0.2F + 0.8F) * 255) << 16 | (int) (gb * 255) << 8 | (int) (gb * 255); + for (int i = 0; i < 5; i++) { + level().addAlwaysVisibleParticle(new DustParticleOptions(rgb, 4.0F), true, getX(), getY(), getZ(), + (random.nextFloat() - 0.5F) * 0.7F, 0.3F + random.nextFloat() * 0.3F, (random.nextFloat() - 0.5F) * 0.7F); + } + } + } + } else if (status == 0 || isInWater()) { + discard(); + } else if (status == 1) { + super.tick(); + } else if (countDown > 0) { + countDown--; + if (countDown == 40) { + dropBombs(); + } + } else { + discard(); + } + } + + private void dropBombs() { + int count = 6 + random.nextInt(2); + for (int i = 0; i < count; i++) { + BombEntity bomb = ModEntities.BOMB.get().create(level(), EntitySpawnReason.TRIGGERED); + if (bomb == null) { + continue; + } + bomb.launch(getX() + (random.nextFloat() - 0.5F) * 15.0F, 260.0F + random.nextFloat() * 10.0F + i * 30, + getZ() + (random.nextFloat() - 0.5F) * 15.0F, 0.0, -0.5, 0.0, 0.0F, 90.0F, 4.0); + bomb.setWeapon(weaponName()); + bomb.explosionPower = 3 + random.nextInt(2); + bomb.explosionPowerInWater = 0; + bomb.power = 30; + bomb.piercing = 0; + bomb.setOwners(shooter, user); + level().addFreshEntity(bomb); + } + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (level().isClientSide() || hit.entity() != null || hit.block() == null) { + return; + } + BlockPos mark = hit.block().relative(hit.side()); + if (!level().getBlockState(mark).isAir()) { + discard(); + return; + } + level().setBlockAndUpdate(mark, Blocks.FIRE.defaultBlockState()); + int solid = 0; + for (int y = mark.getY() + 1; y < level().getMaxY(); y++) { + if (!level().getBlockState(new BlockPos(mark.getX(), y, mark.getZ())).isAir() && ++solid >= 5) { + break; + } + } + if (solid < 5) { + setStatus(2); + setPos(mark.getX() + 0.5, mark.getY() + 1.1, mark.getZ() + 0.5); + xo = getX(); + yo = getY(); + zo = getZ(); + countDown = 100; + } else { + discard(); + } + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From 0d05d03e42f7e1e72cefe7076c4be6dd70233a78 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:29 +0800 Subject: [PATCH 0264/1461] Add entity/projectile/ProjectileEntity.java to the 26.2 shared sources --- .../entity/projectile/ProjectileEntity.java | 790 ++++++++++++++++++ 1 file changed, 790 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java new file mode 100644 index 00000000..30f171c5 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java @@ -0,0 +1,790 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Combat; +import com.jnhprojects.mcheli.weapon.Explosions; +import com.jnhprojects.mcheli.weapon.ModDamageTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Everything a weapon fires: rounds, rockets, missiles, bombs and torpedoes. A port of + * {@code MCH_EntityBaseBullet}. + * + *

Projectiles fly on both sides, the client only for looks; hits, fuses and guidance are decided + * on the server. They are never saved: like the original, one found in a loaded chunk vanishes. + */ +public abstract class ProjectileEntity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TARGET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BOMBLET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.BOOLEAN); + /** Who fired, for clients: their rounds must not stop on their own vehicle. */ + private static final EntityDataAccessor DATA_SHOOTER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_USER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + + /** Beyond 5820 blocks from whoever fired it, a projectile is dropped. */ + private static final double MAX_RANGE_SQ = 3.38724E7; + + protected @Nullable Entity shooter; + protected @Nullable Entity user; + protected @Nullable Entity target; + private @Nullable WeaponDefinition weapon; + + public int explosionPower; + public int explosionPowerInWater; + public int power = 1; + public int piercing; + public int delayFuse; + public int sprinkleTime; + public double acceleration = 1.0; + public double accelerationFactor = 1.0; + private int age; + /** Client only: the round hit something here and waits, hidden, for the server to remove it. */ + private boolean spent; + + public Vec3 prevMotion = Vec3.ZERO; + /** Position two ticks ago, used to spread trail particles over the distance covered. */ + protected double xo2; + protected double yo2; + protected double zo2; + + protected ProjectileEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_TARGET, 0); + builder.define(DATA_BOMBLET, false); + builder.define(DATA_SHOOTER, 0); + builder.define(DATA_USER, 0); + } + + /** + * The second constructor of the original: place the projectile and point it along + * {@code (mx, my, mz)} at {@code acceleration} blocks per tick, capped at 3.9. + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float yaw, float pitch, double acceleration) { + snapTo(x, y, z, yaw, pitch); + xo2 = x; + yo2 = y; + zo2 = z; + double a = Math.min(acceleration, 3.9); + double d = Math.sqrt(mx * mx + my * my + mz * mz); + Vec3 motion = d > 0.0 ? new Vec3(mx * a / d, my * a / d, mz * a / d) : Vec3.ZERO; + setDeltaMovement(motion); + prevMotion = motion; + this.acceleration = a; + } + + // Weapon + + public void setWeapon(String name) { + WeaponDefinition def = MCHeli.content().weapon(name); + if (def == null) { + return; + } + weapon = def; + if (!level().isClientSide()) { + entityData.set(DATA_WEAPON, name); + } + if (def.bomblet > 0) { + sprinkleTime = def.bombletSTime; + } + piercing = def.piercing; + if (boostsAcceleration() && def.acceleration > 4.0F) { + accelerationFactor = def.acceleration / 4.0F; + } + } + + /** Rounds and rockets faster than 4 blocks a tick move several steps per tick instead. */ + protected boolean boostsAcceleration() { + return false; + } + + public @Nullable WeaponDefinition weapon() { + return weapon; + } + + /** Model drawn when the weapon names none, one of the original's {@code MCH_DefaultBulletModels}. */ + public abstract String defaultModel(); + + /** The model to draw: {@code ModelBomblet} for bomblets, else {@code ModelBullet}, else the default. */ + public String model() { + if (weapon == null) { + return defaultModel(); + } + String name = isBomblet() ? weapon.bombletModel : weapon.bulletModel; + return name.isEmpty() ? defaultModel() : name; + } + + public String weaponName() { + return entityData.get(DATA_WEAPON); + } + + /** {@code setParameterFromWeapon}: power and charges from the weapon file, and who to credit. */ + public void setParameters(WeaponDefinition def, @Nullable Entity shooter, @Nullable Entity user) { + explosionPower = def.explosion; + explosionPowerInWater = def.explosionInWater; + power = def.power; + piercing = def.piercing; + setOwners(shooter, user); + } + + /** The same, inherited from a parent projectile, for bomblets. */ + public void setParameters(ProjectileEntity parent) { + explosionPower = parent.explosionPower; + explosionPowerInWater = parent.explosionPowerInWater; + power = parent.power; + piercing = parent.piercing; + setOwners(parent.shooter, parent.user); + } + + public void setOwners(@Nullable Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + entityData.set(DATA_SHOOTER, shooter != null ? shooter.getId() : 0); + entityData.set(DATA_USER, user != null ? user.getId() : 0); + } + + public @Nullable Entity user() { + return user; + } + + public @Nullable Entity shooter() { + return shooter; + } + + public boolean isBomblet() { + return entityData.get(DATA_BOMBLET); + } + + public void setBomblet() { + sprinkleTime = 0; + entityData.set(DATA_BOMBLET, true); + } + + public void setTarget(@Nullable Entity entity) { + target = entity; + if (!level().isClientSide()) { + entityData.set(DATA_TARGET, entity != null ? entity.getId() : 0); + } + } + + public @Nullable Entity target() { + return target; + } + + /** A flare seduced this missile: it loses its target and is gone. */ + public void decoy() { + target = null; + discard(); + } + + public int age() { + return age; + } + + public float gravity() { + return weapon != null ? weapon.gravity : 0.0F; + } + + public float gravityInWater() { + return weapon != null ? weapon.gravityInWater : 0.0F; + } + + // Tick + + @Override + public void tick() { + if (spent) { + return; + } + if (level().isClientSide() && age == 0) { + int targetId = entityData.get(DATA_TARGET); + if (targetId > 0) { + target = level().getEntity(targetId); + } + } + Vec3 motion = getDeltaMovement(); + if (!motion.equals(prevMotion)) { + faceMotion(); + } + prevMotion = motion; + age++; + xo2 = xo; + yo2 = yo; + zo2 = zo; + super.tick(); + + if (weapon == null) { + if (age >= 2) { + discard(); + return; + } + setWeapon(weaponName()); + if (weapon == null) { + return; + } + } + + if (!level().isClientSide()) { + if (getY() <= level().getMaxY() && !level().isLoaded(blockPosition())) { + if (weapon.delayFuse <= 0) { + discard(); + return; + } + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + if (delayFuse > 0) { + delayFuse--; + if (delayFuse == 0) { + onTimeout(); + discard(); + return; + } + } + if (!isValid()) { + discard(); + return; + } + if (weapon.timeFuse > 0 && age > weapon.timeFuse) { + onTimeout(); + discard(); + return; + } + if (weapon.explosionAltitude > 0 && isGroundWithin(weapon.explosionAltitude)) { + impact(new Impact(position(), null, null, Direction.DOWN), 1.0F); + } + } + + motion = getDeltaMovement(); + setDeltaMovement(motion.x, motion.y + (isInWater() ? gravityInWater() : gravity()), motion.z); + if (!isRemoved()) { + checkCollisions(); + } + motion = getDeltaMovement().scale(accelerationFactor); + double nx = getX() + motion.x; + double ny = getY() + motion.y; + double nz = getZ() + motion.z; + if (level().isClientSide()) { + splash(nx, ny, nz); + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + level().addParticle(ParticleTypes.BUBBLE, nx - m.x * 0.25, ny - m.y * 0.25, nz - m.z * 0.25, m.x, m.y, m.z); + } + setPos(nx, ny, nz); + } + + /** Turns the nose along the flight path, the way every projectile class did after moving. */ + protected void faceMotion() { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + + /** {@code setMotion}: flies straight along {@code (dx, dy, dz)} at full speed. */ + protected void aimAlong(double dx, double dy, double dz) { + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d > 0.0) { + setDeltaMovement(dx * acceleration / d, dy * acceleration / d, dz * acceleration / d); + } + } + + /** {@code guidanceToTarget}: turns a seventh of the way towards the target each tick. */ + protected void guideTo(double tx, double ty, double tz) { + double dx = tx - getX(); + double dy = ty - getY(); + double dz = tz - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d <= 0.0) { + return; + } + Vec3 m = getDeltaMovement(); + setDeltaMovement((m.x * 6.0 + dx * acceleration / d) / 7.0, + (m.y * 6.0 + dy * acceleration / d) / 7.0, + (m.z * 6.0 + dz * acceleration / d) / 7.0); + faceMotion(); + } + + /** {@code checkValid}: whoever fired it still exists and is not too far away. */ + private boolean isValid() { + if (shooter == null && user == null) { + return false; + } + if (user != null && user.isRemoved()) { + return false; + } + Entity origin = user != null ? user : shooter; + double dx = getX() - origin.getX(); + double dz = getZ() - origin.getZ(); + return dx * dx + dz * dz < MAX_RANGE_SQ; + } + + private boolean isGroundWithin(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(Direction.DOWN); + if (!level().getBlockState(pos).isAir()) { + return true; + } + } + return false; + } + + /** Spawns {@code count} trail particles spread over the last two ticks of flight. */ + protected void trail(String name, int count, float size) { + if (!level().isClientSide() || name.isEmpty() || count < 1 || count > 50) { + return; + } + ParticleOptions particle = trailParticle(name); + double x = (getX() - xo) / count; + double y = (getY() - yo) / count; + double z = (getZ() - zo) / count; + double x2 = (xo - xo2) / count; + double y2 = (yo - yo2) / count; + double z2 = (zo - zo2) / count; + for (int i = 0; i < count; i++) { + double px = (xo + x * i + xo2 + x2 * i) / 2.0; + double py = (yo + y * i + yo2 + y2 * i) / 2.0; + double pz = (zo + z * i + zo2 + z2 * i) / 2.0; + level().addAlwaysVisibleParticle(particle, true, px, py, pz, 0.0, 0.0, 0.0); + } + } + + /** + * The original's own smoke particle ({@code explode}) was a large white puff; the cloud is the + * closest vanilla one. Other names are 1.7.10 particle names. + */ + private static ParticleOptions trailParticle(String name) { + return switch (name) { + case "explode", "cloud" -> ParticleTypes.CLOUD; + case "flame" -> ParticleTypes.FLAME; + case "largesmoke" -> ParticleTypes.LARGE_SMOKE; + case "heart" -> ParticleTypes.HEART; + case "fireworksspark" -> ParticleTypes.FIREWORK; + default -> ParticleTypes.SMOKE; + }; + } + + private void splash(double nx, double ny, double nz) { + if (weapon == null || weapon.power <= 0) { + return; + } + boolean wasDry = !level().getFluidState(BlockPos.containing(xo + 0.5, yo + 0.5, zo + 0.5)).is(FluidTags.WATER); + if (wasDry && level().getFluidState(BlockPos.containing(nx + 0.5, ny + 0.5, nz + 0.5)).is(FluidTags.WATER)) { + float pwr = Math.min(weapon.power, 20); + int n = random.nextInt(1 + (int) pwr / 3) + (int) pwr / 2 + 1; + for (int i = 0; i < n; i++) { + level().addParticle(ParticleTypes.SPLASH, nx, ny + 0.5, nz, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3, + pwr * 0.03 * (random.nextDouble() * 0.5 + 0.5) * 1.8, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3); + } + } + } + + /** {@code onUpdateTimeout}: the fuse ran out in mid air. */ + protected void onTimeout() { + if (!(level() instanceof ServerLevel server) || weapon == null) { + return; + } + if (isInWater()) { + if (explosionPowerInWater > 0) { + explode(server, getX(), getY(), getZ(), explosionPowerInWater, explosionPowerInWater, true); + } + } else if (explosionPower > 0) { + explode(server, getX(), getY(), getZ(), explosionPower, weapon.explosionBlock, false); + } else if (explosionPower < 0) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } + + /** {@code onUpdateBomblet}: once the sprinkle timer runs out the carrier splits up. */ + protected void tickBomblets() { + if (!level().isClientSide() && sprinkleTime > 0 && !isRemoved() && weapon != null) { + sprinkleTime--; + if (sprinkleTime == 0) { + for (int i = 0; i < weapon.bomblet; i++) { + sprinkleBomblet(); + } + discard(); + } + } + } + + protected void sprinkleBomblet() { + } + + /** Creates a copy of this projectile's kind for a bomblet, flying from here. */ + protected @Nullable T spawnChild(EntityType type, double mx, double my, double mz, float yaw, float pitch) { + T child = type.create(level(), EntitySpawnReason.TRIGGERED); + if (child == null) { + return null; + } + child.launch(getX(), getY(), getZ(), mx, my, mz, yaw, pitch, acceleration); + child.setWeapon(weaponName()); + child.setParameters(this); + return child; + } + + // Collision + + /** Where something was hit. {@code entity} is set for entity hits, {@code block} for block hits. */ + public record Impact(Vec3 location, @Nullable Entity entity, @Nullable BlockPos block, Direction side) { + } + + /** {@code onUpdateCollided}: the swept ray of this tick against blocks, then entities. */ + protected void checkCollisions() { + if (weapon == null) { + return; + } + Vec3 motion = getDeltaMovement().scale(accelerationFactor); + Vec3 from = position(); + Vec3 to = from.add(motion); + BlockHitResult blockHit = null; + for (int i = 0; i < 5; i++) { + blockHit = level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (blockHit.getType() != HitResult.Type.BLOCK) { + blockHit = null; + break; + } + BlockState state = level().getBlockState(blockHit.getBlockPos()); + if (user == null || level().isClientSide() || !Combat.isBulletBreakable(state)) { + break; + } + level().destroyBlock(blockHit.getBlockPos(), true, user); + } + + if (weapon.delayFuse > 0) { + if (blockHit != null) { + bounce(blockHit.getDirection()); + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + return; + } + + if (blockHit != null) { + to = blockHit.getLocation(); + } + Impact entityHit = findEntity(from, to, motion); + if (entityHit != null) { + // A vehicle applies the factor of the box struck itself, when it takes the damage. + impact(entityHit, 1.0F); + } else if (blockHit != null) { + impact(new Impact(blockHit.getLocation(), null, blockHit.getBlockPos(), blockHit.getDirection()), 1.0F); + } + } + + private @Nullable Impact findEntity(Vec3 from, Vec3 to, Vec3 motion) { + AABB area = getBoundingBox().expandTowards(motion).inflate(21.0); + Entity best = null; + Vec3 bestAt = null; + double bestDistance = 0.0; + List clipped = new ArrayList<>(); + for (Entity candidate : level().getEntities(this, area, this::canHit)) { + Optional at; + if (candidate instanceof AircraftEntity vehicle) { + at = vehicle.clipHitBoxes(from, to, 0.3); + clipped.add(vehicle); + } else { + at = candidate.getBoundingBox().inflate(0.3).clip(from, to); + } + if (at.isPresent()) { + double d = from.distanceTo(at.get()); + if (best == null || d < bestDistance) { + best = candidate; + bestAt = at.get(); + bestDistance = d; + } + } + } + // Only the vehicle actually hit keeps the factor of the box the ray struck. + for (AircraftEntity vehicle : clipped) { + if (vehicle != best) { + vehicle.consumeHitBoxFactor(); + } + } + return best != null ? new Impact(bestAt, best, null, Direction.UP) : null; + } + + /** Clients only learn who fired through synched data; look them up once it has arrived. */ + private void resolveOwners() { + if (shooter == null && entityData.get(DATA_SHOOTER) > 0) { + shooter = level().getEntity(entityData.get(DATA_SHOOTER)); + } + if (user == null && entityData.get(DATA_USER) > 0) { + user = level().getEntity(entityData.get(DATA_USER)); + } + } + + /** {@code canBeCollidedEntity}: never the shooter, its crew, or rounds fired by the same side. */ + public boolean canHit(Entity entity) { + if (level().isClientSide()) { + resolveOwners(); + } + if (!entity.isPickable() || entity == user || entity == shooter) { + return false; + } + if (entity instanceof ProjectileEntity other) { + if (level().isClientSide()) { + return false; + } + if (other.shooter != null && other.shooter == shooter || other.user != null && other.user == user) { + return false; + } + } + return !(shooter instanceof AircraftEntity vehicle) || !vehicle.hasPassenger(entity); + } + + /** {@code boundBullet}: a delayed fuse round bounces off what it hits. */ + private void bounce(Direction side) { + Vec3 m = getDeltaMovement(); + float bound = weapon != null ? weapon.bound : 0.0F; + switch (side) { + case DOWN -> { + if (m.y > 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case UP -> { + if (m.y < 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case NORTH -> { + if (m.z > 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case SOUTH -> { + if (m.z < 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case WEST -> { + if (m.x > 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + case EAST -> { + if (m.x < 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + } + } + + /** {@code onImpact}: damage what was hit, then explode or pierce through. */ + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + if (weapon != null && (weapon.explosion == 0 || weapon.modeNum >= 2) && hit.block() != null) { + debris(hit); + } + if (piercing <= 0) { + // The server decides what the hit does; until its removal arrives the round stays + // where it struck instead of flying on through the hull. + spent = true; + setPos(hit.location()); + setDeltaMovement(Vec3.ZERO); + } + return; + } + if (weapon == null) { + discard(); + return; + } + if (hit.entity() != null) { + hitEntity(hit.entity(), damageFactor); + piercing = 0; + } + float expPower = explosionPower * damageFactor; + float expPowerInWater = explosionPowerInWater * damageFactor; + Vec3 at = hit.location(); + if (piercing > 0) { + piercing--; + if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, 1.0F, 1.0F, false); + } + return; + } + if (expPowerInWater == 0.0F) { + if (weapon.fae) { + explodeFae(server, expPower); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } else if (hit.entity() != null) { + if (isInWater()) { + explode(server, at.x, at.y, at.z, expPowerInWater, expPowerInWater, true); + } else { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } + } else if (isInWater() || hit.block() != null && isWaterAround(hit.block())) { + BlockPos b = hit.block() != null ? hit.block() : blockPosition(); + explode(server, b.getX(), b.getY(), b.getZ(), expPowerInWater, expPowerInWater, true); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + discard(); + } + + /** {@code MCH_Lib.isBlockInWater}: water on any side of the block. */ + private boolean isWaterAround(BlockPos pos) { + for (Direction d : Direction.values()) { + if (level().getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return level().getFluidState(pos).is(FluidTags.WATER); + } + + /** Client side chips flying off the block a round struck. */ + private void debris(Impact hit) { + if (weapon == null || hit.block() == null) { + return; + } + BlockState state = level().getBlockState(hit.block()); + if (state.isAir()) { + return; + } + float p = weapon.power; + Vec3 m = getDeltaMovement(); + var particle = new BlockParticleOption(ParticleTypes.BLOCK, state); + for (int i = 0; i < p / 3.0F; i++) { + level().addParticle(particle, + hit.location().x + (random.nextFloat() - 0.5) * p / 10.0, + hit.location().y + 0.1, + hit.location().z + (random.nextFloat() - 0.5) * p / 10.0, + -m.x * p / 2.0, p / 2.0F, -m.z * p / 2.0); + } + } + + /** {@code onImpactEntity}. */ + protected void hitEntity(Entity entity, float damageFactor) { + if (!entity.isRemoved()) { + DamageSource source = ModDamageTypes.bullet(level(), this, user); + Combat.hurt(entity, source, power * damageFactor, weapon); + } + confirmHit(); + } + + /** {@code notifyHitBullet}: the shooter hears that a round connected. */ + protected void confirmHit() { + if (user instanceof ServerPlayer player) { + ModSounds.playTo(player, "hit", 1.0F, 1.0F); + } + } + + protected void explode(ServerLevel server, double x, double y, double z, float size, float blockSize, boolean inWater) { + if (weapon == null) { + return; + } + boolean destroyBlocks = !isBomblet() || random.nextInt(3) == 0; + Explosions.Params p = new Explosions.Params(this, user, size, blockSize, weapon.flaming, destroyBlocks, 0, inWater, weapon); + if (Explosions.explode(server, x, y, z, p)) { + confirmHit(); + } + } + + /** Fuel air explosives burn whatever they catch. */ + private void explodeFae(ServerLevel server, float size) { + Explosions.Params p = new Explosions.Params(this, user, size, weapon.explosionBlock, weapon.flaming, true, 15, false, weapon); + if (Explosions.explode(server, getX(), getY(), getZ(), p)) { + confirmHit(); + } + } + + // Entity plumbing + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + // Something thrown at a projectile sets it off, as in the original. + if (damage > 0.0F && source.is(DamageTypes.THROWN) && !isRemoved()) { + impact(new Impact(position().add(0.5, 0.5, 0.5), null, blockPosition(), Direction.UP), 1.0F); + return true; + } + return false; + } + + /** Client only: whether the round already hit something and should no longer be drawn. */ + public boolean isSpent() { + return spent; + } + + @Override + public boolean isPickable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putString("WeaponName", weaponName()); + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From c68fdb20c793f0e2eebf35f75bdbdc08a5efc992 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:29 +0800 Subject: [PATCH 0265/1461] Add entity/projectile/RocketEntity.java to the 26.2 shared sources --- .../entity/projectile/RocketEntity.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java new file mode 100644 index 00000000..c39fa6fe --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** An unguided rocket, {@code MCH_EntityRocket}, which may burst into bomblets. */ +public class RocketEntity extends ProjectileEntity { + public RocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return !isBomblet(); + } + + @Override + public void tick() { + super.tick(); + tickBomblets(); + WeaponDefinition w = weapon(); + if (!isBomblet() && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + RocketEntity child = spawnChild(ModEntities.ROCKET.get(), m.x, m.y, m.z, getYRot(), getXRot()); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(child.getDeltaMovement().add( + (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread)); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From a2bb40aece417e66d8ba6ff9213d66b9d1fad4cc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:30 +0800 Subject: [PATCH 0266/1461] Hold and run the interpolation handler in ThrowableEntity on 26.2 --- .../entity/projectile/ThrowableEntity.java | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java new file mode 100644 index 00000000..479ead5a --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java @@ -0,0 +1,200 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.Explosions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ARGB; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A thrown grenade, {@code MCH_EntityThrowable}. It flies under its definition's gravity, bounces + * off blocks losing speed, and when its fuse runs out either explodes or, for a smoke grenade, + * pours out coloured smoke until it disappears. + */ +public class ThrowableEntity extends Entity { + private static final EntityDataAccessor DATA_NAME = SynchedEntityData.defineId(ThrowableEntity.class, EntityDataSerializers.STRING); + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private @Nullable Entity thrower; + private @Nullable ThrowableDefinition definition; + private int definitionGeneration = -1; + + public ThrowableEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Throws {@code def} from {@code thrower}'s eyes the way it looks. + * + * @param strength 0.2 for a flick up to 1.4 for a full draw, the original's acceleration + */ + public static ThrowableEntity thrown(Level level, Entity thrower, ThrowableDefinition def, float strength) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + Vec3 look = thrower.getLookAngle(); + entity.setPos(thrower.getX() + look.x * 0.3, thrower.getEyeY() - 0.1, thrower.getZ() + look.z * 0.3); + entity.setYRot(thrower.getYRot()); + entity.setDeltaMovement(look.scale(1.5 * strength).add(thrower.getDeltaMovement())); + entity.thrower = thrower; + entity.setDefinition(def); + return entity; + } + + /** One fired from a dispenser: no thrower, straight along {@code direction}. */ + public static ThrowableEntity dispensed(Level level, Vec3 at, Vec3 direction, ThrowableDefinition def) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + entity.setPos(at); + entity.setDeltaMovement(direction.normalize().scale(def.dispenseAcceleration)); + entity.setDefinition(def); + return entity; + } + + private void setDefinition(ThrowableDefinition def) { + entityData.set(DATA_NAME, def.name()); + definition = def; + definitionGeneration = MCHeli.content().generation(); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_NAME, ""); + } + + /** The definition by name, looked up again after a content reload. */ + public @Nullable ThrowableDefinition definition() { + if (definitionGeneration != MCHeli.content().generation()) { + definition = MCHeli.content().throwable(entityData.get(DATA_NAME)); + definitionGeneration = MCHeli.content().generation(); + } + return definition; + } + + public String definitionName() { + return entityData.get(DATA_NAME); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + ThrowableDefinition def = definition(); + if (def == null) { + // The original gave a missing definition ten ticks to arrive. + if (tickCount > 10 && !level().isClientSide()) { + discard(); + } + return; + } + if (level().isClientSide()) { + if (def.smokes() && tickCount >= def.timeFuse) { + smoke(def); + } + return; + } + if (tickCount >= def.timeFuse && !def.smokes()) { + Explosions.explode((ServerLevel) level(), getX(), getY(), getZ(), new Explosions.Params(this, thrower, + def.explosion, def.explosion, false, true, 0, isInWater(), null)); + discard(); + return; + } + if (tickCount >= def.aliveTime) { + discard(); + return; + } + fly(def); + } + + /** {@code onUpdate} and {@code boundBullet}: gravity of its own, and a bounce off whatever it hits. */ + private void fly(ThrowableDefinition def) { + boolean water = isInWater(); + Vec3 m = getDeltaMovement().add(0.0, water ? def.gravityInWater : def.gravity, 0.0); + Vec3 from = position(); + move(MoverType.SELF, m); + // Whichever axis came up short hit something. + Vec3 moved = position().subtract(from); + double mx = blocked(moved.x, m.x) ? -m.x * def.bound : m.x; + double my = m.y; + double mz = blocked(moved.z, m.z) ? -m.z * def.bound : m.z; + if (blocked(moved.y, m.y)) { + mx *= 0.9; + mz *= 0.9; + // Off the floor or the ceiling it keeps a share of its speed, unless that would be a crawl. + my = Math.abs(my) > 0.05 ? -my * def.bound : 0.0; + } + double drag = water ? 0.8 : 0.99; + setDeltaMovement(mx * drag, my * drag, mz * drag); + } + + private static boolean blocked(double moved, double wanted) { + return Math.abs(moved) < Math.abs(wanted) - 1.0E-6; + } + + private void smoke(ThrowableDefinition def) { + int color = def.smokeColor; + for (int i = 0; i < def.smokeNum; i++) { + // A little noise per puff, keeping greys grey. + float r = ARGB.redFloat(color) * 0.9F + random.nextFloat() * 0.1F; + float g = ARGB.greenFloat(color) * 0.9F + random.nextFloat() * 0.1F; + float b = ARGB.blueFloat(color) * 0.9F + random.nextFloat() * 0.1F; + if (ARGB.red(color) == ARGB.green(color)) { + g = r; + } + if (ARGB.red(color) == ARGB.blue(color)) { + b = r; + } + if (ARGB.green(color) == ARGB.blue(color)) { + b = g; + } + float size = def.smokeSize + random.nextFloat() * def.smokeSize / 3.0F; + level().addAlwaysVisibleParticle(new SmokeParticleOptions(ARGB.colorFromFloat(1.0F, r, g, b) & 0xFFFFFF, size), true, + getX(), getY() + 0.5, getZ(), + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2, + def.smokeVelocityVertical * random.nextFloat() * 0.2, + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2); + } + } + + /** The server moves it; clients ease towards each position it sends. */ + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From d2a12c2c08670d38c3b888c260c3bd729b8bd97d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:30 +0800 Subject: [PATCH 0267/1461] Add entity/projectile/TorpedoEntity.java to the 26.2 shared sources --- .../entity/projectile/TorpedoEntity.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java new file mode 100644 index 00000000..a89f7c01 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java @@ -0,0 +1,56 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A torpedo, {@code MCH_EntityTorpedo}. Once in the water it settles on its running speed and + * either holds course or, when guided, runs to the point the gunner aimed at. + */ +public class TorpedoEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + public double accelerationInWater = 2.0; + + public TorpedoEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + boolean guided = w != null && w.guidedTorpedo; + if (!level().isClientSide() && isInWater()) { + if (acceleration < accelerationInWater) { + acceleration += 0.1; + } else if (acceleration > accelerationInWater + 0.2F) { + acceleration -= 0.1; + } + if (guided) { + aimAlong(targetX - getX(), targetY - getY(), targetZ - getZ()); + } else { + Vec3 m = getDeltaMovement(); + aimAlong(m.x, m.y * 0.8F, m.z); + } + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + if (guided) { + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + } + + @Override + public String defaultModel() { + return "torpedo"; + } +} From 986e49efb6cee8d06646242edf0c7b1a10934939 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:31 +0800 Subject: [PATCH 0268/1461] Add entity/projectile/TvMissileEntity.java to the 26.2 shared sources --- .../entity/projectile/TvMissileEntity.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java new file mode 100644 index 00000000..a06fef1a --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A wire guided missile flown by the gunner's view, {@code MCH_EntityTvMissile}. It steers while + * it is the vehicle's active TV missile; once another is fired it carries on straight. + */ +public class TvMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 1440000.0; + + public TvMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + Entity aimer = user(); + if (aimer != null) { + if (distanceToSqr(aimer) > MAX_DISTANCE_SQ) { + discard(); + } + if (!level().isClientSide() && !isRemoved()) { + steer(aimer); + } + } else if (!level().isClientSide()) { + discard(); + } + } + + private void steer(Entity aimer) { + if (aimer.isRemoved() || !(shooter() instanceof AircraftEntity vehicle) || vehicle.tvMissile() != this) { + return; + } + Vec3 dir = Vec3.directionFromRotation(aimer.getXRot(), aimer.getYRot()); + aimAlong(dir.x, dir.y, dir.z); + setYRot(aimer.getYRot()); + setXRot(aimer.getXRot()); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From ad36743ffafd02b86c177b8ebdc694287a3bb5c4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:31 +0800 Subject: [PATCH 0269/1461] Add item/AircraftItem.java to the 26.2 shared sources --- .../jnhprojects/mcheli/item/AircraftItem.java | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java new file mode 100644 index 00000000..7b04e4a4 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java @@ -0,0 +1,146 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.ChatFormatting; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.component.TooltipDisplay; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Places a vehicle, the same way a boat is placed. + * + *

There is one item per vehicle kind ({@code mcheli:helicopter}, {@code mcheli:plane} and so + * on). Which vehicle a stack places is stored in its {@link ModDataComponents#VEHICLE} component, + * and its look comes from the {@code item_model} component pointing at the model the import wrote + * for that vehicle. Items are registered once at startup, so this is what lets definitions be + * added or reloaded while the game runs. + */ +public class AircraftItem extends Item { + private final String kind; + + public AircraftItem(String kind, Properties properties) { + super(properties); + this.kind = kind; + } + + /** The vehicle kind this item holds, such as {@code helicopter}. */ + public String kind() { + return kind; + } + + /** + * A stack that places {@code def}. Vehicles from the import show the item model the import + * wrote for them; item models only load with the resources, so pack vehicles keep their kind's + * own model. + */ + public static ItemStack stackOf(AircraftDefinition def) { + ItemStack stack = new ItemStack(ModItems.forKind(def.kind())); + stack.set(ModDataComponents.VEHICLE.get(), def.name()); + if (def.pack().equals(ContentSource.ORIGINAL)) { + stack.set(DataComponents.ITEM_MODEL, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, def.name())); + } + return stack; + } + + /** The definition name a stack places, or {@code null} if it is not a vehicle item or has none. */ + public static @Nullable String vehicleOf(ItemStack stack) { + return stack.getItem() instanceof AircraftItem item ? item.definitionName(stack) : null; + } + + protected @Nullable String definitionName(ItemStack stack) { + return stack.get(ModDataComponents.VEHICLE.get()); + } + + @Override + public Component getName(ItemStack stack) { + String name = definitionName(stack); + if (name == null) { + return super.getName(stack); + } + AircraftDefinition def = MCHeli.content().any(name); + // Vehicles added at runtime may bring no lang entry; their DisplayName stands in. + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + /** Who made a vehicle from a content pack. The import's vehicles have their credits in the docs. */ + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display, Consumer out, TooltipFlag flag) { + super.appendHoverText(stack, context, display, out, flag); + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + PackManifest credit = def != null ? def.credit() : null; + if (credit != null) { + out.accept((credit.author().isEmpty() + ? Component.translatable("item.mcheli.vehicle.from_pack", credit.name()) + : Component.translatable("item.mcheli.vehicle.from_pack_by", credit.name(), credit.author())) + .withStyle(ChatFormatting.GRAY)); + } + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + HitResult hit = getPlayerPOVHitResult(level, player, ClipContext.Fluid.ANY); + if (!(hit instanceof BlockHitResult blockHit) || hit.getType() != HitResult.Type.BLOCK) { + return InteractionResult.PASS; + } + + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + if (!level.isClientSide()) { + player.sendOverlayMessage(Component.translatable("message.mcheli.vehicle.unknown", String.valueOf(name))); + } + return InteractionResult.FAIL; + } + if (!level.isClientSide() && ServerConfig.isDisabled(def.name())) { + player.sendOverlayMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName())); + return InteractionResult.FAIL; + } + + AircraftEntity vehicle = ModEntities.typeFor(def).create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (vehicle == null) { + return InteractionResult.FAIL; + } + Vec3 at = blockHit.getLocation(); + vehicle.setType(def); + vehicle.snapTo(at.x, at.y, at.z, player.getYRot(), 0.0F); + if (!level.noCollision(vehicle, vehicle.getBoundingBox())) { + return InteractionResult.FAIL; + } + + if (!level.isClientSide()) { + level.addFreshEntity(vehicle); + level.gameEvent(player, GameEvent.ENTITY_PLACE, at); + stack.consume(1, player); + } + player.awardStat(Stats.ITEM_USED.get(this)); + return InteractionResult.SUCCESS; + } +} From a79ca36cdf4c89190d6b6507d49c8ed2cfcbdd00 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:32 +0800 Subject: [PATCH 0270/1461] Add item/ChainItem.java to the 26.2 shared sources --- .../jnhprojects/mcheli/item/ChainItem.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java new file mode 100644 index 00000000..2f18676a --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java @@ -0,0 +1,85 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.ChainEntity; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; + +import java.util.Map; +import java.util.UUID; +import java.util.WeakHashMap; + +/** + * The tow chain, {@code MCH_ItemChain}. Use it on both ends, in either order, two to sixteen + * blocks apart: the vehicle pulls, so one end must be a vehicle. Use it on either end of a chain + * to take the chain off, or on the hooked end again to let go of it. Vehicles and containers + * hand the click over here, and every step is told to the player above the hotbar. + */ +public class ChainItem extends Item { + private static final double MIN_DISTANCE = 2.0; + private static final double MAX_DISTANCE = 16.0; + + /** The first end each player has hooked, until they hook the second. Server only. */ + private static final Map HOOKED = new WeakHashMap<>(); + + public ChainItem(Properties properties) { + super(properties.stacksTo(1)); + } + + /** A click with the chain on {@code target}. */ + public static InteractionResult useOn(Entity target, Player player) { + if (!(player.level() instanceof ServerLevel level)) { + return InteractionResult.SUCCESS; + } + ChainEntity existing = ChainEntity.on(target); + if (existing != null) { + MCHeli.LOGGER.debug("Chain: {} takes the chain off {}", player.getName().getString(), target); + existing.disconnect(); + HOOKED.remove(player); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.removed")); + return InteractionResult.SUCCESS; + } + UUID firstId = HOOKED.get(player); + Entity first = firstId != null ? level.getEntity(firstId) : null; + // An end hooked far away was most likely forgotten: start over from this one. + if (first == null || first.isRemoved() || target.distanceTo(first) > MAX_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} hooks {}", player.getName().getString(), target); + HOOKED.put(player, target.getUUID()); + ModSounds.playAt(target, "chain", 1.0F, 1.0F); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.hooked", target.getDisplayName())); + return InteractionResult.SUCCESS; + } + if (first == target) { + MCHeli.LOGGER.debug("Chain: {} lets go of {}", player.getName().getString(), target); + HOOKED.remove(player); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.released")); + return InteractionResult.SUCCESS; + } + double distance = target.distanceTo(first); + if (distance < MIN_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} and {} are only {} blocks apart", first, target, distance); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.too_close", String.format("%.1f", distance))); + return InteractionResult.FAIL; + } + if (!ChainEntity.canTow(first) && !ChainEntity.canTow(target)) { + MCHeli.LOGGER.debug("Chain: neither {} nor {} can tow", first, target); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.no_vehicle")); + return InteractionResult.FAIL; + } + MCHeli.LOGGER.debug("Chain: {} chains {} and {}", player.getName().getString(), first, target); + ChainEntity chain = ChainEntity.connect(level, first, target); + HOOKED.remove(player); + Entity towed = chain.towed(); + Entity tow = chain.tow(); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.connected", + towed != null ? towed.getDisplayName() : first.getDisplayName(), tow != null ? tow.getDisplayName() : target.getDisplayName())); + return InteractionResult.SUCCESS; + } +} From b39471884b6a58a242cd27a68b0de15cd7cade3e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:32 +0800 Subject: [PATCH 0271/1461] Add item/FuelItem.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/item/FuelItem.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java new file mode 100644 index 00000000..efa8f295 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +/** + * The fuel can, {@code MCH_ItemFuel}. It holds 600 units; the durability bar shows how much is + * left. Using it refills it from the coal and charcoal in the inventory, as in the original. + * + *

A can in a fuel slot of the vehicle screen empties into the tank while the vehicle stands on + * something, as in the original. Not in the original: right-clicking a vehicle with the can pours + * up to 100 units at once, which saves opening the screen. + */ +public class FuelItem extends Item { + /** Units a full can holds, the original's maximum damage. */ + public static final int CAPACITY = 600; + /** Units poured into a vehicle per click: what the original moved per fuel slot per update. */ + private static final int POUR = 100; + private static final int PER_COAL = 100; + private static final int PER_CHARCOAL = 75; + + public FuelItem(Properties properties) { + super(properties.durability(CAPACITY)); + } + + public static int contents(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + /** Fills a vehicle's tank from the can. Server only. */ + public static InteractionResult pour(ItemStack stack, AircraftEntity vehicle, Player player) { + int added = vehicle.refuel(player.getAbilities().instabuild ? POUR : Math.min(contents(stack), POUR)); + if (added <= 0) { + return InteractionResult.FAIL; + } + if (!player.getAbilities().instabuild) { + stack.setDamageValue(stack.getDamageValue() + added); + } + vehicle.level().playSound(null, vehicle.getX(), vehicle.getY(), vehicle.getZ(), SoundEvents.BUCKET_EMPTY, SoundSource.NEUTRAL, 1.0F, 1.0F); + return InteractionResult.SUCCESS; + } + + /** Charcoal first, then coal, each one worth part of the can, until it is full. */ + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + if (!stack.isDamaged() || player.getAbilities().instabuild) { + return InteractionResult.PASS; + } + if (!level.isClientSide()) { + refill(stack, player.getInventory(), Items.CHARCOAL, PER_CHARCOAL); + refill(stack, player.getInventory(), Items.COAL, PER_COAL); + } + return InteractionResult.SUCCESS; + } + + private static void refill(ItemStack can, Inventory inventory, Item coal, int worth) { + for (int slot = 0; slot < inventory.getContainerSize() && can.isDamaged(); slot++) { + ItemStack item = inventory.getItem(slot); + while (item.is(coal) && !item.isEmpty() && can.isDamaged()) { + can.setDamageValue(Math.max(can.getDamageValue() - worth, 0)); + item.shrink(1); + } + } + } +} From 5030f18f31dd3e0a3c898a9e09cf11071c630a5b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:33 +0800 Subject: [PATCH 0272/1461] Add item/LauncherItem.java to the 26.2 shared sources --- .../jnhprojects/mcheli/item/LauncherItem.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java new file mode 100644 index 00000000..a83ff5bd --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; + +import java.util.function.Supplier; + +/** + * A shoulder launched missile, {@code MCH_ItemLightWeaponBase}: the FIM-92 Stinger and the + * FGM-148 Javelin. Held up it is a sight that locks onto targets; the attack key fires once + * locked. It holds one missile, and its bar shows whether it is loaded: an empty launcher reloads + * from a missile in the inventory after two seconds held but not raised. + */ +public class LauncherItem extends Item { + /** The original's durability: 0 is loaded, the maximum is empty. */ + private static final int EMPTY = 10; + + private final String weapon; + private final Supplier missile; + + /** + * @param weapon the weapon definition it fires, such as {@code fim92} + * @param missile the item it reloads from + */ + public LauncherItem(String weapon, Supplier missile, Properties properties) { + super(properties.durability(EMPTY)); + this.weapon = weapon; + this.missile = missile; + } + + public String weapon() { + return weapon; + } + + public Item missile() { + return missile.get(); + } + + public static boolean isLoaded(ItemStack stack) { + return stack.getDamageValue() < stack.getMaxDamage(); + } + + public static void setLoaded(ItemStack stack, boolean loaded) { + stack.setDamageValue(loaded ? 0 : stack.getMaxDamage()); + } + + /** Not from a vehicle seat or the GLTD, as in the original. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity) && !(player.getVehicle() instanceof GltdEntity); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResult.PASS; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.BOW; + } +} From 54ecfcd8f2664e7dcb7e4f36ef29811cc0bb7828 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:33 +0800 Subject: [PATCH 0273/1461] Add item/LegacyAircraftItem.java to the 26.2 shared sources --- .../mcheli/item/LegacyAircraftItem.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java new file mode 100644 index 00000000..0a286838 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * A vehicle item as 1.0.0 saved it: one item per vehicle, such as {@code mcheli:ah-64}. + * + *

These are still registered for the vehicles imported at startup, so that stacks in 1.0.0 + * worlds load instead of vanishing. They work as before, are not listed anywhere, and turn into + * the generic item of their kind ({@link AircraftItem#stackOf}) as soon as they sit in a player's + * inventory. Stacks in chests convert once a player takes them out. + */ +public final class LegacyAircraftItem extends AircraftItem { + private final String definitionName; + + public LegacyAircraftItem(String kind, String definitionName, Properties properties) { + super(kind, properties); + this.definitionName = definitionName; + } + + @Override + protected @Nullable String definitionName(ItemStack stack) { + return definitionName; + } + + @Override + public void inventoryTick(ItemStack stack, ServerLevel level, Entity owner, @Nullable EquipmentSlot slot) { + if (!(owner instanceof Player player)) { + return; + } + AircraftDefinition def = MCHeli.content().any(definitionName); + if (def == null) { + return; + } + Inventory inventory = player.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + if (inventory.getItem(i) == stack) { + inventory.setItem(i, convert(stack, def)); + return; + } + } + } + + /** The generic stack for {@code def}, keeping count, custom name and anything else set on the old one. */ + static ItemStack convert(ItemStack legacy, AircraftDefinition def) { + ItemStack converted = AircraftItem.stackOf(def); + converted.applyComponents(legacy.getComponentsPatch()); + converted.set(ModDataComponents.VEHICLE.get(), def.name()); + converted.setCount(legacy.getCount()); + return converted; + } +} From 3381c097d4506fb03783b5fcb404cd16f275faf9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:34 +0800 Subject: [PATCH 0274/1461] Add item/ParachuteItem.java to the 26.2 shared sources --- .../mcheli/item/ParachuteItem.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java new file mode 100644 index 00000000..2af7b79b --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; + +/** The parachute, {@code MCH_ItemParachute}: used in mid-air, it opens above the player. */ +public class ParachuteItem extends Item { + public ParachuteItem(Properties properties) { + super(properties.stacksTo(1)); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (player.isPassenger() || player.onGround()) { + return InteractionResult.PASS; + } + if (!level.isClientSide()) { + ParachuteEntity.open(level, player, ParachuteEntity.Kind.ITEM); + player.getItemInHand(hand).consume(1, player); + } + return InteractionResult.SUCCESS; + } +} From bf938fa12be909b3a1b632c9990a83187e63c1c3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:34 +0800 Subject: [PATCH 0275/1461] Add item/PlacedEntityItem.java to the 26.2 shared sources --- .../mcheli/item/PlacedEntityItem.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java new file mode 100644 index 00000000..34c524d5 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java @@ -0,0 +1,62 @@ +package com.jnhprojects.mcheli.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; + +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Sets up a piece of equipment that lives in the world as an entity: the GLTD, the container, the + * UAV station. Like the originals' items it goes on top of the block clicked, facing the way the + * player looks, and only where it fits. + */ +public class PlacedEntityItem extends Item { + private final Supplier> type; + + public PlacedEntityItem(Supplier> type, Properties properties) { + super(properties); + this.type = type; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + Player player = context.getPlayer(); + BlockPos above = context.getClickedPos().above(); + Entity entity = type.get().create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (entity == null) { + return InteractionResult.FAIL; + } + entity.snapTo(above.getX() + 0.5, above.getY(), above.getZ() + 0.5, player != null ? player.getYRot() : 0.0F, 0.0F); + prepare(entity, player); + if (!level.noCollision(entity, entity.getBoundingBox().deflate(0.1)) || !level.getEntities(entity, entity.getBoundingBox()).isEmpty()) { + return InteractionResult.FAIL; + } + if (!level.isClientSide()) { + level.addFreshEntity(entity); + level.gameEvent(player, GameEvent.ENTITY_PLACE, entity.position()); + } + ItemStack stack = context.getItemInHand(); + if (player != null) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, player); + return InteractionResult.SUCCESS; + } + + /** Sets up the new entity before it is checked for room and spawned. */ + protected void prepare(Entity entity, @Nullable Player player) { + } +} From a51fbe0e7ad2e8fa87e85c17ee21d7c64c65146c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:35 +0800 Subject: [PATCH 0276/1461] Add item/RangefinderItem.java to the 26.2 shared sources --- .../mcheli/item/RangefinderItem.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java new file mode 100644 index 00000000..6a4fb996 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; + +/** + * The laser rangefinder, {@code MCH_ItemRangeFinder}. Held up to the eye it is a scope that reads + * the range to whatever it points at; the attack key spots targets for the team, using one of + * its ten charges. Crafting it with redstone charges it again. + */ +public class RangefinderItem extends Item { + public static final int CHARGES = 10; + + public RangefinderItem(Properties properties) { + super(properties.durability(CHARGES)); + } + + /** Not from a vehicle seat: the vehicle's own sight does that job. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity); + } + + /** Charges left, from none to {@link #CHARGES}. */ + public static int charges(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResult.PASS; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.SPYGLASS; + } +} From f4263a0813303142693a192ab47dac5de7d3f918 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:35 +0800 Subject: [PATCH 0277/1461] Add item/ThrowableItem.java to the 26.2 shared sources --- .../mcheli/item/ThrowableItem.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java new file mode 100644 index 00000000..df8f33f8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java @@ -0,0 +1,123 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.Direction; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.dispenser.BlockSource; +import net.minecraft.core.dispenser.DefaultDispenseItemBehavior; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Hand grenades, {@code MCH_ItemThrowable}. Like the vehicles there is one item, and the + * {@link ModDataComponents#THROWABLE} component says which definition a stack throws, so pack + * throwables need no item of their own. Held like a bow: the longer the draw, the further it goes. + */ +public class ThrowableItem extends Item { + private static final int FULL_DRAW = 35; + + public ThrowableItem(Properties properties) { + super(properties); + } + + /** A stack of {@code def}, showing the item model the import wrote for it. */ + public static ItemStack stackOf(ThrowableDefinition def) { + ItemStack stack = new ItemStack(ModItems.THROWABLE.get()); + stack.set(ModDataComponents.THROWABLE.get(), def.name()); + stack.set(DataComponents.MAX_STACK_SIZE, def.stackSize); + if (def.pack().equals(ContentSource.ORIGINAL)) { + stack.set(DataComponents.ITEM_MODEL, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, def.name())); + } + return stack; + } + + private static @Nullable ThrowableDefinition definition(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + return name != null ? MCHeli.content().throwable(name) : null; + } + + @Override + public Component getName(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + if (name == null) { + return super.getName(stack); + } + ThrowableDefinition def = MCHeli.content().throwable(name); + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (definition(player.getItemInHand(hand)) == null) { + return InteractionResult.FAIL; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.BOW; + } + + @Override + public boolean releaseUsing(ItemStack stack, Level level, LivingEntity entity, int remainingTime) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return false; + } + int drawn = Math.max(getUseDuration(stack, entity) - remainingTime, 5); + float strength = drawn <= FULL_DRAW ? drawn / 25.0F : 1.0F; + level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), SoundEvents.ARROW_SHOOT, SoundSource.PLAYERS, + 0.5F, 0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)); + if (!level.isClientSide()) { + level.addFreshEntity(ThrowableEntity.thrown(level, entity, def, strength)); + } + if (entity instanceof Player player) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, entity); + return true; + } + + /** Dispensers throw them straight out, {@code MCH_ItemThrowableDispenseBehavior}. */ + public static class DispenseBehavior extends DefaultDispenseItemBehavior { + @Override + protected ItemStack execute(BlockSource source, ItemStack stack) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return super.execute(source, stack); + } + Direction facing = source.state().getValue(DispenserBlock.FACING); + Vec3 at = source.center().add(facing.getStepX() * 0.7, facing.getStepY() * 0.7, facing.getStepZ() * 0.7); + Vec3 direction = new Vec3(facing.getStepX(), facing.getStepY() + 0.1, facing.getStepZ()); + source.level().addFreshEntity(ThrowableEntity.dispensed(source.level(), at, direction, def)); + stack.shrink(1); + return stack; + } + } +} From 59727883dd12d2c8820e0bf0f2d3957bbc0211c6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:35 +0800 Subject: [PATCH 0278/1461] Add item/UavStationItem.java to the 26.2 shared sources --- .../mcheli/item/UavStationItem.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java new file mode 100644 index 00000000..7e3bc1c8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java @@ -0,0 +1,33 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station and the portable UAV controller, {@code MCH_ItemUavStation}. Both place the same + * entity; the kind decides its model and which drones it launches. + */ +public class UavStationItem extends PlacedEntityItem { + private final int kind; + + public UavStationItem(int kind, Properties properties) { + super(ModEntities.UAV_STATION, properties); + this.kind = kind; + } + + /** Squared to the nearest quarter turn, facing the player, as the original placed it. */ + @Override + protected void prepare(Entity entity, @Nullable Player player) { + if (entity instanceof UavStationEntity station) { + station.setKind(kind); + float yaw = player != null ? player.getYRot() : 0.0F; + int quarter = Math.floorMod(Math.round(Mth.wrapDegrees(yaw) / 90.0F), 4); + station.setYRot(quarter * 90.0F - 180.0F); + station.initOffset(); + } + } +} From 8d1c9bdd29e36ff444c4f5993b3d608582012e77 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:36 +0800 Subject: [PATCH 0279/1461] Add item/VehicleIngredient.java to the 26.2 shared sources --- .../mcheli/item/VehicleIngredient.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java new file mode 100644 index 00000000..c2619049 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemStackTemplate; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Matches one vehicle, such as an AH-6 in the recipe that turns it into an AH-6X. Every vehicle of + * a kind shares an item, so a plain item ingredient would accept any of them. In JSON: + *

{"type": "mcheli:vehicle", "vehicle": "ah-6"}
+ * with the type also under {@code neoforge:ingredient_type} and {@code fabric:type}, since each + * loader names custom ingredients its own way. + * + *

This is the loader-independent half; each loader module wraps it in its own ingredient class. + */ +public final class VehicleIngredient { + public static final Identifier ID = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "vehicle"); + + private VehicleIngredient() { + } + + /** The JSON fields, given a way to build the loader's ingredient from a vehicle name. */ + public static MapCodec codec(Function create, Function vehicle) { + return Codec.STRING.fieldOf("vehicle").xmap(create, vehicle); + } + + public static StreamCodec streamCodec(Function create, Function vehicle) { + return ByteBufCodecs.STRING_UTF8.map(create, vehicle).cast(); + } + + /** The items a vehicle can be: the generic item of each kind. */ + @SuppressWarnings("deprecation") + public static HolderSet kindItems() { + return HolderSet.direct(Item::builtInRegistryHolder, + ModItems.HELICOPTER.get(), ModItems.PLANE.get(), ModItems.TANK.get(), ModItems.VEHICLE.get()); + } + + public static Stream> items() { + return kindItems().stream(); + } + + public static boolean matches(String vehicle, @Nullable ItemStack stack) { + return stack != null && vehicle.equals(AircraftItem.vehicleOf(stack)); + } + + /** What recipe books show: the vehicle itself, or nothing while it is not loaded. */ + public static SlotDisplay display(String vehicle) { + AircraftDefinition def = MCHeli.content().any(vehicle); + return def != null ? new SlotDisplay.ItemStackSlotDisplay(ItemStackTemplate.fromNonEmptyStack(AircraftItem.stackOf(def))) + : SlotDisplay.Empty.INSTANCE; + } +} From bb62e3118cc49efe48945a510253b8348338375c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:36 +0800 Subject: [PATCH 0280/1461] Add item/WrenchItem.java to the 26.2 shared sources --- .../jnhprojects/mcheli/item/WrenchItem.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java new file mode 100644 index 00000000..8ad8f03f --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java @@ -0,0 +1,42 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +/** + * The wrench, {@code MCH_ItemWrench}. Right-clicking a damaged vehicle repairs 10 hit points, once + * a second, at one point of the wrench's durability. Sneaking switches the vehicle to its next + * paint scheme. + * + *

The original repaired while the use key was held; a click per repair works the same with + * today's item use, which stops as soon as the cursor leaves the hull. + */ +public class WrenchItem extends Item { + /** An iron tool's durability, the original's tool material. */ + private static final int DURABILITY = 250; + private static final int REPAIR = 10; + private static final int COOLDOWN = 20; + + public WrenchItem(Properties properties) { + super(properties.durability(DURABILITY)); + } + + /** Server only. */ + public static InteractionResult useOn(ItemStack stack, AircraftEntity vehicle, Player player, InteractionHand hand) { + if (player.isSecondaryUseActive()) { + return vehicle.nextTexture() ? InteractionResult.SUCCESS : InteractionResult.FAIL; + } + if (player.getCooldowns().isOnCooldown(stack) || !vehicle.repair(REPAIR)) { + return InteractionResult.FAIL; + } + player.getCooldowns().addCooldown(stack, COOLDOWN); + stack.hurtAndBreak(1, player, hand); + ModSounds.playAt(vehicle, "wrench", 1.0F, 0.9F + vehicle.getRandom().nextFloat() * 0.2F); + return InteractionResult.SUCCESS; + } +} From 43472ae3ed74fd4e8c9f3a241cbe3da9e05ff949 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:37 +0800 Subject: [PATCH 0281/1461] Add menu/DraftingTableMenu.java to the 26.2 shared sources --- .../mcheli/menu/DraftingTableMenu.java | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java new file mode 100644 index 00000000..ec1b4d89 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java @@ -0,0 +1,148 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.ResultContainer; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; + +import java.util.List; + +/** + * The drafting table, {@code MCH_DraftingTableGuiContainer}: the player's inventory and one output + * slot. The screen browses the MCHeli recipes in the player's recipe book and asks for one by its + * display id; the server finds the recipe, takes the ingredients from the inventory and puts the + * result in the output slot. + */ +public class DraftingTableMenu extends AbstractContainerMenu { + public static final int OUTPUT_X = 178; + public static final int OUTPUT_Y = 90; + + private final ContainerLevelAccess access; + private final ResultContainer output = new ResultContainer(); + private final int outputSlot; + + /** The client side, from the open screen packet. */ + public DraftingTableMenu(int containerId, Inventory inventory) { + this(containerId, inventory, ContainerLevelAccess.NULL); + } + + public DraftingTableMenu(int containerId, Inventory inventory, ContainerLevelAccess access) { + super(ModMenus.DRAFTING_TABLE.get(), containerId); + this.access = access; + addStandardInventorySlots(inventory, 30, 140); + outputSlot = slots.size(); + addSlot(new Slot(output, 0, OUTPUT_X, OUTPUT_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + } + + /** + * {@code createRecipeItem}: makes one of the recipe behind {@code id}, if it is ours, the output + * slot is free and the player carries the ingredients. Creative players need neither. + */ + public void craft(ServerPlayer player, RecipeDisplayId id) { + boolean creative = player.getAbilities().instabuild; + Slot slot = slots.get(outputSlot); + if (slot.hasItem() && !creative) { + return; + } + RecipeManager.ServerDisplayInfo info = player.level().getServer().getRecipeManager().getRecipeFromDisplay(id); + if (info == null) { + return; + } + RecipeHolder holder = info.parent(); + if (!holder.id().identifier().getNamespace().equals(MCHeli.MOD_ID) || !(holder.value() instanceof CraftingRecipe recipe)) { + return; + } + List ingredients = recipe.placementInfo().ingredients(); + if (!creative && !take(player.getInventory(), ingredients, true)) { + return; + } + if (!creative) { + take(player.getInventory(), ingredients, false); + } + slot.set(recipe.assemble(CraftingInput.EMPTY)); + broadcastChanges(); + } + + /** + * Finds, and unless {@code dryRun} removes, one item per ingredient. A stack serves several + * ingredients as far as its count goes. + */ + private static boolean take(Inventory inventory, List ingredients, boolean dryRun) { + int[] used = new int[inventory.getContainerSize()]; + for (Ingredient ingredient : ingredients) { + int found = -1; + for (int i = 0; i < inventory.getContainerSize() && found < 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && stack.getCount() > used[i] && ingredient.test(stack)) { + found = i; + } + } + if (found < 0) { + return false; + } + used[found]++; + } + if (!dryRun) { + for (int i = 0; i < used.length; i++) { + if (used[i] > 0) { + inventory.removeItem(i, used[i]); + } + } + } + return true; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(access, player, ModBlocks.DRAFTING_TABLE.get()); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + if (index != outputSlot) { + return ItemStack.EMPTY; + } + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (!moveItemStackTo(stack, 0, outputSlot, true)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** What is left in the output goes back to the player, as in the original. */ + @Override + public void removed(Player player) { + super.removed(player); + if (!player.level().isClientSide()) { + clearContainer(player, output); + } + } +} From 487e1f70681eb68d4a6bd73adb12da221b38e830 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:37 +0800 Subject: [PATCH 0282/1461] Add menu/UavStationMenu.java to the 26.2 shared sources --- .../mcheli/menu/UavStationMenu.java | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java new file mode 100644 index 00000000..4c5f7bd8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java @@ -0,0 +1,154 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_ContainerUavStation}: one slot for the drone to put up, the + * launch offset with its -10, -1, +1 and +10 buttons per axis, and Continue to take the last drone + * again. The player sits at the station while it is open, so the client finds it as their vehicle. + */ +public class UavStationMenu extends AbstractContainerMenu { + /** Button ids: axis times four plus the step, then Continue. */ + public static final int[] STEPS = {-10, -1, 1, 10}; + public static final int BUTTON_CONTINUE = 12; + + private static final int DATA_SIZE = 4; + private static final int DATA_CAN_CONTINUE = 3; + + private final @Nullable UavStationEntity station; + private final ContainerData data; + + /** The client side, from the open screen packet. */ + public UavStationMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof UavStationEntity s ? s : null, + new SimpleContainer(1), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public UavStationMenu(int containerId, Inventory inventory, UavStationEntity station) { + this(containerId, inventory, station, station.slot(), new ServerData(station)); + } + + private UavStationMenu(int containerId, Inventory inventory, @Nullable UavStationEntity station, Container slot, ContainerData data) { + super(ModMenus.UAV_STATION.get(), containerId); + this.station = station; + this.data = data; + checkContainerSize(slot, 1); + addSlot(new Slot(slot, 0, 20, 20) { + @Override + public boolean mayPlace(ItemStack stack) { + return AircraftItem.vehicleOf(stack) != null; + } + + @Override + public int getMaxStackSize() { + return 1; + } + }); + addStandardInventorySlots(inventory, 8, 84); + addDataSlots(data); + } + + public @Nullable UavStationEntity station() { + return station; + } + + /** The launch offset along {@code axis} (0 x, 1 y, 2 z), as the server last reported it. */ + public int offset(int axis) { + return (short) data.get(axis); + } + + public boolean canContinue() { + return data.get(DATA_CAN_CONTINUE) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (station == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CONTINUE) { + return station.continueControl(server); + } + if (id < 0 || id >= 12) { + return false; + } + int[] pos = {station.offset().getX(), station.offset().getY(), station.offset().getZ()}; + pos[id / 4] += STEPS[id % 4]; + station.setOffset(new BlockPos(pos[0], pos[1], pos[2])); + return true; + } + + @Override + public boolean stillValid(Player player) { + return station != null && station.isAlive() && player.getVehicle() == station; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (index == 0) { + if (!moveItemStackTo(stack, 1, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (AircraftItem.vehicleOf(stack) == null || !moveItemStackTo(stack, 0, 1, false)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** Whatever is left in the slot goes back to the player, as with a crafting table. */ + @Override + public void removed(Player player) { + super.removed(player); + if (station != null && !player.level().isClientSide()) { + clearContainer(player, station.slot()); + } + } + + private record ServerData(UavStationEntity station) implements ContainerData { + @Override + public int get(int index) { + BlockPos o = station.offset(); + return switch (index) { + case 0 -> o.getX(); + case 1 -> o.getY(); + case 2 -> o.getZ(); + default -> station.lastUav() != null ? 1 : 0; + }; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } +} From 83e37fd5b1138a2e10d63abb61a3582e5663008f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:38 +0800 Subject: [PATCH 0283/1461] Add menu/VehicleMenu.java to the 26.2 shared sources --- .../jnhprojects/mcheli/menu/VehicleMenu.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java new file mode 100644 index 00000000..b2dba1e4 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java @@ -0,0 +1,217 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.weapon.Resupply; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGuiContainer}: fuel slots, a parachute slot per ejection + * seat, reloading the weapons from the crew member's own items and the way into the cargo chest. + * + *

Only crew open it, so the client finds the vehicle as the one its player rides. The weapon + * rounds and which weapons can be reloaded right now come from the server through data slots, + * since a client only hears about the ammunition of the seat it sits in. + */ +public class VehicleMenu extends AbstractContainerMenu { + /** Weapons the screen can show; more than any vehicle in the original has. */ + public static final int MAX_WEAPONS = 32; + /** Button ids from zero up reload that weapon; this one opens the cargo chest. */ + public static final int BUTTON_CARGO = 1000; + + public static final int FUEL_X = 8; + public static final int FUEL_Y = 30; + public static final int PARACHUTE_X = 8; + public static final int PARACHUTE_Y = 66; + public static final int INVENTORY_Y = 102; + + private static final int DATA_RELOADABLE = MAX_WEAPONS; + private static final int DATA_SIZE = MAX_WEAPONS + 2; + + private final @Nullable AircraftEntity vehicle; + private final Container equipment; + private final ContainerData data; + private final int parachuteSlots; + private boolean supplied; + + /** The client side, from the open screen packet. */ + public VehicleMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof AircraftEntity v ? v : null, + new SimpleContainer(VehicleInventory.EQUIPMENT_SLOTS), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public VehicleMenu(int containerId, Inventory inventory, AircraftEntity vehicle) { + this(containerId, inventory, vehicle, vehicle.inventory().equipment(), new ServerData(vehicle, inventory.player)); + } + + private VehicleMenu(int containerId, Inventory inventory, @Nullable AircraftEntity vehicle, Container equipment, ContainerData data) { + super(ModMenus.VEHICLE.get(), containerId); + this.vehicle = vehicle; + this.equipment = equipment; + this.data = data; + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + parachuteSlots = def != null ? VehicleInventory.parachuteSlots(def) : 0; + checkContainerSize(equipment, VehicleInventory.EQUIPMENT_SLOTS); + + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + addSlot(new Slot(equipment, i, FUEL_X + i * 18, FUEL_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() instanceof FuelItem; + } + }); + } + for (int i = 0; i < VehicleInventory.PARACHUTE_SLOTS; i++) { + boolean active = i < parachuteSlots; + addSlot(new Slot(equipment, VehicleInventory.FUEL_SLOTS + i, PARACHUTE_X + i * 18, PARACHUTE_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return active && stack.getItem() instanceof ParachuteItem; + } + + @Override + public boolean isActive() { + return active; + } + }); + } + addStandardInventorySlots(inventory, 8, INVENTORY_Y); + addDataSlots(data); + } + + public @Nullable AircraftEntity vehicle() { + return vehicle; + } + + /** Parachute slots in use: one per ejection seat. */ + public int parachuteSlots() { + return parachuteSlots; + } + + /** Magazine and reserve of a weapon together, as the server last reported them. */ + public int rounds(int weapon) { + return weapon >= 0 && weapon < MAX_WEAPONS ? data.get(weapon) : 0; + } + + /** Whether the server would reload this weapon now: the vehicle is down and the items are there. */ + public boolean canReload(int weapon) { + if (weapon < 0 || weapon >= MAX_WEAPONS) { + return false; + } + int bits = (data.get(DATA_RELOADABLE) & 0xFFFF) | (data.get(DATA_RELOADABLE + 1) & 0xFFFF) << 16; + return (bits & 1 << weapon) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (vehicle == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CARGO) { + return vehicle.openCargo(server); + } + if (id >= 0 && id < MAX_WEAPONS && Resupply.supply(server, vehicle, id)) { + supplied = true; + return true; + } + return false; + } + + @Override + public boolean stillValid(Player player) { + return vehicle != null && equipment.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + int equipmentEnd = VehicleInventory.EQUIPMENT_SLOTS; + if (index < equipmentEnd) { + if (!moveItemStackTo(stack, equipmentEnd, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof FuelItem) { + if (!moveItemStackTo(stack, 0, VehicleInventory.FUEL_SLOTS, false)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof ParachuteItem && parachuteSlots > 0) { + int first = VehicleInventory.FUEL_SLOTS; + if (!moveItemStackTo(stack, first, first + parachuteSlots, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** As in the original, closing the screen after a resupply loads every magazine from its reserve. */ + @Override + public void removed(Player player) { + super.removed(player); + if (supplied && vehicle != null && vehicle.weapons() != null && !player.level().isClientSide()) { + vehicle.weapons().reloadAll(); + } + } + + /** Rounds per weapon and the reload bits, worked out on the server whenever the menu syncs. */ + private record ServerData(AircraftEntity vehicle, Player player) implements ContainerData { + @Override + public int get(int index) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons == null) { + return 0; + } + if (index < MAX_WEAPONS) { + WeaponSet set = weapons.set(index); + // Data slots travel as shorts. + return set != null ? Math.min(set.ammo() + set.restAmmo(), Short.MAX_VALUE) : 0; + } + int bits = 0; + if (player instanceof ServerPlayer server) { + for (int i = 0; i < Math.min(weapons.sets().size(), MAX_WEAPONS); i++) { + if (Resupply.canSupply(server, vehicle, i)) { + bits |= 1 << i; + } + } + } + return index == DATA_RELOADABLE ? bits & 0xFFFF : bits >>> 16; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } +} From 05543b66f7b361a2e15227ced46a104f138e0430 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:38 +0800 Subject: [PATCH 0284/1461] Add network/BoardVehiclePacket.java to the 26.2 shared sources --- .../mcheli/network/BoardVehiclePacket.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java new file mode 100644 index 00000000..ba56ca3e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.Vec3; + +/** + * Client to server: the player right-clicked a vehicle's hull outside its main hit box. The + * server repeats the reach check against the hull boxes, then treats it as an ordinary click. + * + * @param location where on the hull the click landed, in world coordinates + */ +public record BoardVehiclePacket(int entityId, Vec3 location) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("board_vehicle"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(BoardVehiclePacket::encode, BoardVehiclePacket::decode); + + /** The same slack vanilla allows on top of the player's reach for a click on an entity. */ + private static final double REACH_SLACK = 3.0; + + public static void encode(BoardVehiclePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeDouble(msg.location.x); + buf.writeDouble(msg.location.y); + buf.writeDouble(msg.location.z); + } + + public static BoardVehiclePacket decode(FriendlyByteBuf buf) { + return new BoardVehiclePacket(buf.readVarInt(), new Vec3(buf.readDouble(), buf.readDouble(), buf.readDouble())); + } + + public static void handle(BoardVehiclePacket msg, ServerPlayer sender) { + if (sender == null || !(sender.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) || vehicle.isRemoved()) { + return; + } + Vec3 eye = sender.getEyePosition(); + double reach = sender.entityInteractionRange() + REACH_SLACK; + // The click must land on the hull, and the hull must be within reach. + if (eye.distanceToSqr(msg.location) > reach * reach) { + return; + } + Vec3 through = eye.add(msg.location.subtract(eye).scale(1.05)); + boolean onHull = vehicle.clipHitBoxes(eye, through, 0.5).isPresent(); + vehicle.consumeHitBoxFactor(); + if (onHull) { + vehicle.interact(sender, InteractionHand.MAIN_HAND, msg.location.subtract(vehicle.position())); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 6d64d100c7ddf102144d7c3513ee74ee248e61c5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:39 +0800 Subject: [PATCH 0285/1461] Add network/CrewActionPacket.java to the 26.2 shared sources --- .../mcheli/network/CrewActionPacket.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java new file mode 100644 index 00000000..1b565e21 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java @@ -0,0 +1,164 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A one-off request from a crew member: lower the gear, fold the wings, change seats and so on. + * + *

The client only asks; the server checks that the sender sits in that vehicle, in a seat + * allowed to do it, and then applies the same conditions the original checked before switching. + * The result reaches every client through synched data. + */ +public record CrewActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("crew_action"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(CrewActionPacket::encode, CrewActionPacket::decode); + + public enum Action { + /** Raise or lower the landing gear. Pilot only. */ + LANDING_GEAR, + /** Fold or unfold hatches, wings or swept wings. Pilot only. */ + FOLD, + /** Tilt the nozzles for vertical flight. Pilot only. */ + VTOL, + /** Eject from seat zero or one. */ + EJECT, + /** Move to the next free crew seat. */ + SWITCH_SEAT, + /** Load nearby cargo onto the racks, or drive onto a carrier's rack. Pilot only. */ + RACK_LOAD, + /** Drop the last cargo, or leave the rack this vehicle rides on. Pilot only. */ + RACK_UNLOAD, + /** Select the next weapon this seat may use. */ + NEXT_WEAPON, + /** Select the previous weapon this seat may use. */ + PREVIOUS_WEAPON, + /** Cycle the fire mode of the selected weapon. */ + WEAPON_MODE, + /** Release flares. Pilot and first crew seat. */ + FLARE, + /** Open the vehicle screen: fuel and parachute slots, reloading, the way to the cargo chest. */ + VEHICLE_SCREEN, + /** Hand the controls to the autopilot and man the weapons, or take them back. Pilot only. */ + GUNNER_MODE, + /** Turn night vision on for the sender, in vehicles with {@code EnableNightVision}. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF, + /** Let the troops down: ropes from a hover, a parachute drop, or out on the ground. Pilot only. */ + DROP_CREW, + /** Slide down a rope or jump with a parachute. Crew other than the pilot. */ + JUMP_OUT, + /** Turn the search light on or off. Pilot and first crew seat. */ + SEARCH_LIGHT + } + + public static void encode(CrewActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static CrewActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown crew action " + ordinal); + } + return new CrewActionPacket(entityId, actions[ordinal]); + } + + public static void handle(CrewActionPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + boolean pilot = vehicle.getControllingPassenger() == sender; + switch (msg.action) { + case LANDING_GEAR -> { + if (pilot) { + vehicle.toggleLandingGear(); + } + } + case FOLD -> { + if (pilot) { + vehicle.toggleFold(); + } + } + case VTOL -> { + if (pilot && vehicle instanceof PlaneEntity plane) { + plane.toggleVtol(); + } + } + case GUNNER_MODE -> { + if (pilot) { + vehicle.setGunnerMode(!vehicle.isGunnerMode()); + } + } + case EJECT -> vehicle.eject(sender); + case SWITCH_SEAT -> vehicle.switchSeat(sender); + case RACK_LOAD -> { + if (pilot) { + if (!vehicle.boardNearbyRack()) { + vehicle.loadNearbyCargo(); + } + } + } + case RACK_UNLOAD -> { + if (pilot) { + if (vehicle.isPassenger()) { + vehicle.leaveRack(); + } else { + vehicle.unloadCargo(); + } + } + } + case NEXT_WEAPON, PREVIOUS_WEAPON -> { + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons != null && !vehicle.isDestroyed()) { + int next = weapons.next(seat, msg.action == Action.NEXT_WEAPON ? 1 : -1); + if (next >= 0) { + weapons.switchTo(sender, seat, next); + } + } + } + case WEAPON_MODE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.switchMode(vehicle.seatOf(sender)); + } + } + case FLARE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null && vehicle.seatOf(sender) <= 1) { + weapons.flares.use(); + } + } + case VEHICLE_SCREEN -> vehicle.openScreen(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> vehicle.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + case DROP_CREW -> { + if (pilot && !vehicle.dropCrew()) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + case SEARCH_LIGHT -> vehicle.toggleSearchLight(sender); + case JUMP_OUT -> { + if (!pilot && !vehicle.jumpOut(sender)) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 91386881ad5a4b9823171eb33ee1841e755ae979 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:39 +0800 Subject: [PATCH 0286/1461] Add network/DraftingTablePacket.java to the 26.2 shared sources --- .../mcheli/network/DraftingTablePacket.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java new file mode 100644 index 00000000..46294aa3 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; + +/** + * The Create button of the drafting table, {@code MCH_DraftingTableCreatePacket}: make one of the + * recipe the player's recipe book knows by this display id. + */ +public record DraftingTablePacket(RecipeDisplayId recipe) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("drafting_table"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(DraftingTablePacket::encode, DraftingTablePacket::decode); + + public static void encode(DraftingTablePacket msg, FriendlyByteBuf buf) { + RecipeDisplayId.STREAM_CODEC.encode(buf, msg.recipe); + } + + public static DraftingTablePacket decode(FriendlyByteBuf buf) { + return new DraftingTablePacket(RecipeDisplayId.STREAM_CODEC.decode(buf)); + } + + public static void handle(DraftingTablePacket msg, ServerPlayer sender) { + if (sender != null && sender.containerMenu instanceof DraftingTableMenu menu && menu.stillValid(sender)) { + menu.craft(sender, msg.recipe); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 43dc9c98bdbe9884722611b7135b6c3d8b78809b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:40 +0800 Subject: [PATCH 0287/1461] Add network/EquipmentActionPacket.java to the 26.2 shared sources --- .../mcheli/network/EquipmentActionPacket.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java new file mode 100644 index 00000000..76090f2e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A request from a player using a piece of equipment they sit at, such as the GLTD. Like + * {@link CrewActionPacket} the client only asks; the equipment checks that the sender is the one + * using it. + */ +public record EquipmentActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("equipment_action"); + public static final StreamCodec STREAM_CODEC = + StreamCodec.ofMember(EquipmentActionPacket::encode, EquipmentActionPacket::decode); + + public enum Action { + /** Mark the spot under the sight and call in the A-10. */ + CALL_STRIKE, + /** Turn night vision on for the sender. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF + } + + public static void encode(EquipmentActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static EquipmentActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown equipment action " + ordinal); + } + return new EquipmentActionPacket(entityId, actions[ordinal]); + } + + public static void handle(EquipmentActionPacket msg, ServerPlayer sender) { + if (sender == null || sender.getVehicle() == null || sender.getVehicle().getId() != msg.entityId) { + return; + } + if (sender.getVehicle() instanceof GltdEntity gltd) { + switch (msg.action) { + case CALL_STRIKE -> gltd.callStrike(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> gltd.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 931b96b8bf52c136d8b738241bf244040fe35f93 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:40 +0800 Subject: [PATCH 0288/1461] Add network/ExplosionEffectPacket.java to the 26.2 shared sources --- .../mcheli/network/ExplosionEffectPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java new file mode 100644 index 00000000..279b1511 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.ExplosionEffect; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to nearby players: draw an MCHeli explosion here. The original's + * {@code MCH_PacketEffectExplosion}, plus a flag for the bigger fireball of a crashing vehicle. + */ +public record ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater, boolean crash) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("explosion_effect"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(ExplosionEffectPacket::encode, ExplosionEffectPacket::decode); + + public ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater) { + this(x, y, z, size, inWater, false); + } + + public static void encode(ExplosionEffectPacket msg, FriendlyByteBuf buf) { + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.size); + buf.writeBoolean(msg.inWater); + buf.writeBoolean(msg.crash); + } + + public static ExplosionEffectPacket decode(FriendlyByteBuf buf) { + return new ExplosionEffectPacket(buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readBoolean(), + buf.readBoolean()); + } + + public static void handle(ExplosionEffectPacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player == null || !Float.isFinite(msg.size)) { + return; + } + float size = Math.min(msg.size, 50.0F); + if (msg.crash) { + ExplosionEffect.crash(player.level(), msg.x, msg.y, msg.z, Math.min(size, ExplosionEffect.MAX_CRASH_SIZE)); + } else { + ExplosionEffect.play(player.level(), msg.x, msg.y, msg.z, size, msg.inWater); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 8a9ec9cf7173f2eb33491fa042489c1a7f35e24f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:40 +0800 Subject: [PATCH 0289/1461] Add network/FireWeaponPacket.java to the 26.2 shared sources --- .../mcheli/network/FireWeaponPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java new file mode 100644 index 00000000..0056dcc0 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.ServerFiring; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Client to server: the crew member's selected weapon fired on their side, so fire it for real. + * The client already checked readiness, ammunition and lock; the server checks again with its own + * timers and only trusts the two option values, a locked entity and a fire mode. + * + * @param weapon the weapon the client fired, ignored unless it is still the seat's selection + */ +public record FireWeaponPacket(int entityId, int weapon, int option1, int option2) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("fire_weapon"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(FireWeaponPacket::encode, FireWeaponPacket::decode); + + public static void encode(FireWeaponPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.option1); + buf.writeVarInt(msg.option2); + } + + public static FireWeaponPacket decode(FriendlyByteBuf buf) { + return new FireWeaponPacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(FireWeaponPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons == null || weapons.selected(seat) != msg.weapon || weapons.operatorSeat(msg.weapon) != seat) { + return; + } + Shot shot = new Shot(vehicle, sender); + shot.option1 = msg.option1; + shot.option2 = msg.option2; + weapons.use(sender, seat, shot, ServerFiring.INSTANCE); + } + + @Override + public Type type() { + return TYPE; + } +} From 22f2dbf21c7a0b03fc511a9155fac30bd4a63c85 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:41 +0800 Subject: [PATCH 0290/1461] Add network/LauncherPacket.java to the 26.2 shared sources --- .../mcheli/network/LauncherPacket.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java new file mode 100644 index 00000000..d3f204c7 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java @@ -0,0 +1,119 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +/** + * The player with a launcher in hand, {@code MCH_PacketLightWeaponPlayerControl}: fire at the + * locked target, or finish a reload. The client locks and times the reload; the server checks + * the launcher, the target and the missile in the inventory. + * + * @param target the locked entity, for {@link Action#FIRE} + * @param mode the Javelin's attack mode: 0 direct, 1 top attack + */ +public record LauncherPacket(Action action, int target, int mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("launcher"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LauncherPacket::encode, LauncherPacket::decode); + + /** Locks further than this are refused, with some slack over the lock range for lag. */ + private static final double MAX_TARGET_DISTANCE = 260.0; + + public enum Action { + FIRE, + RELOAD + } + + public static void encode(LauncherPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeVarInt(msg.target); + buf.writeVarInt(msg.mode); + } + + public static LauncherPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown launcher action " + ordinal); + } + return new LauncherPacket(actions[ordinal], buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(LauncherPacket msg, ServerPlayer sender) { + ItemStack stack = sender == null ? ItemStack.EMPTY : sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof LauncherItem launcher) || !LauncherItem.canUse(sender)) { + return; + } + switch (msg.action) { + case FIRE -> fire(sender, stack, launcher, msg.target, msg.mode); + case RELOAD -> reload(sender, stack, launcher); + } + } + + private static void fire(ServerPlayer player, ItemStack stack, LauncherItem launcher, int targetId, int mode) { + WeaponDefinition def = MCHeli.content().weapon(launcher.weapon()); + ServerLevel level = player.level(); + Entity target = level.getEntity(targetId); + if (def == null || !LauncherItem.isLoaded(stack) || target == null || target.isRemoved() + || target.distanceToSqr(player) > MAX_TARGET_DISTANCE * MAX_TARGET_DISTANCE) { + return; + } + ProjectileEntity missile = def.type == WeaponType.AT_MISSILE + ? ModEntities.AT_MISSILE.get().create(level, EntitySpawnReason.TRIGGERED) + : ModEntities.AA_MISSILE.get().create(level, EntitySpawnReason.TRIGGERED); + if (missile == null) { + return; + } + float yaw = player.getYRot(); + float pitch = player.getXRot(); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + Vec3 at = player.getEyePosition().add(dir); + missile.launch(at.x, at.y, at.z, dir.x, dir.y, dir.z, yaw, pitch, def.acceleration); + missile.setWeapon(launcher.weapon()); + missile.setParameters(def, player, player); + missile.setTarget(target); + if (missile instanceof ATMissileEntity atMissile) { + atMissile.guidanceType = Math.clamp(mode, 0, Math.max(def.modeNum - 1, 0)); + } + level.addFreshEntity(missile); + if (def.sound != null) { + ModSounds.playAt(player, def.sound, def.soundVolume, def.soundPitch); + } + LauncherItem.setLoaded(stack, false); + } + + private static void reload(ServerPlayer player, ItemStack stack, LauncherItem launcher) { + if (LauncherItem.isLoaded(stack)) { + return; + } + if (!player.getAbilities().instabuild) { + int slot = player.getInventory().findSlotMatchingItem(new ItemStack(launcher.missile())); + if (slot < 0) { + return; + } + player.getInventory().removeItem(slot, 1); + } + LauncherItem.setLoaded(stack, true); + } + + @Override + public Type type() { + return TYPE; + } +} From cbf5f2101e632bd511fd5bc35b1c7e6a870847e6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:41 +0800 Subject: [PATCH 0291/1461] Add network/LockWarningPacket.java to the 26.2 shared sources --- .../mcheli/network/LockWarningPacket.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java new file mode 100644 index 00000000..97d2cbf4 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java @@ -0,0 +1,44 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +/** + * Client to server: this player is locking on to an entity. If it is a vehicle with flares, or a + * crew member of one, its crew hears the warning. The original's {@code MCH_PacketNotifyLock}. + */ +public record LockWarningPacket(int targetId) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("lock_warning"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LockWarningPacket::encode, LockWarningPacket::decode); + + public static void encode(LockWarningPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.targetId); + } + + public static LockWarningPacket decode(FriendlyByteBuf buf) { + return new LockWarningPacket(buf.readVarInt()); + } + + public static void handle(LockWarningPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity)) { + return; + } + Entity target = sender.level().getEntity(msg.targetId); + if (target != null && target.distanceToSqr(sender) < 400.0 * 400.0) { + AircraftEntity vehicle = target instanceof AircraftEntity a ? a : target.getVehicle() instanceof AircraftEntity a ? a : null; + if (vehicle != null) { + vehicle.warnLocked(); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From fc320d60ed39c7c4f5618b44d54916e3ea10f997 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:42 +0800 Subject: [PATCH 0292/1461] Add network/MarkerPacket.java to the 26.2 shared sources --- .../mcheli/network/MarkerPacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java new file mode 100644 index 00000000..1de0bfc3 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to client: the team spotted these entities for {@code ticks}, or marked {@code point}, + * {@code MCH_PacketNotifySpotedEntity} and {@code MCH_PacketNotifyMarkPoint}. Only the spotter and + * their teammates get it. + */ +public record MarkerPacket(int[] entities, int ticks, BlockPos point) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("marker"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(MarkerPacket::encode, MarkerPacket::decode); + /** Spotting sends no point; this stands in for none. */ + private static final BlockPos NO_POINT = new BlockPos(0, Integer.MIN_VALUE, 0); + private static final int MAX_ENTITIES = 256; + + public static MarkerPacket spotted(int[] entities, int ticks) { + return new MarkerPacket(entities, ticks, NO_POINT); + } + + public static MarkerPacket point(BlockPos point) { + return new MarkerPacket(new int[0], 0, point); + } + + public static void encode(MarkerPacket msg, FriendlyByteBuf buf) { + buf.writeVarIntArray(msg.entities); + buf.writeVarInt(msg.ticks); + buf.writeBlockPos(msg.point); + } + + public static MarkerPacket decode(FriendlyByteBuf buf) { + return new MarkerPacket(buf.readVarIntArray(MAX_ENTITIES), buf.readVarInt(), buf.readBlockPos()); + } + + public static void handle(MarkerPacket msg) { + if (msg.entities.length > 0) { + Markers.spotted(msg.entities, msg.ticks); + } + if (!msg.point.equals(NO_POINT)) { + Markers.point(msg.point); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 762bff3ceec412406306df67332ee438026f1b64 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:42 +0800 Subject: [PATCH 0293/1461] Add network/ModNetwork.java to the 26.2 shared sources --- .../mcheli/network/ModNetwork.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java new file mode 100644 index 00000000..26065705 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java @@ -0,0 +1,90 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Our packets. Each one is a custom payload with its own id and the same encoding on every loader, + * so a client on one loader can talk to a server on another. The loader module registers the + * lists below with its own networking API; handlers always run on the main thread. + */ +public final class ModNetwork { + /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ + public static final int PROTOCOL_VERSION = 11; + + /** Client to server. The handler gets the sending player. */ + public static final List> SERVERBOUND = List.of( + new Serverbound<>(PilotStatePacket.TYPE, PilotStatePacket.STREAM_CODEC, PilotStatePacket::handle), + new Serverbound<>(CrewActionPacket.TYPE, CrewActionPacket.STREAM_CODEC, CrewActionPacket::handle), + new Serverbound<>(EquipmentActionPacket.TYPE, EquipmentActionPacket.STREAM_CODEC, EquipmentActionPacket::handle), + new Serverbound<>(SpotPacket.TYPE, SpotPacket.STREAM_CODEC, SpotPacket::handle), + new Serverbound<>(LauncherPacket.TYPE, LauncherPacket.STREAM_CODEC, LauncherPacket::handle), + new Serverbound<>(DraftingTablePacket.TYPE, DraftingTablePacket.STREAM_CODEC, DraftingTablePacket::handle), + new Serverbound<>(TeamPacket.TYPE, TeamPacket.STREAM_CODEC, TeamPacket::handle), + new Serverbound<>(TestFlightPacket.TYPE, TestFlightPacket.STREAM_CODEC, TestFlightPacket::handle), + new Serverbound<>(FireWeaponPacket.TYPE, FireWeaponPacket.STREAM_CODEC, FireWeaponPacket::handle), + new Serverbound<>(BoardVehiclePacket.TYPE, BoardVehiclePacket.STREAM_CODEC, BoardVehiclePacket::handle), + new Serverbound<>(LockWarningPacket.TYPE, LockWarningPacket.STREAM_CODEC, LockWarningPacket::handle), + new Serverbound<>(PackRequestPacket.TYPE, PackRequestPacket.STREAM_CODEC, PackRequestPacket::handle), + new Serverbound<>(PackSubmitPacket.TYPE, PackSubmitPacket.STREAM_CODEC, PackSubmitPacket::handle)); + + /** Server to client. Handlers touch client classes, so they only ever run on a client. */ + public static final List> CLIENTBOUND = List.of( + new Clientbound<>(WeaponStatePacket.TYPE, WeaponStatePacket.STREAM_CODEC, WeaponStatePacket::handle), + new Clientbound<>(ExplosionEffectPacket.TYPE, ExplosionEffectPacket.STREAM_CODEC, ExplosionEffectPacket::handle), + new Clientbound<>(PackListPacket.TYPE, PackListPacket.STREAM_CODEC, PackListPacket::handle), + new Clientbound<>(PackDataPacket.TYPE, PackDataPacket.STREAM_CODEC, PackDataPacket::handle), + new Clientbound<>(PackSoundPacket.TYPE, PackSoundPacket.STREAM_CODEC, PackSoundPacket::handle), + new Clientbound<>(MarkerPacket.TYPE, MarkerPacket.STREAM_CODEC, MarkerPacket::handle)); + + private ModNetwork() { + } + + static CustomPacketPayload.Type type(String name) { + return new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, name)); + } + + public static void sendToServer(CustomPacketPayload message) { + Platform.get().sendToServer(message); + } + + public static void sendToPlayer(ServerPlayer player, CustomPacketPayload message) { + Platform.get().sendToPlayer(player, message); + } + + public static void sendToAll(MinecraftServer server, CustomPacketPayload message) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + sendToPlayer(player, message); + } + } + + /** Sends to every player in {@code level} within {@code range} blocks of the point. */ + public static void sendNear(ServerLevel level, double x, double y, double z, double range, CustomPacketPayload message) { + for (ServerPlayer player : level.players()) { + if (player.distanceToSqr(x, y, z) < range * range) { + sendToPlayer(player, message); + } + } + } + + public record Serverbound(CustomPacketPayload.Type type, + StreamCodec codec, + BiConsumer handler) { + } + + public record Clientbound(CustomPacketPayload.Type type, + StreamCodec codec, + Consumer handler) { + } +} From f49ccb7627334e955dcc4afb0014f53cea547e0d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:43 +0800 Subject: [PATCH 0294/1461] Add network/PackDataPacket.java to the 26.2 shared sources --- .../mcheli/network/PackDataPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java new file mode 100644 index 00000000..89c1644d --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: one chunk of a pack's zip. Chunks of a pack arrive in order, so + * {@code offset} only lets the client check that nothing went missing. + * + * @param total length of the whole zip, as in the {@link PackListPacket} + */ +public record PackDataPacket(String hash, int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_data"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackDataPacket::encode, PackDataPacket::decode); + + /** Well under the 1 MiB limit of a clientbound custom payload. */ + public static final int CHUNK_SIZE = 128 * 1024; + + public static void encode(PackDataPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.hash, 64); + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackDataPacket decode(FriendlyByteBuf buf) { + return new PackDataPacket(buf.readUtf(64), buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackDataPacket msg) { + ClientAccess.packs().onPackData(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From b5b7409f57dbc6fadd227dbfa5d6b63c712c1c32 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:43 +0800 Subject: [PATCH 0295/1461] Add network/PackListPacket.java to the 26.2 shared sources --- .../mcheli/network/PackListPacket.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java new file mode 100644 index 00000000..ad7f0c85 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +import java.util.ArrayList; +import java.util.List; + +/** + * Server to player: the content packs the server uses, in load order. Sent on join and after + * {@code /mcheli reload}. The client requests the packs it has not cached ({@link PackRequestPacket}) + * and then rebuilds its content from exactly this list, so both sides fly the same definitions. + * The imported 1.0.3 content is never listed: every player imports their own copy. + */ +public record PackListPacket(List packs) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_list"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackListPacket::encode, PackListPacket::decode); + + public static final int MAX_PACKS = 256; + + /** + * @param size length of the pack's zip in bytes, which is what {@link PackDataPacket} sends + */ + public record Entry(String id, String version, String hash, int size) { + } + + public static void encode(PackListPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.packs.size()); + for (Entry entry : msg.packs) { + buf.writeUtf(entry.id, 64); + buf.writeUtf(entry.version, 32); + buf.writeUtf(entry.hash, 64); + buf.writeVarInt(entry.size); + } + } + + public static PackListPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > MAX_PACKS) { + throw new DecoderException("Too many content packs: " + count); + } + List packs = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + packs.add(new Entry(buf.readUtf(64), buf.readUtf(32), buf.readUtf(64), buf.readVarInt())); + } + return new PackListPacket(List.copyOf(packs)); + } + + public static void handle(PackListPacket msg) { + ClientAccess.packs().onPackList(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 3ca528ce15b5e4f30a54b9fafc4f4f1ae70af721 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:44 +0800 Subject: [PATCH 0296/1461] Add network/PackRequestPacket.java to the 26.2 shared sources --- .../mcheli/network/PackRequestPacket.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java new file mode 100644 index 00000000..50f10f74 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.network; + +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Player to server: send me these packs, by content hash. The server only answers for hashes in + * the list it sent, so a client cannot ask for anything else. + */ +public record PackRequestPacket(List hashes) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_request"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackRequestPacket::encode, PackRequestPacket::decode); + + public static void encode(PackRequestPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.hashes.size()); + for (String hash : msg.hashes) { + buf.writeUtf(hash, 64); + } + } + + public static PackRequestPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > PackListPacket.MAX_PACKS) { + throw new DecoderException("Too many content packs requested: " + count); + } + List hashes = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + hashes.add(buf.readUtf(64)); + } + return new PackRequestPacket(List.copyOf(hashes)); + } + + public static void handle(PackRequestPacket msg, ServerPlayer sender) { + if (sender != null) { + PackSync.request(sender, msg.hashes); + } + } + + @Override + public Type type() { + return TYPE; + } +} From a1381500b872451586d1bc67bfab4e32107158b8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:44 +0800 Subject: [PATCH 0297/1461] Add network/PackSoundPacket.java to the 26.2 shared sources --- .../mcheli/network/PackSoundPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java new file mode 100644 index 00000000..8bac5034 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: play a sound a content pack brought, once, at a point. The game's own sound + * packet ends at the sound manager, which only knows the sounds in {@code sounds.json}, and a pack + * has none: it would be silent. See {@code ModSounds}. + */ +public record PackSoundPacket(String sound, double x, double y, double z, float volume, float pitch) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_sound"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSoundPacket::encode, PackSoundPacket::decode); + + private static final int MAX_NAME = 256; + + public static void encode(PackSoundPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.sound, MAX_NAME); + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.volume); + buf.writeFloat(msg.pitch); + } + + public static PackSoundPacket decode(FriendlyByteBuf buf) { + return new PackSoundPacket(buf.readUtf(MAX_NAME), buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readFloat()); + } + + public static void handle(PackSoundPacket msg) { + ClientAccess.packs().onPackSound(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 3bc93cdaad9a028b063d50b8c21921f5ff59c1d2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:45 +0800 Subject: [PATCH 0298/1461] Add network/PackSubmissions.java to the 26.2 shared sources --- .../mcheli/network/PackSubmissions.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java new file mode 100644 index 00000000..b25eba0a --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java @@ -0,0 +1,185 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Util; +import org.jspecify.annotations.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Players offering a server the vehicles they built, for its operators to approve. + *

+ *  Submit in the workshop   ── PackSubmitPacket(chunk) ──▶  until the whole zip is here
+ *  checked off the main thread, kept in mcheli/pending, operators told
+ *  /mcheli approve <pack>    moves it to mcheli/packs and reloads, so every player gets it
+ * 
+ * Nothing a client sends is trusted. Submissions are off until the server's owner turns them on, one + * zip can be no larger than the server allows for a pack, a player sends one at a time with a pause + * in between, chunks out of order end the transfer, and a few transfers run at once at most. The + * bytes stay in memory only while they arrive; {@link PendingPacks} checks them as it would any pack. + * + *

Everything here runs on the server thread except the check, which reads the whole pack. + */ +public final class PackSubmissions { + private static final int MAX_UPLOADS = 4; + private static final long COOLDOWN_MILLIS = 30_000L; + + private static final Map UPLOADS = new HashMap<>(); + private static final Map LAST_SUBMISSION = new HashMap<>(); + + private PackSubmissions() { + } + + static void receive(ServerPlayer player, PackSubmitPacket packet) { + UUID id = player.getUUID(); + if (packet.offset() == 0) { + UPLOADS.remove(id); + Component refusal = refusal(player, packet.total()); + if (refusal != null) { + player.sendSystemMessage(refusal); + return; + } + UPLOADS.put(id, new ByteArrayOutputStream()); + } + ByteArrayOutputStream upload = UPLOADS.get(id); + // No upload means it was refused above and the rest of its chunks are still on their way. + if (upload == null) { + return; + } + long end = (long) upload.size() + packet.data().length; + if (packet.offset() != upload.size() || packet.total() > ServerConfig.maxPackBytes() || end > packet.total()) { + UPLOADS.remove(id); + player.sendSystemMessage(Component.translatable("message.mcheli.submit.failed", "chunks arrived out of order")); + return; + } + upload.write(packet.data(), 0, packet.data().length); + if (end == packet.total()) { + UPLOADS.remove(id); + LAST_SUBMISSION.put(id, System.currentTimeMillis()); + check(player, upload.toByteArray()); + } + } + + private static @Nullable Component refusal(ServerPlayer player, int total) { + if (!ServerConfig.allowSubmissions()) { + return Component.translatable("message.mcheli.submit.off"); + } + if (total <= 0 || total > ServerConfig.maxPackBytes()) { + return Component.translatable("message.mcheli.submit.too_large", ServerConfig.maxPackBytes() >> 20); + } + if (UPLOADS.size() >= MAX_UPLOADS) { + return Component.translatable("message.mcheli.submit.busy"); + } + long now = System.currentTimeMillis(); + LAST_SUBMISSION.values().removeIf(time -> now - time >= COOLDOWN_MILLIS); + return LAST_SUBMISSION.containsKey(player.getUUID()) ? Component.translatable("message.mcheli.submit.busy") : null; + } + + private static void check(ServerPlayer player, byte[] zip) { + MinecraftServer server = player.level().getServer(); + UUID id = player.getUUID(); + String name = player.getScoreboardName(); + CompletableFuture.supplyAsync(() -> { + try { + return MCHeli.packs().pending().store(zip, name, id); + } catch (IOException | PackException | RuntimeException e) { + throw new java.util.concurrent.CompletionException(e); + } + }, Util.nonCriticalIoPool()).whenComplete((submission, failure) -> server.execute(() -> { + if (failure != null) { + Throwable cause = failure.getCause() != null ? failure.getCause() : failure; + MCHeli.LOGGER.info("{} sent a pack that was refused: {}", name, cause.toString()); + tell(server, id, Component.translatable("message.mcheli.submit.failed", cause.getMessage() != null ? cause.getMessage() : cause.toString())); + } else { + arrived(server, submission); + } + })); + } + + private static void arrived(MinecraftServer server, PendingPacks.Submission submission) { + MCHeli.LOGGER.info("{} submitted content pack {} {} ({})", submission.submitter(), submission.id(), + submission.manifest().version(), ContentPack.shortHash(submission.hash())); + // Even with approval off, a pack taking over one that is in use waits for a person. + boolean replaces = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (!ServerConfig.requireApproval() && !replaces) { + try { + install(server, submission); + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not install submitted content pack {}", submission.id(), e); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.failed", e.getMessage())); + } + return; + } + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.waiting", submission.id())); + Component note = Component.translatable("message.mcheli.submit.to_operators", submission.submitter(), submission.id(), submission.manifest().version()); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (PackSync.seesProblems(player)) { + player.sendSystemMessage(note); + } + } + } + + /** + * Puts a waiting pack in use: into {@code mcheli/packs}, then a reload that sends every player + * the new list. + * + * @return whether the pack is in use now; the server's allow list or size limits may leave it out + */ + public static boolean install(MinecraftServer server, PendingPacks.Submission submission) throws IOException, PackException { + MCHeli.packs().pending().install(submission, MCHeli.packs().folder()); + ContentReload.reload(server); + MCHeli.LOGGER.info("Content pack {} from {} approved", submission.id(), submission.submitter()); + boolean live = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (live) { + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.approved", submission.id())); + } + return live; + } + + /** Throws a waiting pack away and tells the player who sent it, if they are here. */ + public static void reject(MinecraftServer server, PendingPacks.Submission submission) throws IOException { + MCHeli.packs().pending().remove(submission); + MCHeli.LOGGER.info("Content pack {} from {} rejected", submission.id(), submission.submitter()); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.rejected", submission.id())); + } + + private static void tell(MinecraftServer server, UUID player, Component message) { + ServerPlayer online = server.getPlayerList().getPlayer(player); + if (online != null) { + online.sendSystemMessage(message); + } + } + + /** An operator joining hears what is waiting for them. */ + static void onLogin(ServerPlayer player) { + if (!PackSync.seesProblems(player)) { + return; + } + try { + List waiting = MCHeli.packs().pending().list(); + if (!waiting.isEmpty()) { + player.sendSystemMessage(Component.translatable("message.mcheli.submit.waiting_for_you", waiting.size())); + } + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not list the submitted content packs", e); + } + } + + static void onLogout(ServerPlayer player) { + UPLOADS.remove(player.getUUID()); + } +} From 9a0c8a775a114fccbd353abb03a4289b0a5b8950 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:45 +0800 Subject: [PATCH 0299/1461] Add network/PackSubmitPacket.java to the 26.2 shared sources --- .../mcheli/network/PackSubmitPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java new file mode 100644 index 00000000..d142e700 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Player to server: one chunk of a pack the player made, offered to the server's operators. See + * {@link PackSubmissions}, which treats all of this as untrusted. + * + * @param offset where this chunk goes; 0 starts a new submission + * @param total length of the whole zip + */ +public record PackSubmitPacket(int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_submit"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSubmitPacket::encode, PackSubmitPacket::decode); + + /** Under the 32 KiB limit of a serverbound custom payload, which is far lower than the other way. */ + public static final int CHUNK_SIZE = 30 * 1024; + + public static void encode(PackSubmitPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackSubmitPacket decode(FriendlyByteBuf buf) { + return new PackSubmitPacket(buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackSubmitPacket msg, ServerPlayer sender) { + PackSubmissions.receive(sender, msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 3a091d891cb027cda83236f24d3403a4a25d6bd7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:45 +0800 Subject: [PATCH 0300/1461] Add network/PackSync.java to the 26.2 shared sources --- .../jnhprojects/mcheli/network/PackSync.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSync.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSync.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSync.java new file mode 100644 index 00000000..b3e53269 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PackSync.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * The server half of the content pack sync. + *

+ *  player joins          ── PackListPacket(id, version, hash, size per pack) ──▶ client
+ *                        ◀── PackRequestPacket(hashes it has not cached) ────────
+ *  every tick            ── PackDataPacket(chunk) ──▶  until each pack is sent
+ * 
+ * Each player gets {@link #CHUNKS_PER_TICK} chunks a tick (about 5 MB a second), one pack after + * the other, so a download never crowds out the game's own packets. A pack's bytes are read when + * its turn comes and dropped once sent. Everything here runs on the server thread. + */ +public final class PackSync { + private static final int CHUNKS_PER_TICK = 2; + + private static final Map> TRANSFERS = new HashMap<>(); + + private PackSync() { + } + + /** Sends a joining player the pack list, and operators any pack problems. */ + public static void onLogin(ServerPlayer player) { + ModNetwork.sendToPlayer(player, list()); + if (seesProblems(player)) { + MCHeli.packs().reportProblems(player::sendSystemMessage); + } + PackSubmissions.onLogin(player); + } + + public static void onLogout(ServerPlayer player) { + TRANSFERS.remove(player.getUUID()); + PackSubmissions.onLogout(player); + } + + /** The packs the server uses now, as the client should load them. */ + public static PackListPacket list() { + List entries = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + try { + entries.add(new PackListPacket.Entry(pack.id(), pack.manifest().version(), pack.hash(), (int) pack.archiveSize())); + } catch (IOException e) { + MCHeli.LOGGER.error("Content pack {} cannot be sent to players", pack, e); + } + } + return new PackListPacket(List.copyOf(entries)); + } + + /** After a reload: drops transfers in progress and sends every player the new list. */ + public static void sendListToAll(MinecraftServer server) { + TRANSFERS.clear(); + ModNetwork.sendToAll(server, list()); + } + + /** Operators, and the owner of a single player or LAN world, are told what is wrong with the packs. */ + public static boolean seesProblems(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + return server.isSingleplayerOwner(player.nameAndId()) + || Commands.hasPermission(Commands.LEVEL_GAMEMASTERS).test(player.createCommandSourceStack()); + } + + static void request(ServerPlayer player, List hashes) { + Deque queue = TRANSFERS.computeIfAbsent(player.getUUID(), id -> new ArrayDeque<>()); + for (String hash : hashes) { + ContentPack pack = find(hash); + if (pack == null) { + MCHeli.LOGGER.debug("{} asked for content pack {}, which the server does not use", player.getScoreboardName(), hash); + continue; + } + if (queue.stream().noneMatch(t -> t.pack == pack)) { + queue.add(new Transfer(pack)); + } + } + } + + private static @Nullable ContentPack find(String hash) { + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.hash().equals(hash)) { + return pack; + } + } + return null; + } + + public static void pump(MinecraftServer server) { + Iterator>> it = TRANSFERS.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey()); + Deque queue = entry.getValue(); + if (player == null || queue.isEmpty()) { + it.remove(); + continue; + } + for (int sent = 0; sent < CHUNKS_PER_TICK && !queue.isEmpty(); sent++) { + Transfer transfer = queue.peek(); + PackDataPacket chunk = transfer.next(); + if (chunk == null) { + queue.poll(); + continue; + } + ModNetwork.sendToPlayer(player, chunk); + if (transfer.done()) { + queue.poll(); + } + } + } + } + + /** One pack on its way to one player. */ + private static final class Transfer { + private final ContentPack pack; + private byte @Nullable [] data; + private int offset; + + Transfer(ContentPack pack) { + this.pack = pack; + } + + /** The next chunk, or {@code null} when the pack could not be read. */ + @Nullable PackDataPacket next() { + if (data == null) { + try { + data = pack.archive(); + } catch (IOException e) { + MCHeli.LOGGER.error("Could not read content pack {} to send it", pack, e); + return null; + } + } + int end = Math.min(offset + PackDataPacket.CHUNK_SIZE, data.length); + PackDataPacket chunk = new PackDataPacket(pack.hash(), offset, data.length, Arrays.copyOfRange(data, offset, end)); + offset = end; + return chunk; + } + + boolean done() { + return data != null && offset >= data.length; + } + } +} From cc03553fdb7833b0a513237d73ddd5f49ac6a70c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:46 +0800 Subject: [PATCH 0301/1461] Add network/PilotStatePacket.java to the 26.2 shared sources --- .../mcheli/network/PilotStatePacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java new file mode 100644 index 00000000..6426f060 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Sent by the pilot every tick. Position, yaw and pitch already travel through the vanilla + * vehicle move packet; this carries the parts vanilla does not know about. + * + * @param keys the pilot's key state as packed by {@link AircraftEntity.ControlInput#write()}, so + * other players see wheels steer and tracks turn + */ +public record PilotStatePacket(int entityId, float throttle, float reverse, float roll, boolean hovering, byte keys) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pilot_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PilotStatePacket::encode, PilotStatePacket::decode); + + public static void encode(PilotStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeFloat(msg.throttle); + buf.writeFloat(msg.reverse); + buf.writeFloat(msg.roll); + buf.writeBoolean(msg.hovering); + buf.writeByte(msg.keys); + } + + public static PilotStatePacket decode(FriendlyByteBuf buf) { + return new PilotStatePacket(buf.readVarInt(), buf.readFloat(), buf.readFloat(), buf.readFloat(), buf.readBoolean(), buf.readByte()); + } + + public static void handle(PilotStatePacket msg, ServerPlayer sender) { + if (sender == null) { + return; + } + // Only the player actually flying this aircraft may drive it. + if (sender.getVehicle() instanceof AircraftEntity aircraft + && aircraft.getId() == msg.entityId + && aircraft.getControllingPassenger() == sender + && Float.isFinite(msg.throttle) + && Float.isFinite(msg.reverse) + && Float.isFinite(msg.roll)) { + aircraft.acceptPilotState(msg.throttle, msg.reverse, msg.roll, msg.hovering, msg.keys); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 9ad2136b322dea0228242cd69f58a0eb59f65b13 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:46 +0800 Subject: [PATCH 0302/1461] Add network/SpotPacket.java to the 26.2 shared sources --- .../mcheli/network/SpotPacket.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java new file mode 100644 index 00000000..1ee72286 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.weapon.Spotting; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; + +/** + * The attack key through a rangefinder, {@code MCH_PacketIndSpotEntity}: spot players and + * vehicles, spot mobs, or mark a point for the team. Spotting uses a charge; marking is free. + */ +public record SpotPacket(Mode mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("spot"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(SpotPacket::encode, SpotPacket::decode); + + /** The rangefinder's modes, in the order its mode key steps through them. */ + public enum Mode { + PLAYERS_AND_VEHICLES(Spotting.PLAYERS | Spotting.GROUND_VEHICLES | Spotting.HELICOPTERS | Spotting.PLANES), + MOBS(Spotting.OTHERS | Spotting.MONSTERS), + MARK_POINT(0); + + final int filter; + + Mode(int filter) { + this.filter = filter; + } + } + + public static void encode(SpotPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.mode.ordinal()); + } + + public static SpotPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Mode[] modes = Mode.values(); + if (ordinal < 0 || ordinal >= modes.length) { + throw new IllegalArgumentException("Unknown spot mode " + ordinal); + } + return new SpotPacket(modes[ordinal]); + } + + public static void handle(SpotPacket msg, ServerPlayer sender) { + if (sender == null || !RangefinderItem.canUse(sender)) { + return; + } + ItemStack stack = sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + stack = sender.getItemInHand(InteractionHand.OFF_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + return; + } + } + boolean done; + if (msg.mode == Mode.MARK_POINT) { + done = Spotting.markPoint(sender); + } else if (RangefinderItem.charges(stack) > 0) { + if (!sender.getAbilities().instabuild) { + // An empty rangefinder stays: it only needs redstone, unlike a worn out tool. + stack.setDamageValue(stack.getDamageValue() + 1); + } + done = Spotting.spot(sender, msg.mode.filter); + } else { + done = false; + } + ModSounds.playAt(sender, done ? "pi" : "ng", 1.0F, 1.0F); + } + + @Override + public Type type() { + return TYPE; + } +} From a3e6dbe0d386f36be05824547d6e606adc0acfd9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:47 +0800 Subject: [PATCH 0303/1461] Add network/TeamPacket.java to the 26.2 shared sources --- .../mcheli/network/TeamPacket.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java new file mode 100644 index 00000000..14b2db42 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A button of the team manager, {@code MCH_PacketIndMultiplayCommand}. Operators only; the + * server checks again. + * + * @param name the new team's name, for {@link Action#CREATE_TEAM} + * @param color the new team's colour, as a vanilla team colour id + */ +public record TeamPacket(Action action, String name, int color, boolean friendlyFire) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("team"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TeamPacket::encode, TeamPacket::decode); + + public enum Action { + PVP, + SHUFFLE, + CREATE_TEAM, + JUMP_TO_SPAWN, + DESTROY_ALL + } + + public TeamPacket(Action action) { + this(action, "", 0, false); + } + + public static void encode(TeamPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeUtf(msg.name, 16); + buf.writeVarInt(msg.color); + buf.writeBoolean(msg.friendlyFire); + } + + public static TeamPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown team action " + ordinal); + } + return new TeamPacket(actions[ordinal], buf.readUtf(16), buf.readVarInt(), buf.readBoolean()); + } + + public static void handle(TeamPacket msg, ServerPlayer sender) { + if (sender == null || !TeamManager.mayManage(sender)) { + return; + } + switch (msg.action) { + case PVP -> TeamManager.togglePvp(sender); + case SHUFFLE -> TeamManager.shuffle(sender); + case CREATE_TEAM -> TeamManager.create(sender, msg.name, msg.color, msg.friendlyFire); + case JUMP_TO_SPAWN -> TeamManager.jumpToSpawn(sender); + case DESTROY_ALL -> TeamManager.destroyAll(sender); + } + } + + @Override + public Type type() { + return TYPE; + } +} From e583b4b450505dce826d3238cf4e01166c397030 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:47 +0800 Subject: [PATCH 0304/1461] Add network/TestFlightPacket.java to the 26.2 shared sources --- .../mcheli/network/TestFlightPacket.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java new file mode 100644 index 00000000..db12a390 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.workshop.TestFlight; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** The workshop's Test flight button: fly the vehicle just saved. The server checks who is asking. */ +public record TestFlightPacket(String vehicle) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("test_flight"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TestFlightPacket::encode, TestFlightPacket::decode); + private static final int MAX_NAME = 64; + + public static void encode(TestFlightPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.vehicle, MAX_NAME); + } + + public static TestFlightPacket decode(FriendlyByteBuf buf) { + return new TestFlightPacket(buf.readUtf(MAX_NAME)); + } + + public static void handle(TestFlightPacket msg, ServerPlayer sender) { + if (sender != null) { + TestFlight.start(sender, msg.vehicle); + } + } + + @Override + public Type type() { + return TYPE; + } +} From a8c23def358620b5076704fab12072701d021867 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:48 +0800 Subject: [PATCH 0305/1461] Add network/WeaponStatePacket.java to the 26.2 shared sources --- .../mcheli/network/WeaponStatePacket.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java new file mode 100644 index 00000000..292f643e --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to crew: the magazine and reserve of one weapon after it fired, reloaded or was + * resupplied. The original's {@code MCH_PacketNotifyAmmoNum}. + */ +public record WeaponStatePacket(int entityId, int weapon, int ammo, int rest) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("weapon_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(WeaponStatePacket::encode, WeaponStatePacket::decode); + + public static void encode(WeaponStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.ammo); + buf.writeVarInt(msg.rest); + } + + public static WeaponStatePacket decode(FriendlyByteBuf buf) { + return new WeaponStatePacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(WeaponStatePacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player != null && player.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.applyAmmo(msg.weapon, msg.ammo, msg.rest); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 361409a9723308c30a162ebb2860ffa611d44cae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:48 +0800 Subject: [PATCH 0306/1461] Add particle/GlowParticleOptions.java to the 26.2 shared sources --- .../mcheli/particle/GlowParticleOptions.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java new file mode 100644 index 00000000..63a3577f --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A soft ball of light blended on top of the scene: the flash of an explosion, a burning wreck, an + * ember. Spawned on the client only, but the codecs let {@code /particle mcheli:glow} show it too. + * + * @param color RGB of the light + * @param size radius in blocks at its largest + * @param lifetime ticks until it has faded out + * @param flicker whether it wavers like a fire rather than fading smoothly like a flash + */ +public record GlowParticleOptions(int color, float size, int lifetime, boolean flicker) implements ParticleOptions { + /** White hot at the centre of a blast. */ + public static final int FLASH = 0xFFE2B0; + /** Burning fuel and the fire inside the smoke. */ + public static final int FIRE = 0xFF8A2E; + /** Embers and the deeper red at the edge of a fireball. */ + public static final int EMBER = 0xFF5A1E; + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.RGB_COLOR_CODEC.fieldOf("color").forGetter(GlowParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(GlowParticleOptions::size), + ExtraCodecs.POSITIVE_INT.fieldOf("lifetime").forGetter(GlowParticleOptions::lifetime), + Codec.BOOL.optionalFieldOf("flicker", false).forGetter(GlowParticleOptions::flicker)) + .apply(i, GlowParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, GlowParticleOptions::color, + ByteBufCodecs.FLOAT, GlowParticleOptions::size, + ByteBufCodecs.VAR_INT, GlowParticleOptions::lifetime, + ByteBufCodecs.BOOL, GlowParticleOptions::flicker, + GlowParticleOptions::new); + + public static GlowParticleOptions flash(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, false); + } + + public static GlowParticleOptions fire(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, true); + } + + @Override + public ParticleType getType() { + return ModParticles.GLOW.get(); + } +} From 5533637cc1b5c80281c45d3621c74a24934b2a90 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:49 +0800 Subject: [PATCH 0307/1461] Add particle/SmokeParticleOptions.java to the 26.2 shared sources --- .../mcheli/particle/SmokeParticleOptions.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java new file mode 100644 index 00000000..29a0b0ef --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java @@ -0,0 +1,42 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A puff of coloured smoke that swells and pales as it drifts, {@code MCH_EntityParticleSmoke} + * with {@code toWhite} set. The smoke grenades use it to mark a spot. + * + * @param color RGB the puff starts with + * @param size the original's particle size; a puff ends up about {@code size / 5} blocks across + * @param lifetime ticks the puff lasts, or 0 for the grenade smoke's three to five seconds + */ +public record SmokeParticleOptions(int color, float size, int lifetime) implements ParticleOptions { + public SmokeParticleOptions(int color, float size) { + this(color, size, 0); + } + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.RGB_COLOR_CODEC.fieldOf("color").forGetter(SmokeParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(SmokeParticleOptions::size), + ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("lifetime", 0).forGetter(SmokeParticleOptions::lifetime)) + .apply(i, SmokeParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, SmokeParticleOptions::color, + ByteBufCodecs.FLOAT, SmokeParticleOptions::size, + ByteBufCodecs.VAR_INT, SmokeParticleOptions::lifetime, + SmokeParticleOptions::new); + + @Override + public ParticleType getType() { + return ModParticles.SMOKE.get(); + } +} From 133f0e99c6e385fc86efb442fbe334a4820fd9cd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:49 +0800 Subject: [PATCH 0308/1461] Add platform/Platform.java to the 26.2 shared sources --- .../jnhprojects/mcheli/platform/Platform.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Platform.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Platform.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Platform.java new file mode 100644 index 00000000..e951f496 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Platform.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; + +import java.nio.file.Path; +import java.util.Objects; + +/** + * What the shared code needs from the mod loader. Each loader module installs its implementation + * first thing during mod construction, before anything else of ours runs. + * + *

Kept small on purpose: registration and events go the other way, with the loader module + * calling into the shared code (see {@code Registration} and the {@code *Hooks} classes). + */ +public interface Platform { + + static Platform get() { + Platform platform = Holder.instance; + if (platform == null) { + throw new IllegalStateException("No mod loader platform installed yet"); + } + return platform; + } + + static void install(Platform platform) { + if (Holder.instance != null) { + throw new IllegalStateException("Platform already installed: " + Holder.instance.loaderName()); + } + Holder.instance = Objects.requireNonNull(platform); + } + + /** {@code forge}, {@code neoforge} or {@code fabric}, for logs and {@code /mcheli status}. */ + String loaderName(); + + /** The game directory, which holds the {@code mcheli} folder. */ + Path gameDir(); + + /** The config directory, which holds the client config. */ + Path configDir(); + + /** Our version as the loader read it from the mod metadata. */ + String modVersion(); + + /** Whether this is a physical client, as opposed to a dedicated server. */ + boolean isClient(); + + void sendToServer(CustomPacketPayload payload); + + void sendToPlayer(ServerPlayer player, CustomPacketPayload payload); + + /** + * A builder for one of our creative tabs, placed after {@code after}. Vanilla's builder is not + * public on every loader and only Forge and NeoForge can order tabs. + */ + CreativeModeTab.Builder creativeTab(ResourceKey after); + + final class Holder { + private static volatile Platform instance; + + private Holder() { + } + } +} From 053605221eca98c082cfbd64b1b8415037c06ef6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:49 +0800 Subject: [PATCH 0309/1461] Add platform/Registered.java to the 26.2 shared sources --- .../mcheli/platform/Registered.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Registered.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Registered.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Registered.java new file mode 100644 index 00000000..3d1b9085 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Registered.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.resources.Identifier; + +import java.util.function.Supplier; + +/** + * A registry entry the shared code declares and the loader module registers, like Forge's + * {@code RegistryObject}. {@link #get()} works once the loader has registered it. + */ +public final class Registered implements Supplier { + private final Identifier id; + private volatile T value; + + Registered(Identifier id) { + this.id = id; + } + + public Identifier id() { + return id; + } + + @Override + public T get() { + T v = value; + if (v == null) { + throw new IllegalStateException("Registry entry " + id + " is not registered yet"); + } + return v; + } + + void bind(T value) { + this.value = value; + } +} From da53dc5502b146c91c67f1fc37c1fd1ddbc53b7f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:50 +0800 Subject: [PATCH 0310/1461] Add platform/Registration.java to the 26.2 shared sources --- .../mcheli/platform/Registration.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Registration.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Registration.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Registration.java new file mode 100644 index 00000000..030a07dd --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/platform/Registration.java @@ -0,0 +1,80 @@ +package com.jnhprojects.mcheli.platform; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.Registry; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Every vanilla registry entry the mod adds. The shared registry classes declare their entries + * here during mod construction; the loader module then registers them its own way: Forge and + * NeoForge from their register event, Fabric straight away. + */ +public final class Registration { + private static final List> PENDING = new ArrayList<>(); + private static final Set>> REGISTRIES = new LinkedHashSet<>(); + + private Registration() { + } + + /** + * Declares an entry. + * + * @param factory makes the value; it gets the entry's key for {@code Item.Properties#setId} + * and the like + */ + public static synchronized Registered register(ResourceKey> registry, String name, + Function, T> factory) { + Identifier id = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, name); + Registered entry = new Registered<>(id); + PENDING.add(new Pending<>(registry, entry, factory)); + REGISTRIES.add(registry); + return entry; + } + + /** The registries with entries of ours, in the order they were first used. */ + public static synchronized List>> registries() { + return List.copyOf(REGISTRIES); + } + + /** + * A key from {@link #registries()} with its element type filled in, for the loader's generic + * register calls. The type is whatever the caller needs; entries are only ever handed to the + * registry they were declared for. + */ + @SuppressWarnings("unchecked") + public static ResourceKey> typed(ResourceKey> registry) { + return (ResourceKey>) registry; + } + + /** + * Creates our entries for one registry, in declaration order, and hands each to the loader. + * Called once per registry. + */ + public static synchronized void registerAll(ResourceKey> registry, BiConsumer sink) { + for (Pending pending : PENDING) { + if (pending.registry.equals(registry)) { + @SuppressWarnings("unchecked") + Pending ours = (Pending) pending; + ours.create(sink); + } + } + } + + private record Pending(ResourceKey> registry, Registered entry, + Function, T> factory) { + @SuppressWarnings("unchecked") + void create(BiConsumer sink) { + T value = factory.apply(ResourceKey.create((ResourceKey>) registry, entry.id())); + entry.bind(value); + sink.accept(entry.id(), value); + } + } +} From b30801a9358e511171d1a7e4a82d9537826d021f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:50 +0800 Subject: [PATCH 0311/1461] Add registry/ModBlockEntities.java to the 26.2 shared sources --- .../mcheli/registry/ModBlockEntities.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java new file mode 100644 index 00000000..b03984ce --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java @@ -0,0 +1,22 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import java.util.Set; + +/** Our block entities: the drafting table's, which draws its model. */ +public final class ModBlockEntities { + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.BLOCK_ENTITY_TYPE, + "drafting_table", key -> new BlockEntityType<>(DraftingTableBlockEntity::new, Set.of(ModBlocks.DRAFTING_TABLE.get()))); + + private ModBlockEntities() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From c81a1dd7ef6f0412ebb4dd03c748790f7e05b891 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:51 +0800 Subject: [PATCH 0312/1461] Add registry/ModBlocks.java to the 26.2 shared sources --- .../mcheli/registry/ModBlocks.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java new file mode 100644 index 00000000..19995a92 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockBehaviour; + +/** Our blocks: the drafting table, lit or not. */ +public final class ModBlocks { + public static final Registered DRAFTING_TABLE = Registration.register(Registries.BLOCK, "drafting_table", + key -> new DraftingTableBlock(BlockBehaviour.Properties.of().setId(key) + .strength(0.2F) + .sound(SoundType.METAL) + .noOcclusion() + .lightLevel(state -> state.getValue(DraftingTableBlock.LIT) ? 15 : 0))); + + private ModBlocks() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From ad534827116c50d728fbc84d781fc3de37fae70d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:51 +0800 Subject: [PATCH 0313/1461] Add registry/ModCreativeTabs.java to the 26.2 shared sources --- .../mcheli/registry/ModCreativeTabs.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java new file mode 100644 index 00000000..870aa8b8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java @@ -0,0 +1,72 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** + * One tab per vehicle kind, listing a stack for each loaded definition of that kind, and one for + * the equipment and throwables. Those lists are read from the content registry whenever a tab is + * built, so reloaded content shows up. + */ +public final class ModCreativeTabs { + public static final Registered HELICOPTERS = register("helicopters", "helicopter", "ah-64", + CreativeModeTabs.SPAWN_EGGS); + public static final Registered PLANES = register("planes", "plane", "f-35b", key(HELICOPTERS)); + public static final Registered TANKS = register("tanks", "tank", "m1a2", key(PLANES)); + public static final Registered VEHICLES = register("vehicles", "vehicle", "mk15", key(TANKS)); + public static final Registered EQUIPMENT = Registration.register(Registries.CREATIVE_MODE_TAB, "equipment", + key -> Platform.get().creativeTab(key(VEHICLES)) + .title(Component.translatable("itemGroup.mcheli.equipment")) + .icon(() -> new ItemStack(ModItems.WRENCH.get())) + .displayItems((params, output) -> { + ModItems.EQUIPMENT.forEach(item -> output.accept(item.get())); + MCHeli.content().throwables().forEach(def -> output.accept(ThrowableItem.stackOf(def))); + }) + .build()); + + private ModCreativeTabs() { + } + + private static ResourceKey key(Registered tab) { + return ResourceKey.create(Registries.CREATIVE_MODE_TAB, tab.id()); + } + + private static Registered register(String name, String kind, String icon, ResourceKey after) { + return Registration.register(Registries.CREATIVE_MODE_TAB, name, key -> Platform.get().creativeTab(after) + .title(Component.translatable("itemGroup.mcheli." + name)) + .icon(() -> { + AircraftDefinition def = MCHeli.content().any(icon); + return def != null ? AircraftItem.stackOf(def) : new ItemStack(ModItems.forKind(kind)); + }) + .displayItems((params, output) -> { + for (AircraftDefinition def : MCHeli.content().all()) { + if (def.kind().equals(kind)) { + output.accept(AircraftItem.stackOf(def)); + } + } + }) + .build()); + } + + /** Our tabs, for rebuilding their contents after a content reload. */ + public static List all() { + return List.of(HELICOPTERS.get(), PLANES.get(), TANKS.get(), VEHICLES.get(), EQUIPMENT.get()); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 5adcd65a9e865da166083b9e3b03c7faf64ef770 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:52 +0800 Subject: [PATCH 0314/1461] Add registry/ModDataComponents.java to the 26.2 shared sources --- .../mcheli/registry/ModDataComponents.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java new file mode 100644 index 00000000..377a5783 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.Codec; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.codec.ByteBufCodecs; + +public final class ModDataComponents { + /** + * The definition a vehicle item places, such as {@code ah-64}. Saved with the stack and sent + * to clients; the item itself only says which kind of vehicle it is. + */ + public static final Registered> VEHICLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "vehicle", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + /** The definition a throwable item throws, such as {@code m18_red}. */ + public static final Registered> THROWABLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "throwable", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + private ModDataComponents() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 0efa64f4604bc95dd51100d1f3013485737db06a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:52 +0800 Subject: [PATCH 0315/1461] Add registry/ModEntities.java to the 26.2 shared sources --- .../mcheli/registry/ModEntities.java | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java new file mode 100644 index 00000000..b6686ccd --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java @@ -0,0 +1,170 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.entity.ContainerEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.RappelEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; + +/** + * One entity type per vehicle kind. The concrete vehicle is stored on the entity, the same approach + * the original took, so adding content never changes the entity registry. + */ +public final class ModEntities { + public static final Registered> HELICOPTER = register("helicopter", HelicopterEntity::new); + public static final Registered> PLANE = register("plane", PlaneEntity::new); + public static final Registered> TANK = register("tank", TankEntity::new); + public static final Registered> VEHICLE = register("vehicle", GroundVehicleEntity::new); + + public static final Registered> BULLET = projectile("bullet", BulletEntity::new); + public static final Registered> ROCKET = projectile("rocket", RocketEntity::new); + public static final Registered> BOMB = projectile("bomb", BombEntity::new); + public static final Registered> AA_MISSILE = projectile("aa_missile", AAMissileEntity::new); + public static final Registered> AS_MISSILE = projectile("as_missile", ASMissileEntity::new); + public static final Registered> AT_MISSILE = projectile("at_missile", ATMissileEntity::new); + public static final Registered> TV_MISSILE = projectile("tv_missile", TvMissileEntity::new); + public static final Registered> TORPEDO = projectile("torpedo", TorpedoEntity::new); + public static final Registered> MARKER_ROCKET = projectile("marker_rocket", MarkerRocketEntity::new); + public static final Registered> DISPENSED_ITEM = projectile("dispensed_item", DispensedItemEntity::new); + public static final Registered> FLARE = Registration.register(Registries.ENTITY_TYPE, "flare", key -> EntityType.Builder + .of(FlareEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(2) + .fireImmune() + .noSave() + .build(key)); + public static final Registered> A10 = Registration.register(Registries.ENTITY_TYPE, "a10", key -> EntityType.Builder + .of(A10Entity::new, MobCategory.MISC) + .sized(5.0F, 3.0F) + .clientTrackingRange(32) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + + public static final Registered> PARACHUTE = Registration.register(Registries.ENTITY_TYPE, "parachute", key -> EntityType.Builder + .of(ParachuteEntity::new, MobCategory.MISC) + .sized(1.5F, 0.6F) + .clientTrackingRange(10) + .updateInterval(1) + .build(key)); + + public static final Registered> THROWABLE = Registration.register(Registries.ENTITY_TYPE, "throwable", key -> EntityType.Builder + .of(ThrowableEntity::new, MobCategory.MISC) + .sized(0.25F, 0.25F) + .clientTrackingRange(8) + .updateInterval(2) + .noSave() + .build(key)); + + public static final Registered> GLTD = Registration.register(Registries.ENTITY_TYPE, "gltd", key -> EntityType.Builder + .of(GltdEntity::new, MobCategory.MISC) + .sized(0.5F, 0.5F) + .clientTrackingRange(10) + .updateInterval(3) + .build(key)); + + public static final Registered> CONTAINER = Registration.register(Registries.ENTITY_TYPE, "container", key -> EntityType.Builder + .of(ContainerEntity::new, MobCategory.MISC) + .sized(2.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key)); + + public static final Registered> CHAIN = Registration.register(Registries.ENTITY_TYPE, "chain", key -> EntityType.Builder + .of(ChainEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(2) + .fireImmune() + .build(key)); + + public static final Registered> RAPPEL = Registration.register(Registries.ENTITY_TYPE, "rappel", key -> EntityType.Builder + .of(RappelEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + + public static final Registered> UAV_STATION = Registration.register(Registries.ENTITY_TYPE, "uav_station", key -> EntityType.Builder + .of(UavStationEntity::new, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key)); + + private ModEntities() { + } + + /** Projectiles are a metre cube like the original's, and never saved. */ + private static Registered> projectile(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + } + + private static Registered> register(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .build(key)); + } + + /** The entity type that hosts a definition of this kind. */ + public static EntityType typeFor(AircraftDefinition def) { + if (def instanceof HelicopterDefinition) { + return HELICOPTER.get(); + } + if (def instanceof PlaneDefinition) { + return PLANE.get(); + } + if (def instanceof TankDefinition) { + return TANK.get(); + } + return VEHICLE.get(); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From ff708bc8e2b62af04747b2d8654064cbaea25341 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:53 +0800 Subject: [PATCH 0316/1461] Add registry/ModItems.java to the 26.2 shared sources --- .../jnhprojects/mcheli/registry/ModItems.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java new file mode 100644 index 00000000..e7415dfd --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java @@ -0,0 +1,102 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.item.LegacyAircraftItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.item.PlacedEntityItem; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.item.UavStationItem; +import com.jnhprojects.mcheli.item.WrenchItem; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; + +import java.util.List; +import java.util.Map; + +/** + * One item per vehicle kind; the stack says which vehicle (see {@link AircraftItem}). The 1.0.0 + * items, one per vehicle, are kept for the content present at startup so old worlds keep them. The + * equipment (fuel can, wrench, parachute and so on) has one item each, and the throwables share one. + */ +public final class ModItems { + public static final Registered HELICOPTER = kind("helicopter"); + public static final Registered PLANE = kind("plane"); + public static final Registered TANK = kind("tank"); + public static final Registered VEHICLE = kind("vehicle"); + + public static final Registered FUEL = Registration.register(Registries.ITEM, "fuel", key -> new FuelItem(new Item.Properties().setId(key))); + public static final Registered WRENCH = Registration.register(Registries.ITEM, "wrench", key -> new WrenchItem(new Item.Properties().setId(key))); + public static final Registered PARACHUTE = Registration.register(Registries.ITEM, "parachute", key -> new ParachuteItem(new Item.Properties().setId(key))); + public static final Registered GLTD = Registration.register(Registries.ITEM, "gltd", + key -> new PlacedEntityItem(ModEntities.GLTD, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered RANGEFINDER = Registration.register(Registries.ITEM, "rangefinder", key -> new RangefinderItem(new Item.Properties().setId(key))); + public static final Registered CONTAINER = Registration.register(Registries.ITEM, "container", + key -> new PlacedEntityItem(ModEntities.CONTAINER, new Item.Properties().setId(key))); + public static final Registered CHAIN = Registration.register(Registries.ITEM, "chain", key -> new ChainItem(new Item.Properties().setId(key))); + public static final Registered STINGER_MISSILE = Registration.register(Registries.ITEM, "fim92_bullet", key -> new Item(new Item.Properties().setId(key).stacksTo(16))); + public static final Registered STINGER = Registration.register(Registries.ITEM, "fim92", + key -> new LauncherItem("fim92", () -> ModItems.STINGER_MISSILE.get(), new Item.Properties().setId(key))); + public static final Registered JAVELIN_MISSILE = Registration.register(Registries.ITEM, "fgm148_bullet", key -> new Item(new Item.Properties().setId(key).stacksTo(16))); + public static final Registered JAVELIN = Registration.register(Registries.ITEM, "fgm148", + key -> new LauncherItem("fgm148", () -> ModItems.JAVELIN_MISSILE.get(), new Item.Properties().setId(key))); + public static final Registered UAV_STATION = Registration.register(Registries.ITEM, "uav_station", + key -> new UavStationItem(UavStationEntity.STATION, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered UAV_CONTROLLER = Registration.register(Registries.ITEM, "uav_station2", + key -> new UavStationItem(UavStationEntity.PORTABLE, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered DRAFTING_TABLE = Registration.register(Registries.ITEM, "drafting_table", + key -> new BlockItem(ModBlocks.DRAFTING_TABLE.get(), new Item.Properties().setId(key).useBlockDescriptionPrefix())); + /** Every throwable; the stack says which (see {@link ThrowableItem}). */ + public static final Registered THROWABLE = Registration.register(Registries.ITEM, "throwable", key -> new ThrowableItem(new Item.Properties().setId(key))); + + /** The equipment tab, in the order it lists them. */ + public static final List> EQUIPMENT = List.of(FUEL, WRENCH, PARACHUTE, GLTD, RANGEFINDER, CONTAINER, CHAIN, + STINGER, STINGER_MISSILE, JAVELIN, JAVELIN_MISSILE, UAV_STATION, UAV_CONTROLLER, DRAFTING_TABLE); + + private static final Map> BY_KIND = + Map.of("helicopter", HELICOPTER, "plane", PLANE, "tank", TANK, "vehicle", VEHICLE); + + private ModItems() { + } + + private static Registered kind(String kind) { + return Registration.register(Registries.ITEM, kind, key -> new AircraftItem(kind, new Item.Properties().setId(key).stacksTo(1))); + } + + /** + * Declares the entries, with a 1.0.0 item for each original vehicle in {@code content}; the + * loader module registers them. + */ + public static void init(ContentRegistry content) { + for (AircraftDefinition def : content.all()) { + String name = def.name(); + // Only the imported content existed in 1.0.0; pack vehicles never had their own item. + if (!def.pack().equals(ContentSource.ORIGINAL) || BY_KIND.containsKey(name) || EQUIPMENT.stream().anyMatch(e -> e.id().getPath().equals(name))) { + // 1.0.0 could not have had such an item either: it would have clashed the same way. + continue; + } + String kind = def.kind(); + Registration.register(Registries.ITEM, name, key -> new LegacyAircraftItem(kind, name, + new Item.Properties().setId(key).stacksTo(1))); + } + } + + /** The generic item of a vehicle kind ({@code helicopter}, {@code plane}, {@code tank} or {@code vehicle}). */ + public static Item forKind(String kind) { + Registered item = BY_KIND.get(kind); + if (item == null) { + throw new IllegalArgumentException("Unknown vehicle kind " + kind); + } + return item.get(); + } +} From 7e2728997448929f4cfaa52cc6fbb6b207d453c5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:53 +0800 Subject: [PATCH 0317/1461] Add registry/ModMenus.java to the 26.2 shared sources --- .../jnhprojects/mcheli/registry/ModMenus.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java new file mode 100644 index 00000000..0dac0052 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; + +/** Our screens with slots. The cargo chest uses vanilla's chest menus and needs no entry here. */ +public final class ModMenus { + public static final Registered> VEHICLE = Registration.register(Registries.MENU, "vehicle", + key -> new MenuType<>(VehicleMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.MENU, "drafting_table", + key -> new MenuType<>(DraftingTableMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> UAV_STATION = Registration.register(Registries.MENU, "uav_station", + key -> new MenuType<>(UavStationMenu::new, FeatureFlags.VANILLA_SET)); + + private ModMenus() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From b73be2678ec52ad16cbcc957f15cce1ad618c10b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:54 +0800 Subject: [PATCH 0318/1461] Add registry/ModParticles.java to the 26.2 shared sources --- .../mcheli/registry/ModParticles.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java new file mode 100644 index 00000000..4ed53acd --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java @@ -0,0 +1,48 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; + +public final class ModParticles { + /** Ignores the particle limit: an explosion that loses its flash to the cap looks broken. */ + public static final Registered> GLOW = Registration.register(Registries.PARTICLE_TYPE, "glow", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return GlowParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return GlowParticleOptions.STREAM_CODEC; + } + }); + + /** Smoke grenades: a marker that vanishes under the particle limit is no marker. */ + public static final Registered> SMOKE = Registration.register(Registries.PARTICLE_TYPE, "smoke", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return SmokeParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return SmokeParticleOptions.STREAM_CODEC; + } + }); + + private ModParticles() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 10cff1ca723588bbb410ab8833c4e1b1409befb9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:54 +0800 Subject: [PATCH 0319/1461] Add team/Markers.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/team/Markers.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/team/Markers.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/team/Markers.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/team/Markers.java new file mode 100644 index 00000000..4d869373 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/team/Markers.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.team; + +import net.minecraft.core.BlockPos; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * What this client's team has spotted, {@code MCH_GuiTargetMarker}: the spotted entities, each for + * as long as it was spotted, and the last marked point. Filled by {@code MarkerPacket} on the + * client's main thread and drawn by the HUD; a dedicated server never touches it. + */ +public final class Markers { + /** A marked point stays up this long, unless another replaces it. */ + public static final int POINT_TICKS = 60 * 20; + + private static final Map SPOTTED = new HashMap<>(); + private static @Nullable BlockPos point; + private static int pointTicks; + + private Markers() { + } + + public static void spotted(int[] entities, int ticks) { + for (int id : entities) { + SPOTTED.merge(id, ticks, Math::max); + } + } + + public static void point(BlockPos pos) { + point = pos; + pointTicks = POINT_TICKS; + } + + /** Entity ids and ticks left. */ + public static Map spotted() { + return SPOTTED; + } + + public static @Nullable BlockPos point() { + return point; + } + + public static void tick() { + SPOTTED.replaceAll((id, ticks) -> ticks - 1); + SPOTTED.values().removeIf(ticks -> ticks <= 0); + if (pointTicks > 0 && --pointTicks == 0) { + point = null; + } + } + + public static void clear() { + SPOTTED.clear(); + point = null; + pointTicks = 0; + } +} From 4b3a3f9441f1ed3b525e6b4899f7889107060b6f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:54 +0800 Subject: [PATCH 0320/1461] Add team/TeamManager.java to the 26.2 shared sources --- .../jnhprojects/mcheli/team/TeamManager.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java new file mode 100644 index 00000000..b6816296 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java @@ -0,0 +1,113 @@ +package com.jnhprojects.mcheli.team; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.Permissions; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.gamerules.GameRules; +import net.minecraft.world.level.portal.TeleportTransition; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.TeamColor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * The team manager's actions, {@code MCH_Multiplay} and {@code MCH_MultiplayPacketHandler}: for + * operators running a match. Teams are the vanilla scoreboard teams, so {@code /team} and the + * player list see the same ones. + */ +public final class TeamManager { + private TeamManager() { + } + + /** Operators only, as the original checked the scoreboard command's permission. */ + public static boolean mayManage(Player player) { + return player.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER); + } + + /** Switches the {@code pvp} game rule in every dimension and says which way it went. */ + public static void togglePvp(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + boolean pvp = !player.level().getGameRules().get(GameRules.PVP); + for (ServerLevel level : server.getAllLevels()) { + level.getGameRules().set(GameRules.PVP, pvp, server); + } + broadcast(server, Component.translatable(pvp ? "message.mcheli.team.pvp_on" : "message.mcheli.team.pvp_off")); + } + + /** {@code shuffleTeam}: every player online goes into one of the teams in turn, in random order. */ + public static void shuffle(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + Scoreboard scoreboard = server.getScoreboard(); + List teams = new ArrayList<>(scoreboard.getPlayerTeams()); + if (teams.isEmpty()) { + player.sendOverlayMessage(Component.translatable("message.mcheli.team.no_teams")); + return; + } + List players = new ArrayList<>(server.getPlayerList().getPlayers()); + Collections.shuffle(players); + Collections.shuffle(teams); + for (int i = 0; i < players.size(); i++) { + scoreboard.addPlayerToTeam(players.get(i).getScoreboardName(), teams.get(i % teams.size())); + } + broadcast(server, Component.translatable("message.mcheli.team.shuffled", teams.size())); + } + + /** The original's create team screen: a name, a colour and friendly fire. */ + public static void create(ServerPlayer player, String name, int color, boolean friendlyFire) { + Scoreboard scoreboard = player.level().getServer().getScoreboard(); + String trimmed = name.trim(); + if (trimmed.isEmpty() || trimmed.length() > 16 || trimmed.contains(" ") || scoreboard.getPlayerTeam(trimmed) != null) { + player.sendOverlayMessage(Component.translatable("message.mcheli.team.bad_name", trimmed)); + return; + } + PlayerTeam team = scoreboard.addPlayerTeam(trimmed); + team.setColor(Optional.of(TeamColor.VALUES.get(Math.floorMod(color, TeamColor.VALUES.size())))); + team.setAllowFriendlyFire(friendlyFire); + player.sendOverlayMessage(Component.translatable("message.mcheli.team.created", trimmed)); + } + + /** {@code jumpSpawnPoint}: every player back to where they would respawn. */ + public static void jumpToSpawn(ServerPlayer player) { + for (ServerPlayer target : List.copyOf(player.level().getServer().getPlayerList().getPlayers())) { + target.stopRiding(); + target.teleport(target.findRespawnPositionAndUseSpawnBlock(false, TeleportTransition.DO_NOTHING)); + } + } + + /** {@code destoryAllAircraft}: every loaded vehicle is gone, its crew left standing. */ + public static void destroyAll(ServerPlayer player) { + int count = 0; + for (ServerLevel level : player.level().getServer().getAllLevels()) { + for (AircraftEntity vehicle : level.getEntities(EntityTypeTest.forClass(AircraftEntity.class), e -> true)) { + vehicle.ejectPassengers(); + vehicle.discard(); + count++; + } + } + player.sendOverlayMessage(Component.translatable("message.mcheli.team.destroyed", count)); + } + + /** {@code entity} and everyone on its team. */ + public static List teammates(ServerPlayer entity) { + List out = new ArrayList<>(); + for (ServerPlayer player : entity.level().getServer().getPlayerList().getPlayers()) { + if (player == entity || player.isAlliedTo(entity)) { + out.add(player); + } + } + return out; + } + + private static void broadcast(MinecraftServer server, Component message) { + server.getPlayerList().broadcastSystemMessage(message, false); + } +} From e9cca16cd107136cf1b5525777724c2be5b3ba55 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:55 +0800 Subject: [PATCH 0321/1461] Add weapon/CasStrike.java to the 26.2 shared sources --- .../jnhprojects/mcheli/weapon/CasStrike.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java new file mode 100644 index 00000000..e78278a8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java @@ -0,0 +1,125 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A close air support run, {@code MCH_WeaponCAS}: three A-10 passes over a spot, 45 ticks apart, + * the second and third offset to either side. Vehicles with a CAS weapon drive theirs from their + * {@link WeaponSet}; the GLTD keeps one of these. + */ +public final class CasStrike { + /** Ticks from the call to the first pass: the time it takes the A-10 to arrive. */ + public static final int FIRST_PASS = 50; + private static final int BETWEEN_PASSES = 45; + private static final int PASSES = 3; + private static final RandomSource RANDOM = RandomSource.create(); + + private final String weapon; + private double x; + private double y; + private double z; + private int direction; + private @Nullable Entity shooter; + private @Nullable Entity user; + private int passes = PASSES; + private int ticksToNext; + + /** @param weapon the CAS weapon definition the A-10s fire, such as {@code a10gau8} */ + public CasStrike(String weapon) { + this.weapon = weapon; + } + + /** Whether passes are still to come. */ + public boolean isRunning() { + return passes < PASSES; + } + + /** + * Calls the strike on {@code at}. The A-10 comes in from one side of the caller's view, + * picked at random, never straight from behind or towards them. + */ + public void start(Vec3 at, float callerYaw, @Nullable Entity shooter, @Nullable Entity user) { + x = at.x; + y = at.y; + z = at.z; + direction = Math.floorMod(approach(callerYaw), 4); + this.shooter = shooter; + this.user = user; + passes = 0; + ticksToNext = FIRST_PASS; + } + + /** The original's approach: the quarter the caller faces, turned one way or the other. */ + static int approach(float callerYaw) { + int facing = (int) Mth.positiveModulo(callerYaw + 45.0F, 360.0F) / 90; + return facing + (RANDOM.nextBoolean() ? -1 : 1); + } + + public void tick(ServerLevel level, WeaponDefinition def) { + if (passes >= PASSES || --ticksToNext > 0) { + return; + } + Vec3 offset = passOffset(passes, direction); + spawnA10(level, weapon, def, direction, x + offset.x, y + 20.0, z + offset.z, shooter, user); + passes++; + ticksToNext = BETWEEN_PASSES; + } + + /** The first pass is dead on; the second strays to one side of the track, the third to the other. */ + static Vec3 passOffset(int pass, int direction) { + if (pass == 0) { + return Vec3.ZERO; + } + double sign = pass == 1 ? 1.0 : -1.0; + if (direction % 2 == 0) { + return new Vec3(RANDOM.nextDouble() * 10.0 * sign, 0.0, (RANDOM.nextDouble() - 0.5) * 10.0); + } + return new Vec3((RANDOM.nextDouble() - 0.5) * 10.0, 0.0, RANDOM.nextDouble() * 10.0 * sign); + } + + /** One A-10, starting 90 blocks short of the spot and flying over it at three blocks a tick. */ + static void spawnA10(ServerLevel level, String weapon, WeaponDefinition def, int direction, double x, double y, double z, + @Nullable Entity shooter, @Nullable Entity user) { + double mx = 0.0; + double mz = 0.0; + switch (direction) { + case 0 -> { + mz = 3.0; + z -= 90.0; + } + case 1 -> { + mx = -3.0; + x += 90.0; + } + case 2 -> { + mz = -3.0; + z += 90.0; + } + default -> { + mx = 3.0; + x -= 90.0; + } + } + A10Entity a10 = ModEntities.A10.get().create(level, EntitySpawnReason.TRIGGERED); + if (a10 == null) { + return; + } + a10.setPos(x, y, z); + a10.setup(weapon, direction, mx, mz, shooter, user); + a10.power = def.power; + a10.explosionPower = def.explosion; + a10.acceleration = def.acceleration; + level.addFreshEntity(a10); + ModSounds.playAt(level, x, y, z, "a-10_snd", 150.0F, 1.0F); + } +} From 60f028f7b38f4c553bfbdc438fde96dcec274dce Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:55 +0800 Subject: [PATCH 0322/1461] Clear the invulnerable time directly on 26.2 --- .../com/jnhprojects/mcheli/weapon/Combat.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java new file mode 100644 index 00000000..204fe93f --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +/** Damage rules shared by bullets and explosions. */ +public final class Combat { + private Combat() { + } + + /** The {@code DamageFactor} kind a target belongs to, or {@code null} for anything else. */ + public static @Nullable String kind(Entity entity) { + return switch (entity) { + case Player p -> "player"; + case HelicopterEntity h -> "helicopter"; + case PlaneEntity p -> "plane"; + case TankEntity t -> "tank"; + case GroundVehicleEntity v -> "vehicle"; + default -> null; + }; + } + + public static float factor(@Nullable WeaponDefinition def, Entity target) { + String kind = kind(target); + return def != null && kind != null ? def.damageFactor(kind) : 1.0F; + } + + /** + * Deals weapon damage. The original scaled every living target's invulnerability window by + * {@code HurtResistantTime}, zero by default, so each round of a burst lands. + */ + public static boolean hurt(Entity target, DamageSource source, float damage, @Nullable WeaponDefinition def) { + if (!(target.level() instanceof ServerLevel level)) { + return false; + } + if (target instanceof LivingEntity living) { + living.invulnerableTime = 0; + } + return target.hurtServer(level, source, damage * factor(def, target)); + } + + /** The convention tag every loader fills, so modded panes count too. */ + private static final TagKey GLASS_PANES = TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("c", "glass_panes")); + + /** + * {@code BulletBreakableBlocks} with its default list: panes, grass, flowers, vines, wheat, + * sugar cane and lily pads, which a round shoots through. + */ + public static boolean isBulletBreakable(BlockState state) { + return state.is(GLASS_PANES) + || state.is(BlockTags.FLOWERS) + || state.is(Blocks.SHORT_GRASS) || state.is(Blocks.TALL_GRASS) + || state.is(Blocks.FERN) || state.is(Blocks.LARGE_FERN) + || state.is(Blocks.VINE) || state.is(Blocks.WHEAT) + || state.is(Blocks.SUGAR_CANE) || state.is(Blocks.LILY_PAD); + } +} From 80169692138f71565b6af15c838343d9fe6c0f76 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:56 +0800 Subject: [PATCH 0323/1461] Add weapon/ExplosionEffect.java to the 26.2 shared sources --- .../mcheli/weapon/ExplosionEffect.java | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java new file mode 100644 index 00000000..4f4b0dc8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java @@ -0,0 +1,232 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.HashSet; +import java.util.Set; + +/** + * The look of an MCHeli explosion, ported from {@code MCH_Explosion.effectExplosion} and + * {@code effectExplosionInWater}: a fireball, ground dust thrown up from the crater, and columns + * and rings of smoke that hang around for seconds. It runs on each client that receives the + * explosion; the burning debris arcs are real flares spawned by the server. + */ +public final class ExplosionEffect { + /** The largest crash fireball drawn; beyond this the particle count gets out of hand. */ + public static final float MAX_CRASH_SIZE = 12.0F; + /** The widest explosion flash, in blocks; past this it washes out the whole screen. */ + private static final float MAX_FLASH_RADIUS = 30.0F; + + private ExplosionEffect() { + } + + /** + * Not in the original: a crashing vehicle goes up in a far bigger show than its fuel explosion + * alone makes. The same shell of smoke as a missile hit in mid-air, a cluster of fireballs + * around it, a plume of smoke that keeps climbing for seconds and burning fuel thrown about. + * The fire glows for several seconds afterwards, which at night lights up the smoke above it. + */ + public static void crash(Level level, double x, double y, double z, float size) { + play(level, x, y, z, size, false); + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, Math.min(size * 3.0F, MAX_FLASH_RADIUS), 20), + true, x, y + size * 0.3, z, 0.0, 0.0, 0.0); + int fireballs = Math.max(3, (int) (size / 2.0F)); + for (int i = 0; i < fireballs; i++) { + double r = size * 0.5 * random.nextDouble(); + double a = random.nextDouble() * Math.PI * 2.0; + double fx = x + Math.cos(a) * r; + double fy = y + random.nextDouble() * size * 0.4; + double fz = z + Math.sin(a) * r; + level.addAlwaysVisibleParticle(ParticleTypes.EXPLOSION_EMITTER, true, fx, fy, fz, 1.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size * (0.4F + random.nextFloat() * 0.3F), + 80 + random.nextInt(60)), true, fx, fy, fz, 0.0, 0.01, 0.0); + } + for (int i = 0; i < size * 4; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + x + (random.nextDouble() - 0.5) * size * 0.6, y + random.nextDouble() * 2.0, z + (random.nextDouble() - 0.5) * size * 0.6, + (random.nextDouble() - 0.5) * 0.02, 0.05 + random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.02); + } + for (int i = 0; i < size * 10; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = (0.2 + random.nextDouble() * 0.4) * size / 6.0; + double up = random.nextDouble() * speed; + level.addAlwaysVisibleParticle(random.nextInt(4) == 0 ? ParticleTypes.LAVA : ParticleTypes.FLAME, true, x, y + 0.5, z, + Math.cos(a) * speed, up, Math.sin(a) * speed); + if (i % 4 == 0) { + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.5F + random.nextFloat() * 0.4F, + 20 + random.nextInt(20)), true, x, y + 0.5, z, Math.cos(a) * speed, up, Math.sin(a) * speed); + } + } + } + + /** + * Not in the original: the light of the blast itself, a white hot flash inside a wider red + * glow, and a few embers thrown out. Faint by day; at night it lights up the smoke. + */ + private static void flash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + float radius = Math.min(2.0F + size * 1.5F, MAX_FLASH_RADIUS); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, radius, 8 + (int) size), true, x, y, z, 0.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.EMBER, Math.min(radius * 1.6F, MAX_FLASH_RADIUS), + 14 + (int) (size * 2.0F)), true, x, y, z, 0.0, 0.0, 0.0); + int embers = Math.min((int) (size * 2.0F), 24); + for (int i = 0; i < embers; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = 0.2 + random.nextDouble() * 0.3 * Math.min(size, 4.0F); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 15 + random.nextInt(15)), true, x, y, z, Math.cos(a) * speed, 0.1 + random.nextDouble() * speed, Math.sin(a) * speed); + } + } + + public static void play(Level level, double x, double y, double z, float size, boolean inWater) { + if (size <= 0.0F) { + return; + } + if (inWater) { + splash(level, x, y, z, size); + return; + } + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(size >= 2.0F ? ParticleTypes.EXPLOSION_EMITTER : ParticleTypes.EXPLOSION, true, x, y, z, 1.0, 0.0, 0.0); + flash(level, x, y, z, size); + + int count = 0; + int puffEvery = (int) Math.max(size, 4.0F); + for (BlockPos pos : affectedBlocks(level, x, y, z, size)) { + count++; + double bx = pos.getX() + random.nextFloat(); + double by = pos.getY() + random.nextFloat(); + double bz = pos.getZ() + random.nextFloat(); + double mx = bx - x; + double my = by - y; + double mz = bz - z; + double d = Math.sqrt(mx * mx + my * my + mz * mz); + if (d == 0.0) { + continue; + } + double push = 0.5 / (d / size + 0.1) * (random.nextFloat() * random.nextFloat() + 0.3F) * 0.5; + mx = mx / d * push; + my = my / d * push; + mz = mz / d * push; + double px = (bx + x) / 2.0; + double py = (by + y) / 2.0; + double pz = (bz + z) / 2.0; + double angle = Math.toRadians(random.nextInt(360)); + + if (count % 4 == 0) { + // Dirt and stone thrown up from the ground under the blast. + BlockState ground = level.getBlockState(BlockPos.containing(px + 0.5, py - 0.5, pz + 0.5)); + if (!ground.isAir()) { + float force = Math.min(size / 3.0F, 2.0F) * (0.5F + random.nextFloat() * 0.5F); + BlockParticleOption dust = new BlockParticleOption(ParticleTypes.BLOCK, ground); + for (int i = 0; i < 3; i++) { + level.addAlwaysVisibleParticle(dust, true, px, py + 1.0, pz, + Math.sin(angle) * force * 0.5, 0.5 + my / 5.0 * force, Math.cos(angle) * force * 0.5); + } + } + } + if (size <= 1.0F || count % puffEvery == 0) { + // Smoke either shoots up as a column or rolls out along the ground. + if (random.nextBoolean()) { + my *= 3.0; + mx *= 0.1; + mz *= 0.1; + } else { + my *= 0.2; + mx *= 3.0; + mz *= 3.0; + } + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, px, py, pz, mx * 0.3, my * 0.3, mz * 0.3); + for (int i = 0; i < 2; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, px, py, pz, mx, my, mz); + } + if (random.nextInt(3) == 0) { + level.addAlwaysVisibleParticle(ParticleTypes.FLAME, true, px, py, pz, mx * 0.5, my * 0.5, mz * 0.5); + } + } + } + } + + /** + * The blocks the original's effect ray marched through: sixteen by sixteen rays from the + * centre, each running until block resistance used up its strength. + */ + @SuppressWarnings("deprecation") // The plain block resistance is what the original used here. + private static Set affectedBlocks(Level level, double x, double y, double z, float size) { + Set out = new HashSet<>(); + RandomSource random = level.getRandom(); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + for (int k = 0; k < 16; k++) { + if (i != 0 && i != 15 && j != 0 && j != 15 && k != 0 && k != 15) { + continue; + } + double dx = i / 15.0F * 2.0F - 1.0F; + double dy = j / 15.0F * 2.0F - 1.0F; + double dz = k / 15.0F * 2.0F - 1.0F; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + dx /= len; + dy /= len; + dz /= len; + float strength = size * (0.7F + random.nextFloat() * 0.6F); + double cx = x; + double cy = y; + double cz = z; + for (float step = 0.3F; strength > 0.0F; strength -= step * 0.75F) { + cursor.set(Mth.floor(cx), Mth.floor(cy), Mth.floor(cz)); + BlockState state = level.getBlockState(cursor); + if (!state.isAir()) { + strength -= (state.getBlock().getExplosionResistance() + 0.3F) * step; + } + if (strength > 0.0F) { + out.add(cursor.immutable()); + } + cx += dx * step; + cy += dy * step; + cz += dz * step; + } + } + } + } + return out; + } + + /** {@code effectExplosionInWater}: a dome of spray from the water around the charge. */ + private static void splash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + int range = Math.max((int) (size + 0.5), 1); + int ex = (int) (x + 0.5); + int ey = (int) (y + 0.5); + int ez = (int) (z + 0.5); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int dy = -range; dy <= range; dy++) { + for (int dx = -range; dx <= range; dx++) { + for (int dz = -range; dz <= range; dz++) { + if (dx * dx + dy * dy + dz * dz >= range * range) { + continue; + } + cursor.set(ex + dx, ey + dy, ez + dz); + if (!level.getFluidState(cursor).is(FluidTags.WATER)) { + continue; + } + for (int n = random.nextInt(2); n > 0; n--) { + double up = 1.0 - Math.sqrt(dx * dx + dz * dz) / range + random.nextFloat() * 0.4 * range * 0.4; + level.addAlwaysVisibleParticle(ParticleTypes.SPLASH, true, cursor.getX(), cursor.getY() + 1.0, cursor.getZ(), + (double) dx / range * (random.nextFloat() - 0.2), up, (double) dz / range * (random.nextFloat() - 0.2)); + level.addAlwaysVisibleParticle(ParticleTypes.BUBBLE, true, cursor.getX(), cursor.getY(), cursor.getZ(), 0.0, up * 0.5, 0.0); + } + } + } + } + } +} From 460a4a0da7c18e15becf291e38f5bb61ceae6c84 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:56 +0800 Subject: [PATCH 0324/1461] Add weapon/Explosions.java to the 26.2 shared sources --- .../jnhprojects/mcheli/weapon/Explosions.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java new file mode 100644 index 00000000..f7fe3e89 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java @@ -0,0 +1,217 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.network.ExplosionEffectPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerExplosion; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Port of {@code MCH_Explosion}. + * + *

The original explosion had two sizes: one for blocks and one, doubled, for entities, with its + * own damage curve and the weapon's per-target {@code DamageFactor}. Entity damage is computed + * here as it was; the blocks, particles and sound go through a vanilla explosion of the block size, + * so mob griefing rules, protection mods and the client effects all behave as players expect. + */ +public final class Explosions { + /** Players this far away still see the smoke of an explosion. */ + private static final double EFFECT_RANGE = 256.0; + /** Share of the blast knockback an empty vehicle takes; they are far heavier than a mob. */ + private static final double VEHICLE_PUSH = 0.25; + + private Explosions() { + } + + /** What an explosion request needs besides its position and size. */ + public record Params(@Nullable Entity exploder, @Nullable Entity cause, float size, float blockSize, + boolean flaming, boolean destroyBlocks, int fireSeconds, boolean inWater, + @Nullable WeaponDefinition weapon) { + } + + /** + * @return whether any entity took damage, which the shooter hears as a hit confirmation + */ + public static boolean explode(ServerLevel level, double x, double y, double z, Params p) { + Vec3 center = new Vec3(x, y, z); + boolean hit = damageEntities(level, center, p); + + boolean breakBlocks = p.destroyBlocks && p.blockSize > 0.0F && ServerConfig.explosionsBreakBlocks(); + float radius = breakBlocks ? p.blockSize : Math.max(p.size, 0.5F); + level.explode(p.exploder, explosionSource(level, p), new Calculator(p.inWater, level), + x, y, z, radius, p.flaming && breakBlocks && ServerConfig.explosionsStartFires(), + breakBlocks ? Level.ExplosionInteraction.MOB : Level.ExplosionInteraction.NONE); + ModNetwork.sendNear(level, x, y, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y, z, p.size, p.inWater)); + if (!p.inWater) { + throwDebris(level, x, y, z, p.size); + } + return hit; + } + + /** + * The look of a vehicle going up when it crashes: no damage or blocks of its own (the crash + * explosion does those), but the {@link ExplosionEffect#crash big fireball} on every nearby + * client and twice the burning debris. Drawn a little above the impact so its smoke spreads + * into the air the way a missile hit's does, rather than straight into the ground. + */ + public static void crashFireball(ServerLevel level, double x, double y, double z, float size) { + float visual = Math.clamp(size * 2.0F, 6.0F, ExplosionEffect.MAX_CRASH_SIZE); + double lift = visual * 0.25; + ModNetwork.sendNear(level, x, y + lift, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y + lift, z, visual, false, true)); + throwDebris(level, x, y, z, visual, 24); + } + + private static void throwDebris(ServerLevel level, double x, double y, double z, float size) { + throwDebris(level, x, y, z, size, 12); + } + + /** + * Big blasts throw burning debris that arcs away trailing smoke, which the original made of + * flares. One piece per point of size, up to {@code maxPieces}. + */ + private static void throwDebris(ServerLevel level, double x, double y, double z, float size, int maxPieces) { + if (size < 4.0F) { + return; + } + int pieces = Math.min((int) size * maxPieces / 12, maxPieces); + for (int i = 0; i < pieces; i++) { + double angle = Math.toRadians(level.getRandom().nextInt(360)); + double speed = Math.min(size / 12.0F, 0.6) * (0.5F + level.getRandom().nextFloat() * 0.5F); + FlareEntity debris = ModEntities.FLARE.get().create(level, EntitySpawnReason.TRIGGERED); + if (debris != null) { + double spread = size / 2.0; + debris.launch(x + (level.getRandom().nextDouble() - 0.5) * spread, y + 2.0, z + (level.getRandom().nextDouble() - 0.5) * spread, + Math.sin(angle) * speed, (1.0 + level.getRandom().nextFloat() * 0.2) * speed, Math.cos(angle) * speed, 2.0F, 0, false); + level.addFreshEntity(debris); + } + } + } + + private static DamageSource explosionSource(ServerLevel level, Params p) { + return level.damageSources().explosion(p.exploder, p.cause); + } + + /** {@code doExplosionA} for entities: twice the block size, the 1.7.10 damage curve and knockback. */ + private static boolean damageEntities(ServerLevel level, Vec3 center, Params p) { + float size = p.size * 2.0F; + if (size <= 0.0F) { + return false; + } + AABB area = new AABB(center, center).inflate(size + 1.0); + DamageSource source = explosionSource(level, p); + boolean hit = false; + for (Entity entity : level.getEntities(p.exploder, area)) { + double d7 = Math.sqrt(entity.distanceToSqr(center)) / size; + if (d7 > 1.0) { + continue; + } + double dx = entity.getX() - center.x; + double dy = entity.getEyeY() - center.y; + double dz = entity.getZ() - center.z; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist == 0.0) { + continue; + } + dx /= dist; + dy /= dist; + dz /= dist; + double exposure = ServerExplosion.getSeenPercent(center, entity); + double impact = (1.0 - d7) * exposure; + float damage = (int) ((impact * impact + impact) / 2.0 * 8.0 * size + 1.0); + if (damage > 0.0F && !(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) + && !(entity instanceof FallingBlockEntity)) { + // Our own rounds caught in the blast do not count as hitting something. + if (!(entity instanceof ProjectileEntity bullet) || bullet.user() != p.cause) { + hit = true; + } + } + Combat.hurt(entity, source, damage, p.weapon); + if (entity instanceof AircraftEntity vehicle) { + // Not in the original: a vehicle someone is driving is simulated on their client, + // so a push here only snaps it back a step. An empty one still rocks a little. + if (vehicle.getControllingPassenger() == null) { + entity.push(dx * impact * VEHICLE_PUSH * 0.4, dy * impact * VEHICLE_PUSH * 0.1, dz * impact * VEHICLE_PUSH * 0.4); + } + } else if (!(entity instanceof ProjectileEntity)) { + entity.push(dx * impact * 0.4, dy * impact * 0.1, dz * impact * 0.4); + } + if (damage > 0.0F && p.fireSeconds > 0) { + double fire = 1.0 - dist / size; + if (fire > 0.0) { + entity.igniteForSeconds((float) (fire * p.fireSeconds)); + } + } + } + return hit; + } + + /** + * Vanilla explosion settings for the block pass: no entity damage or knockback (done above), + * and under water the blocks crumble the way the original's water charges made them. + */ + private static final class Calculator extends ExplosionDamageCalculator { + private final boolean inWater; + private final Level level; + + Calculator(boolean inWater, Level level) { + this.inWater = inWater; + this.level = level; + } + + // Vanilla's own calculator reads the block's resistance the same deprecated way. + @SuppressWarnings("deprecation") + @Override + public Optional getBlockExplosionResistance(Explosion explosion, BlockGetter getter, BlockPos pos, + BlockState block, FluidState fluid) { + if (!inWater) { + return super.getBlockExplosionResistance(explosion, getter, pos, block, fluid); + } + if (block.isAir()) { + return Optional.empty(); + } + float resistance = block.getBlock().getExplosionResistance(); + return Optional.of(resistance * (level.getRandom().nextFloat() * 0.2F + 0.2F)); + } + + @Override + public boolean shouldDamageEntity(Explosion explosion, Entity entity) { + return false; + } + + @Override + public float getKnockbackMultiplier(Entity entity) { + return 0.0F; + } + } + + /** {@code playExplosionSound}: the bang of a round that has no explosive charge. */ + public static void playSound(Level level, double x, double y, double z) { + float pitch = (1.0F + (level.getRandom().nextFloat() - level.getRandom().nextFloat()) * 0.2F) * 0.7F; + level.playSound(null, x, y, z, SoundEvents.GENERIC_EXPLODE.value(), SoundSource.BLOCKS, 4.0F, pitch); + } +} From 0155759b170cf73a149f55205eb110b1c535bd79 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:57 +0800 Subject: [PATCH 0325/1461] Add weapon/FlareDispenser.java to the 26.2 shared sources --- .../mcheli/weapon/FlareDispenser.java | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java new file mode 100644 index 00000000..12ddc658 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java @@ -0,0 +1,195 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.level.block.LevelEvent; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and smoke dischargers, ported from {@code MCH_Flare} and the flare half of + * {@code MCH_MissileDetector}. While a salvo burns, every missile homing on the vehicle within 60 + * blocks is decoyed. + * + *

The ten {@code FlareType} values pick a pattern: 1 random scatter, 2 and 3 to the sides and + * rear, 4 and 5 trailing, 10 a ring of smoke grenades. Unknown types fall back to type 1. + */ +public final class FlareDispenser { + private static final int DECOY_RANGE = 60; + + /** {@code num, interval, tickWait, tickEnable, numFlareMax} per type. */ + private static final int[][] TYPES = new int[11][]; + + static { + TYPES[1] = new int[]{1, 3, 200, 100, 16}; + TYPES[2] = new int[]{3, 5, 300, 200, 16}; + TYPES[3] = new int[]{2, 3, 200, 100, 16}; + TYPES[4] = new int[]{1, 3, 200, 100, 16}; + TYPES[5] = new int[]{2, 3, 200, 100, 16}; + TYPES[10] = new int[]{8, 1, 250, 60, 1}; + for (int i : new int[]{0, 6, 7, 8, 9}) { + TYPES[i] = TYPES[1]; + } + } + + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final RandomSource random = RandomSource.create(); + private int tick; + private int fired; + private int type; + private int typeIndex; + + FlareDispenser(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + } + + public boolean isFitted() { + return !def.flareTypes.isEmpty(); + } + + /** Waiting out the reload of the last salvo. */ + public boolean isPreparing() { + return tick != 0; + } + + /** The salvo is still burning, so incoming missiles are being decoyed. */ + public boolean isActive() { + int[] p = params(type); + return tick != 0 && tick > p[2] - p[3]; + } + + public int ticksLeft() { + return tick; + } + + public int cooldown() { + return params(type)[2]; + } + + public int currentType() { + return def.flareTypes.isEmpty() ? 0 : def.flareTypes.get(typeIndex % def.flareTypes.size()); + } + + public void nextType() { + if (!def.flareTypes.isEmpty()) { + typeIndex = (typeIndex + 1) % def.flareTypes.size(); + } + } + + /** {@code useFlare}: starts a salvo of the current type if the last one has finished. */ + public boolean use() { + if (!isFitted() || tick != 0 || vehicle.isDestroyed()) { + return false; + } + type = currentType(); + fired = 0; + // The client waited 50 ticks longer before showing the dispenser as ready again. + tick = params(type)[2] + (vehicle.level().isClientSide() ? 50 : 0); + return true; + } + + private static int[] params(int type) { + return type >= 0 && type < TYPES.length && TYPES[type] != null ? TYPES[type] : TYPES[1]; + } + + void tick() { + if (type <= 0 || vehicle.isRemoved()) { + return; + } + if (tick > 0) { + tick--; + } + int[] p = params(type); + if (!vehicle.level().isClientSide()) { + if (tick > 0 && tick % p[1] == 0 && fired < p[4]) { + Vec3 at = vehicle.toWorld(def.flarePosition); + spawnSalvo(new Vec3(at.x + vehicle.xo - vehicle.getX(), at.y + vehicle.yo - vehicle.getY(), at.z + vehicle.zo - vehicle.getZ())); + } + if (isActive()) { + decoyMissiles(); + } + } + } + + private void spawnSalvo(Vec3 at) { + fired++; + int[] p = params(type); + Vec3 m = vehicle.getDeltaMovement(); + double x = at.x - m.x * 2.0; + double y = at.y - m.y * 2.0 - 1.0; + double z = at.z - m.z * 2.0; + vehicle.level().levelEvent(LevelEvent.SOUND_DISPENSER_PROJECTILE_LAUNCH, net.minecraft.core.BlockPos.containing(x, y, z), 0); + for (int i = 0; i < p[0]; i++) { + double tx = 0.0; + double ty = m.y; + double tz = 0.0; + int fuse = 0; + double r = vehicle.getYRot(); + double fy = y; + switch (type) { + case 1 -> { + tx = Mth.sin(random.nextFloat() * 360.0F); + tz = Mth.cos(random.nextFloat() * 360.0F); + } + case 2, 3 -> { + if (i == 0) { + r += 90.0; + } else if (i == 1) { + r -= 90.0; + } else if (i == 2) { + r += 180.0; + } + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 0.6; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 0.6; + } + case 4 -> { + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 1.3; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 1.3; + } + case 5 -> { + r = Math.toRadians(r); + tx = (-Math.sin(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + tz = (Math.cos(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + } + default -> { + } + } + tx += m.x; + ty += m.y / 2.0; + tz += m.z; + if (type == 10) { + r += 360.0 / p[0] / 2.0 + i * (360.0 / p[0]); + r = Math.toRadians(r); + tx = -Math.sin(r) * 2.0; + tz = Math.cos(r) * 2.0; + ty = 0.7; + fy += 2.0; + fuse = 10; + } + FlareEntity flare = ModEntities.FLARE.get().create(vehicle.level(), EntitySpawnReason.TRIGGERED); + if (flare != null) { + flare.launch(x, fy, z, tx * 0.5, ty * 0.5, tz * 0.5, 6.0F, fuse, type == 4); + vehicle.level().addFreshEntity(flare); + } + } + } + + /** {@code destroyMissile}: missiles homing on this vehicle or its crew lose their target. */ + private void decoyMissiles() { + for (ProjectileEntity missile : vehicle.level().getEntitiesOfClass(ProjectileEntity.class, + vehicle.getBoundingBox().inflate(DECOY_RANGE), p -> p.target() != null)) { + if (missile.target() == vehicle || vehicle.hasPassenger(missile.target())) { + missile.decoy(); + } + } + } +} From 185a603967e14b716684ad3e66b8bf76fe5d2c4b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:57 +0800 Subject: [PATCH 0326/1461] Add weapon/Guidance.java to the 26.2 shared sources --- .../jnhprojects/mcheli/weapon/Guidance.java | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java new file mode 100644 index 00000000..b8142e92 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java @@ -0,0 +1,220 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Predicate; + +/** + * Lock-on for seeking missiles, ported from {@code MCH_WeaponGuidanceSystem}. It runs on the + * client of the crew member aiming; the locked entity travels to the server with the fire request. + * + *

A target is acquired inside a cone around the aim, then the lock count climbs while it stays + * in the cone. Stealth shrinks both the range and the cone and stretches the time to lock. + */ +public final class Guidance { + public final boolean canLockInAir; + public final boolean canLockOnGround; + public final boolean canLockInWater = false; + public final boolean ridableOnly; + public final double lockRange = 200.0; + public final int lockAngle = 5; + private final int lockCountMax; + private final @Nullable Predicate filter; + + private @Nullable Entity target; + private @Nullable Entity lastLocked; + private int lockCount; + private int prevLockCount; + private int continueLockCount; + private int soundCount; + + public Guidance(boolean air, boolean ground, boolean ridableOnly, int lockTime, @Nullable Predicate filter) { + this.canLockInAir = air; + this.canLockOnGround = ground; + this.ridableOnly = ridableOnly; + this.lockCountMax = Math.max(lockTime, 1); + this.filter = filter; + } + + public int lockCount() { + return lockCount; + } + + public int lockCountMax() { + float stealth = stealth(target); + return (int) (lockCountMax + lockCountMax * stealth); + } + + public @Nullable Entity target() { + return lockCount > 0 && target != null && !target.isRemoved() ? target : null; + } + + public @Nullable Entity lastLocked() { + return lastLocked; + } + + public boolean isLocked() { + return lockCount == lockCountMax() && lastLocked != null; + } + + /** Called every client tick: a lock decays unless {@link #lock} kept it alive this tick. */ + public void tick() { + if (lockCount != prevLockCount) { + prevLockCount = lockCount; + } else { + lockCount = 0; + prevLockCount = 0; + } + } + + /** + * Advances the lock for one tick of aiming from {@code user}'s view. + * + * @param eye where the view is seen from. The original used the player's position, which on a + * 1.7.10 client was eye height; gunner sights and launcher cameras sit higher still, + * and a 5 degree cone measured from the wrong point misses targets close by. + * @return true once the lock is complete; {@link #lastLocked()} is the target to send + */ + public boolean lock(Entity user, Vec3 eye) { + if (lockCount == 0) { + acquire(user, eye); + return false; + } + if (target == null || target.isRemoved() || !inLockableState(target)) { + clear(); + return false; + } + double range = lockRange * (1.0F - stealth(target)); + if (user.distanceToSqr(target) >= range * range) { + clear(); + return false; + } + soundCount = (soundCount + 1) % 15; + if (inCone(eye, user.getYRot(), user.getXRot(), target, lockAngle)) { + if (lockCount < lockCountMax()) { + lockCount++; + } + } else if (continueLockCount > 0) { + continueLockCount--; + if (continueLockCount <= 0 && lockCount > 0) { + lockCount--; + } + } else { + continueLockCount = 0; + lockCount--; + } + if (lockCount >= lockCountMax()) { + if (continueLockCount <= 0) { + continueLockCount = Math.min(lockCountMax() / 3, 20); + } + lastLocked = target; + prevLockCount = lockCount - 1; + return true; + } + return false; + } + + /** True on the tick the target should hear that it is being tracked. */ + public boolean shouldNotifyTarget() { + return lockCount > 0 && soundCount == 1; + } + + private void acquire(Entity user, Vec3 eye) { + Entity best = null; + double bestDistance = lockRange * lockRange * 2.0; + for (Entity entity : user.level().getEntities(user, user.getBoundingBox().inflate(lockRange), this::canLock)) { + double d = entity.distanceToSqr(user); + float stealth = 1.0F - stealth(entity); + double range = lockRange * stealth; + float angle = lockAngle * (stealth / 2.0F + 0.5F); + if (d < range * range && d < bestDistance && inCone(eye, user.getYRot(), user.getXRot(), entity, angle)) { + Vec3 to = new Vec3(entity.getX(), entity.getY() + entity.getBbHeight() / 2.0F, entity.getZ()); + HitResult hit = user.level().clip(new ClipContext(eye, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, user)); + if (hit.getType() == HitResult.Type.MISS) { + best = entity; + bestDistance = d; + } + } + } + target = best; + if (best != null) { + lockCount++; + } + } + + public void clear() { + target = null; + lockCount = 0; + continueLockCount = 0; + soundCount = 0; + } + + private boolean canLock(Entity entity) { + if (ridableOnly && entity instanceof Player && entity.getVehicle() == null) { + return false; + } + if (!(entity instanceof LivingEntity) && !(entity instanceof AircraftEntity)) { + return false; + } + if (filter != null && !filter.test(entity)) { + return false; + } + return inLockableState(entity); + } + + private boolean inLockableState(Entity entity) { + if (!canLockInWater && entity.isInWater()) { + return false; + } + boolean onGround = isOnGround(entity); + return onGround ? canLockOnGround : canLockInAir; + } + + /** On the ground, or less than twelve blocks above something solid. */ + public static boolean isOnGround(Entity entity) { + if (entity.onGround()) { + return true; + } + BlockPos.MutableBlockPos pos = BlockPos.containing(entity.getX() + 0.5, entity.getY() + 0.5, entity.getZ() + 0.5).mutable(); + for (int i = 0; i < 12; i++) { + if (!entity.level().getBlockState(pos).isAir() && !entity.level().getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + pos.move(0, -1, 0); + } + return false; + } + + public static float stealth(@Nullable Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + return vehicle.stealth(); + } + return entity != null && entity.getVehicle() instanceof AircraftEntity vehicle ? vehicle.stealth() : 0.0F; + } + + /** {@code inLockRange}: whether {@code target} is within {@code angle} degrees of the aim. */ + public static boolean inCone(Entity from, float yaw, float pitch, Entity target, float angle) { + return inCone(from.position(), yaw, pitch, target, angle); + } + + public static boolean inCone(Vec3 from, float yaw, float pitch, Entity target, float angle) { + double dx = target.getX() - from.x; + double dy = target.getY() + target.getBbHeight() / 2.0F - from.y; + double dz = target.getZ() - from.z; + float targetYaw = (float) Math.toDegrees(Math.atan2(dz, dx)); + float diffYaw = Mth.positiveModulo(targetYaw - yaw - 90.0F, 360.0F); + float targetPitch = -(float) Math.toDegrees(Math.atan2(dy, Math.sqrt(dx * dx + dz * dz))); + float diffPitch = targetPitch - pitch; + return (diffYaw < angle || diffYaw > 360.0F - angle) && Math.abs(diffPitch) < angle; + } +} From 9ed4017e5ff6b6b3abdad26ab85375af7fedf442 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:58 +0800 Subject: [PATCH 0327/1461] Add weapon/LegacyItems.java to the 26.2 shared sources --- .../mcheli/weapon/LegacyItems.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java new file mode 100644 index 00000000..f763f73f --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import org.jspecify.annotations.Nullable; + +/** + * Maps the 1.7.10 item names and damage values in the content files to today's items. Names + * that still exist resolve directly; the few that were renamed or flattened are listed here. + */ +public final class LegacyItems { + private static final String[] DYES = { + "ink_sac", "red_dye", "green_dye", "cocoa_beans", "lapis_lazuli", "purple_dye", "cyan_dye", "light_gray_dye", + "gray_dye", "pink_dye", "lime_dye", "yellow_dye", "light_blue_dye", "magenta_dye", "orange_dye", "bone_meal"}; + + private LegacyItems() { + } + + public static @Nullable Item item(String name, int damage) { + if (name.isEmpty()) { + return null; + } + String id = switch (name) { + case "dye" -> DYES[Math.floorMod(damage, DYES.length)]; + case "boat" -> "oak_boat"; + case "reeds" -> "sugar_cane"; + case "wool" -> "white_wool"; + case "log" -> "oak_log"; + case "planks" -> "oak_planks"; + default -> name.startsWith("minecraft:") ? name.substring("minecraft:".length()) : name; + }; + Identifier key = Identifier.tryParse(id.contains(":") ? id : "minecraft:" + id); + if (key == null) { + return null; + } + Item item = BuiltInRegistries.ITEM.getValue(key); + return item == Items.AIR ? null : item; + } +} From 1b423251fa92b2804e9d1dabe3b199113d73ec59 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:58 +0800 Subject: [PATCH 0328/1461] Add weapon/ModDamageTypes.java to the 26.2 shared sources --- .../mcheli/weapon/ModDamageTypes.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java new file mode 100644 index 00000000..b4007926 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * Damage types defined in {@code data/mcheli/damage_type}. Bullets and missiles use their own type + * so death messages name the gun instead of calling it a thrown snowball, which is what the + * original's {@code causeThrownDamage} amounted to. + */ +public final class ModDamageTypes { + public static final ResourceKey BULLET = + ResourceKey.create(Registries.DAMAGE_TYPE, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "bullet")); + + private ModDamageTypes() { + } + + public static DamageSource bullet(Level level, Entity projectile, @Nullable Entity owner) { + return new DamageSource(level.registryAccess().lookupOrThrow(Registries.DAMAGE_TYPE).getOrThrow(BULLET), projectile, owner); + } +} From bd9891ee8314951b57fb1eeac59cecc28224b6c0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:58 +0800 Subject: [PATCH 0329/1461] Add weapon/Resupply.java to the 26.2 shared sources --- .../jnhprojects/mcheli/weapon/Resupply.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java new file mode 100644 index 00000000..533f5fba --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** + * Resupplying a weapon from a crew member's own items, the {@code supplyAmmo} path of the + * original's vehicle screen. One supply costs the weapon's {@code Item} lines and adds + * {@code SuppliedNum} rounds; it only works while the vehicle stands on something. + */ +public final class Resupply { + private Resupply() { + } + + /** {@code canPlayerSupplyAmmo}: whether {@link #supply} would do anything right now. */ + public static boolean canSupply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.set(weapon) : null; + if (set == null || vehicle.isDestroyed() || player.getVehicle() != vehicle || !vehicle.canSupply()) { + return false; + } + if (set.ammo() + set.restAmmo() >= set.maxAmmo()) { + return false; + } + return player.getAbilities().instabuild || canPay(player.getInventory(), set.definition()); + } + + /** Pays for and adds one supply of {@code weapon}. Server only. */ + public static boolean supply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + if (!canSupply(player, vehicle, weapon)) { + return false; + } + WeaponSet set = vehicle.weapons().set(weapon); + if (!player.getAbilities().instabuild) { + pay(player.getInventory(), set.definition()); + } + set.supply(); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + return true; + } + + private static boolean canPay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null || inventory.countItem(item) < cost.count()) { + return false; + } + } + return true; + } + + private static void pay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + int left = cost.count(); + for (int i = 0; i < inventory.getContainerSize() && left > 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.is(item)) { + continue; + } + if (item == Items.WATER_BUCKET || item == Items.LAVA_BUCKET) { + // Buckets give their bucket back, one at a time. + inventory.setItem(i, new ItemStack(Items.BUCKET)); + left--; + } else { + int take = Math.min(left, stack.getCount()); + stack.shrink(take); + left -= take; + } + } + } + inventory.setChanged(); + } +} From c8d8f2b63f8bcd3a3fd67a627a0ca18886e232c5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:59 +0800 Subject: [PATCH 0330/1461] Add weapon/ServerFiring.java to the 26.2 shared sources --- .../mcheli/weapon/ServerFiring.java | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java new file mode 100644 index 00000000..3f867744 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java @@ -0,0 +1,418 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * What each weapon type does when the server fires it: the {@code shot} methods of the + * {@code MCH_Weapon*} classes. Every method returns whether a round was used. + */ +public final class ServerFiring implements WeaponSet.Trigger { + public static final ServerFiring INSTANCE = new ServerFiring(); + private static final RandomSource RANDOM = RandomSource.create(); + + private ServerFiring() { + } + + @Override + public boolean fire(WeaponSet set, int mount, Shot shot) { + if (!(shot.shooter.level() instanceof ServerLevel level)) { + return false; + } + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_1, MACHINE_GUN_2 -> gun(level, set, shot); + case ROCKET -> rocket(level, set, shot); + case BOMB -> bomb(level, set, shot); + case AA_MISSILE -> aaMissile(level, set, mount, shot); + case AS_MISSILE -> asMissile(level, set, shot); + case AT_MISSILE -> atMissile(level, set, mount, shot); + case TV_MISSILE -> tvMissile(level, set, mount, shot); + case TORPEDO -> torpedo(level, set, shot); + case CAS -> cas(level, set, shot); + case MARKER_ROCKET -> markerRocket(level, set, shot); + case DISPENSER -> dispenser(level, set, shot); + case TARGETING_POD -> targetingPod(level, set, shot); + case DUMMY, SMOKE -> false; + case null -> false; + }; + } + + private static @Nullable T spawn(ServerLevel level, EntityType type, WeaponSet set, Shot shot, + Vec3 dir, float yaw, float pitch, double acceleration) { + T e = type.create(level, EntitySpawnReason.TRIGGERED); + if (e == null) { + return null; + } + e.launch(shot.x, shot.y, shot.z, dir.x, dir.y, dir.z, yaw, pitch, acceleration); + e.setWeapon(set.name()); + e.setParameters(set.definition(), shot.shooter, shot.user); + return e; + } + + private static Vec3 forward(float yaw, float pitch) { + return Vec3.directionFromRotation(pitch, yaw); + } + + private static void sound(WeaponSet set, Entity at) { + sound(set, at, soundName(set)); + } + + private static String soundName(WeaponSet set) { + return set.ownSound != null ? set.ownSound.sound() : set.definition().sound; + } + + private static void sound(WeaponSet set, Entity at, String name) { + if (set.soundAllowed) { + WeaponDefinition w = set.definition(); + float rnd = w.soundPitchRandom; + float pitch = set.ownSound != null && set.ownSound.pitch() != null ? set.ownSound.pitch() : w.soundPitch; + ModSounds.playAt(at, name, w.soundVolume, pitch * (1.0F - rnd) + RANDOM.nextFloat() * rnd); + } + } + + // Guns, rockets, bombs + + private static boolean gun(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + BulletEntity e = spawn(level, ModEntities.BULLET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, w.acceleration); + if (e == null) { + return false; + } + if (w.type == WeaponType.MACHINE_GUN_2 && w.modeNum >= 2) { + // Mode 0 fires ball ammunition, which only bangs; mode 1 is high explosive. + e.explosionPower = shot.option1 == 0 ? -w.explosion : w.explosion; + } + Vec3 m = e.getDeltaMovement(); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean rocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + RocketEntity e = spawn(level, ModEntities.ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + if (shot.option1 == 0 && set.definition().modeNum > 1) { + e.piercing = 0; + } + level.addFreshEntity(e); + return true; + } + + private static boolean bomb(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + if (w.destruct) { + // A drone's self destruct charge. + if (shot.shooter instanceof HelicopterEntity drone && drone.definition() != null && drone.definition().uav) { + Explosions.explode(level, drone.getX(), drone.getY(), drone.getZ(), + new Explosions.Params(null, shot.user, w.explosion, w.explosionBlock, w.flaming, true, 0, false, null)); + sound(set, shot.shooter); + drone.ejectPassengers(); + drone.discard(); + } + return true; + } + sound(set, shot.shooter); + Vec3 motion = shot.shooter.getDeltaMovement(); + BombEntity e = spawn(level, ModEntities.BOMB.get(), set, shot, motion, shot.shooter.getYRot(), 0.0F, w.acceleration); + if (e == null) { + return false; + } + e.setDeltaMovement(motion); + level.addFreshEntity(e); + return true; + } + + // Missiles + + private static float fixedYaw(WeaponSet set, int mount) { + return set.mounts().get(mount).yaw(); + } + + private static float fixedPitch(WeaponSet set, int mount) { + return set.mounts().get(mount).pitch(); + } + + private static boolean aaMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + sound(set, shot.shooter); + float yaw = shot.shooter.getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + AAMissileEntity e = spawn(level, ModEntities.AA_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + level.addFreshEntity(e); + return true; + } + + /** Ray from the shooter along the aimer's view, as the original's AS missile and CAS sights. */ + private static @Nullable BlockHitResult sight(ServerLevel level, Shot shot, double eyeHeight, double range) { + Entity aimer = shot.aimer(); + Vec3 from = shot.shooter.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(forward(aimer.getYRot(), aimer.getXRot()).scale(range)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + return hit.getType() == HitResult.Type.BLOCK ? hit : null; + } + + private static boolean nextToWater(ServerLevel level, BlockPos pos) { + if (level.getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + for (Direction d : Direction.values()) { + if (level.getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + private static boolean asMissile(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 1.62, 250.0); + if (hit == null || nextToWater(level, hit.getBlockPos())) { + return false; + } + Entity aimer = shot.aimer(); + ASMissileEntity e = spawn(level, ModEntities.AS_MISSILE.get(), set, shot, forward(aimer.getYRot(), aimer.getXRot()), + aimer.getYRot(), aimer.getXRot(), set.definition().acceleration); + if (e == null) { + return false; + } + e.targetX = hit.getLocation().x; + e.targetY = hit.getLocation().y; + e.targetZ = hit.getLocation().z; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean atMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + ATMissileEntity e = spawn(level, ModEntities.AT_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + e.guidanceType = shot.option2; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean tvMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.aimer().getXRot() + fixedPitch(set, mount); + boolean tvGuided = shot.option1 == 0; + float acceleration = set.definition().acceleration * (tvGuided ? 1.0F : 1.5F); + TvMissileEntity e = spawn(level, ModEntities.TV_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, acceleration); + if (e == null) { + return false; + } + level.addFreshEntity(e); + if (shot.shooter instanceof AircraftEntity vehicle) { + vehicle.setTvMissile(e); + } + sound(set, shot.shooter); + return true; + } + + private static boolean torpedo(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Vec3 vehicleMotion = shot.shooter.getDeltaMovement(); + float yaw = shot.yaw; + float pitch = shot.pitch; + Vec3 target = null; + if (w.guidedTorpedo) { + Entity aimer = shot.aimer(); + yaw = aimer.getYRot(); + pitch = aimer.getXRot(); + Vec3 from = aimer.position(); + Vec3 to = from.add(forward(yaw, pitch).scale(150.0)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + if (hit.getType() != HitResult.Type.BLOCK || !nextToWater(level, hit.getBlockPos())) { + return false; + } + target = hit.getLocation(); + } + Vec3 m = forward(yaw, pitch).scale(w.acceleration).add(vehicleMotion); + double speed = m.length(); + TorpedoEntity e = spawn(level, ModEntities.TORPEDO.get(), set, shot, m, yaw, 0.0F, speed); + if (e == null) { + return false; + } + e.setDeltaMovement(m); + e.accelerationInWater = w.accelerationInWater; + if (target != null) { + e.targetX = target.x; + e.targetY = target.y; + e.targetZ = target.z; + } + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + // Support weapons + + private static boolean markerRocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + MarkerRocketEntity e = spawn(level, ModEntities.MARKER_ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + e.setStatus(1); + level.addFreshEntity(e); + return true; + } + + private static boolean dispenser(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + DispensedItemEntity e = spawn(level, ModEntities.DISPENSED_ITEM.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + Vec3 m = shot.shooter.getDeltaMovement().add(e.getDeltaMovement().scale(0.5)); + e.setDeltaMovement(m); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + return true; + } + + /** + * Spots everything of the weapon's {@code Target} kinds inside its cone, which then glows for + * {@code MarkTime}. Marking a block for the team needs the team manager of a later phase. + */ + private static boolean targetingPod(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Entity aimer = shot.aimer(); + if ((w.target & WeaponDefinition.TARGET_BLOCK) != 0) { + // {@code MCH_Multiplay.markPoint}: the block under the crew's view, 300 blocks out, goes to the team. + boolean marked = aimer instanceof ServerPlayer player && Spotting.markPoint(player, new Vec3(shot.x, shot.y, shot.z), 300.0); + sound(set, aimer, marked ? soundName(set) : "ng"); + return true; + } + List spotted = new ArrayList<>(); + double range = Math.max(w.length, 1.0F); + for (LivingEntity entity : level.getEntitiesOfClass(LivingEntity.class, aimer.getBoundingBox().inflate(range))) { + if (entity == aimer || shot.shooter.hasPassenger(entity) || aimer.distanceToSqr(entity) > range * range + || !matchesTarget(entity, w.target) || !Guidance.inCone(aimer, aimer.getYRot(), aimer.getXRot(), entity, w.angle)) { + continue; + } + spotted.add(entity.getId()); + } + if (aimer instanceof ServerPlayer player) { + Spotting.tellTeam(player, spotted, Math.max(w.markTime, 20)); + } + sound(set, shot.shooter, !spotted.isEmpty() ? soundName(set) : "ng"); + return true; + } + + private static boolean matchesTarget(LivingEntity entity, int bits) { + Entity vehicle = entity.getVehicle(); + if (vehicle instanceof PlaneEntity) { + return (bits & WeaponDefinition.TARGET_PLANES) != 0; + } + if (vehicle instanceof HelicopterEntity) { + return (bits & WeaponDefinition.TARGET_HELICOPTERS) != 0; + } + if (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity) { + return (bits & WeaponDefinition.TARGET_GROUND_VEHICLES) != 0; + } + if (entity instanceof Player) { + return (bits & WeaponDefinition.TARGET_PLAYERS) != 0; + } + if (entity instanceof Enemy) { + return (bits & WeaponDefinition.TARGET_MONSTERS) != 0; + } + return (bits & WeaponDefinition.TARGET_OTHERS) != 0; + } + + // Close air support + + private static boolean cas(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 2.0, 150.0); + if (hit == null) { + return false; + } + Vec3 at = hit.getLocation(); + set.casX = at.x; + set.casY = at.y; + set.casZ = at.z; + set.casDirection = Math.floorMod(CasStrike.approach(shot.aimer().getYRot()), 4); + set.casUser = shot.user; + set.casShooter = shot.shooter; + set.casStartTick = CasStrike.FIRST_PASS; + set.casCount = 0; + if (shot.user instanceof ServerPlayer player) { + ModSounds.playTo(player, set.definition().sound, 1.0F, 1.0F); + } + return true; + } + + /** {@code MCH_WeaponCAS.update}: three passes, 45 ticks apart, the later ones offset to the sides. */ + static void tick(AircraftEntity vehicle, WeaponSet set) { + if (set.casCount >= 3 || set.countWait == 0 || set.ticksSinceShot(0) != set.casStartTick + || !(vehicle.level() instanceof ServerLevel level)) { + return; + } + Vec3 offset = CasStrike.passOffset(set.casCount, set.casDirection); + CasStrike.spawnA10(level, set.name(), set.definition(), set.casDirection, set.casX + offset.x, set.casY + 20.0, + set.casZ + offset.z, set.casShooter, set.casUser); + set.casStartTick = set.ticksSinceShot(0) + 45; + set.casCount++; + } +} From 313aa0062c3b25d7076703cf338a97255256dde8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:41:59 +0800 Subject: [PATCH 0331/1461] Add weapon/Shot.java to the 26.2 shared sources --- .../com/jnhprojects/mcheli/weapon/Shot.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java new file mode 100644 index 00000000..6a9f53cf --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java @@ -0,0 +1,40 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +/** + * One trigger pull, the port of {@code MCH_WeaponParam}. The weapon set fills in the aim and the + * muzzle position; the client fills in the two option values (a locked target, a fire mode) and + * the server receives them with the fire request. + */ +public final class Shot { + /** The vehicle firing, or the player for hand held weapons. */ + public final Entity shooter; + /** Who pulled the trigger. Damage and kills are credited to this entity. */ + public final @Nullable Entity user; + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + public float roll; + public int option1; + public int option2; + public boolean infiniteAmmo; + /** Set by the weapon set when the magazine ran dry with this shot. */ + public boolean reload; + + public Shot(Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + this.x = shooter.getX(); + this.y = shooter.getY(); + this.z = shooter.getZ(); + } + + /** The entity whose view steers guided weapons: the user if there is one, else the shooter. */ + public Entity aimer() { + return user != null ? user : shooter; + } +} From e545846292efaa0802c3f757268c46fa4c325af7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:00 +0800 Subject: [PATCH 0332/1461] Add weapon/Spotting.java to the 26.2 shared sources --- .../jnhprojects/mcheli/weapon/Spotting.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java new file mode 100644 index 00000000..b9331154 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.network.MarkerPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +/** + * Spotting with the rangefinder, {@code MCH_Multiplay.spotEntity} and {@code markPoint}. + * + *

Spotted targets and marked points go to the spotter and their team only, as screen markers + * the HUD draws ({@code MCH_GuiTargetMarker}); nobody else sees them. + */ +public final class Spotting { + // The original's target filter bits. + public static final int OTHERS = 1; + public static final int MONSTERS = 2; + public static final int PLAYERS = 4; + public static final int GROUND_VEHICLES = 8; + public static final int HELICOPTERS = 16; + public static final int PLANES = 32; + + /** {@code RangeFinderSpotDist}. */ + public static final double RANGE = 400.0; + /** {@code RangeFinderSpotTime} in ticks, for players and vehicles. */ + private static final int PLAYER_TIME = 15 * 20; + /** Mobs stay marked for a minute. */ + private static final int MOB_TIME = 60 * 20; + /** Half the width of the cone a spot covers, in degrees: the original's angle times two. */ + private static final double CONE = 40.0; + + private Spotting() { + } + + static boolean matches(int filter, Entity entity) { + int bit; + if (entity instanceof PlaneEntity) { + bit = PLANES; + } else if (entity instanceof HelicopterEntity) { + bit = HELICOPTERS; + } else if (entity instanceof AircraftEntity) { + bit = GROUND_VEHICLES; + } else if (entity instanceof Player) { + bit = PLAYERS; + } else if (entity instanceof Enemy) { + bit = MONSTERS; + } else if (entity instanceof LivingEntity) { + bit = OTHERS; + } else { + return false; + } + return (filter & bit) != 0; + } + + /** + * Marks what {@code spotter} sees within {@link #RANGE} and the cone around their view: never + * a teammate, and only targets in plain sight. + * + * @return whether anything was spotted + */ + public static boolean spot(ServerPlayer spotter, int filter) { + ServerLevel level = spotter.level(); + Vec3 eye = spotter.getEyePosition(); + Vec3 look = spotter.getLookAngle(); + int ticks = (filter & ~(OTHERS | MONSTERS)) == 0 ? MOB_TIME : PLAYER_TIME; + List spotted = new ArrayList<>(); + for (Entity entity : level.getEntities(spotter, spotter.getBoundingBox().inflate(RANGE), e -> matches(filter, e))) { + if (isFriendly(spotter, entity)) { + continue; + } + Vec3 to = entity.getBoundingBox().getCenter().subtract(eye); + double dist = to.length(); + if (dist < 1.0 || dist > RANGE || Math.toDegrees(Math.acos(to.scale(1.0 / dist).dot(look))) > CONE) { + continue; + } + if (level.clip(new ClipContext(eye, entity.getBoundingBox().getCenter(), ClipContext.Block.COLLIDER, + ClipContext.Fluid.NONE, spotter)).getType() == HitResult.Type.BLOCK) { + continue; + } + spotted.add(entity.getId()); + } + tellTeam(spotter, spotted, ticks); + return !spotted.isEmpty(); + } + + /** Sends the spotted entities to {@code spotter} and their team, {@code MCH_PacketNotifySpotedEntity}. */ + public static void tellTeam(ServerPlayer spotter, List entities, int ticks) { + if (entities.isEmpty()) { + return; + } + MarkerPacket packet = MarkerPacket.spotted(entities.stream().mapToInt(Integer::intValue).toArray(), ticks); + for (ServerPlayer player : TeamManager.teammates(spotter)) { + ModNetwork.sendToPlayer(player, packet); + } + } + + /** Teammates, and a vehicle crewed by one, are never spotted. */ + private static boolean isFriendly(ServerPlayer spotter, Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + for (Entity crew : vehicle.getPassengers()) { + if (crew.isAlliedTo(spotter)) { + return true; + } + } + return false; + } + return entity.isAlliedTo(spotter); + } + + /** + * Tells the spotter and their team where they are looking, as block coordinates. + * + * @return whether the view ended on a block within range + */ + public static boolean markPoint(ServerPlayer spotter) { + return markPoint(spotter, spotter.getEyePosition(), RANGE); + } + + /** + * As above, looking from {@code from} along the spotter's view: a targeting pod marks from + * the vehicle, out to {@code range}. + */ + public static boolean markPoint(ServerPlayer spotter, Vec3 from, double range) { + ServerLevel level = spotter.level(); + BlockHitResult hit = level.clip(new ClipContext(from, from.add(spotter.getLookAngle().scale(range)), + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, spotter)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + BlockPos pos = hit.getBlockPos(); + MarkerPacket marker = MarkerPacket.point(pos); + Component message = Component.translatable("message.mcheli.mark_point", spotter.getDisplayName(), pos.getX(), pos.getY(), pos.getZ()); + for (ServerPlayer player : level.getServer().getPlayerList().getPlayers()) { + if (player == spotter || player.isAlliedTo(spotter)) { + player.sendSystemMessage(message); + ModNetwork.sendToPlayer(player, marker); + } + } + return true; + } +} From 6d23229d9d38651205d42966c6967854097d76f5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:00 +0800 Subject: [PATCH 0333/1461] Add weapon/VehicleWeapons.java to the 26.2 shared sources --- .../mcheli/weapon/VehicleWeapons.java | 597 ++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java new file mode 100644 index 00000000..85dee9f8 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java @@ -0,0 +1,597 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.weapon.MuzzleFlash; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.WeaponStatePacket; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The armament of one vehicle: its weapon sets, which one each seat has selected, the flares, and + * the effects every client derives from the synced firing state. Ported from the weapon handling + * in {@code MCH_EntityAircraft} ({@code createWeapon}, {@code switchWeapon}, + * {@code useCurrentWeapon}, {@code updateWeapons} and {@code updateWeaponsRotation}). + */ +public final class VehicleWeapons { + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final boolean clientSide; + private final List sets = new ArrayList<>(); + private final int[] selected; + private final @Nullable Guidance[] guidance; + private final RandomSource random = RandomSource.create(); + + /** One bit per mount, set while that mount shows muzzle effects. Synced by the vehicle. */ + private int useStat; + private int prevUseStat; + + private final float[] bayAngle; + private final float[] prevBayAngle; + + private final int[] sentAmmo; + private final int[] sentRest; + + public final FlareDispenser flares; + + // Recoil that kicks the whole hull, from Recoil in the weapon file. + private int recoilCount; + private float recoilValue; + private float recoilYaw; + + public VehicleWeapons(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + this.clientSide = vehicle.level().isClientSide(); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + if (weapon != null && !group.mounts().isEmpty()) { + WeaponSet set = new WeaponSet(weapon, group.mounts(), clientSide); + set.ownSound = def.weaponSounds.get(group.weaponType()); + sets.add(set); + } + } + selected = new int[def.seatCount()]; + Arrays.fill(selected, -1); + guidance = new Guidance[sets.size()]; + for (int i = 0; i < sets.size(); i++) { + WeaponDefinition w = sets.get(i).definition(); + if (w.type == WeaponType.AA_MISSILE || w.type == WeaponType.AT_MISSILE) { + boolean aa = w.type == WeaponType.AA_MISSILE; + guidance[i] = new Guidance(aa, !aa, w.ridableOnly, w.lockTime, this::canLock); + } + } + bayAngle = new float[def.weaponBays.size()]; + prevBayAngle = new float[def.weaponBays.size()]; + sentAmmo = new int[sets.size()]; + sentRest = new int[sets.size()]; + Arrays.fill(sentAmmo, -1); + flares = new FlareDispenser(vehicle, def); + } + + public List sets() { + return sets; + } + + public @Nullable WeaponSet set(int id) { + return id >= 0 && id < sets.size() ? sets.get(id) : null; + } + + public int indexOf(String weapon) { + for (int i = 0; i < sets.size(); i++) { + if (sets.get(i).name().equalsIgnoreCase(weapon)) { + return i; + } + } + return -1; + } + + public @Nullable WeaponSet byName(String weapon) { + return set(indexOf(weapon)); + } + + public @Nullable Guidance guidance(int id) { + return id >= 0 && id < guidance.length ? guidance[id] : null; + } + + /** A vehicle does not lock its own crew or cargo. */ + private boolean canLock(Entity entity) { + return entity != vehicle && !vehicle.hasPassenger(entity); + } + + // Selection + + public int selected(int seat) { + return seat >= 0 && seat < selected.length ? selected[seat] : -1; + } + + public @Nullable WeaponSet current(int seat) { + return set(selected(seat)); + } + + /** The seat that operates a weapon: its own seat, or the pilot while that seat has no player. */ + public int operatorSeat(int id) { + WeaponSet set = set(id); + if (set == null) { + return -1; + } + WeaponMount mount = set.firstMount(); + if (mount.usableByPilot() && !(vehicle.occupant(mount.seatIndex()) instanceof Player)) { + return 0; + } + return mount.seatIndex(); + } + + /** {@code getNextWeaponID}: the next weapon {@code seat} may use, stepping forwards or back. */ + public int next(int seat, int step) { + if (sets.isEmpty() || seat < 0) { + return -1; + } + int id = selected(seat); + for (int i = 0; i < sets.size(); i++) { + id = step >= 0 ? (id + 1) % sets.size() : id > 0 ? id - 1 : sets.size() - 1; + WeaponMount mount = sets.get(id).firstMount(); + int owner = mount.seatIndex(); + if (owner == seat || seat == 0 && mount.usableByPilot() && !(vehicle.occupant(owner) instanceof Player)) { + return id; + } + } + return -1; + } + + /** {@code initCurrentWeapon}: a player took a seat. Server only. */ + public void onSeated(Entity entity, int seat) { + if (seat < 0 || seat >= selected.length) { + return; + } + selected[seat] = -1; + if (entity instanceof Player) { + switchTo(entity, seat, next(seat, 1)); + } else { + vehicle.publishWeaponSelection(); + } + } + + public void onLeft(int seat) { + if (seat >= 0 && seat < selected.length) { + selected[seat] = -1; + vehicle.publishWeaponSelection(); + } + } + + /** {@code switchWeapon}. Server only; the selection reaches clients through synced data. */ + public void switchTo(Entity user, int seat, int id) { + if (seat < 0 || seat >= selected.length || sets.isEmpty()) { + return; + } + WeaponSet before = current(seat); + if (before != null) { + before.reload(); + } + selected[seat] = Math.min(id, sets.size() - 1); + WeaponSet now = current(seat); + if (now != null) { + now.onSwitchWeapon(vehicle.hasInfiniteAmmo(user)); + } + vehicle.publishWeaponSelection(); + } + + public String encodeSelection() { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < selected.length; i++) { + if (i > 0) { + b.append(','); + } + b.append(selected[i]); + } + return b.toString(); + } + + /** Applies the server's selection on a client, running the switch delay where it changed. */ + public void decodeSelection(String encoded, @Nullable Entity localPlayer) { + String[] parts = encoded.isEmpty() ? new String[0] : encoded.split(","); + for (int i = 0; i < selected.length; i++) { + int id; + try { + id = i < parts.length ? Integer.parseInt(parts[i]) : -1; + } catch (NumberFormatException e) { + id = -1; + } + if (selected[i] != id) { + selected[i] = id; + WeaponSet set = set(id); + Entity occupant = vehicle.occupant(i); + if (set != null && occupant != null && occupant == localPlayer) { + set.onSwitchWeapon(vehicle.hasInfiniteAmmo(occupant)); + Guidance g = guidance(id); + if (g != null) { + g.clear(); + } + } + } + } + } + + // Firing + + /** + * {@code useCurrentWeapon}: fires the selected weapon of {@code seat} if it is ready. The + * trigger decides what the shot does on this side. + */ + public boolean use(Entity user, int seat, Shot shot, WeaponSet.Trigger trigger) { + int id = selected(seat); + WeaponSet set = set(id); + if (set == null || !set.canUse() || vehicle.isDestroyed()) { + return false; + } + shot.infiniteAmmo = vehicle.hasInfiniteAmmo(user); + int mount = set.currentMount(); + // As the original did, face the vehicle where the user looks for the length of the shot. + boolean followUser = vehicle.aimsWithUser(set); + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + if (followUser) { + vehicle.setYRot(user.getYRot()); + vehicle.setXRot(user.getXRot()); + } + boolean fired; + try { + fired = set.use(shot, (s, m, sh) -> { + Vec3 muzzle = muzzleOffset(s, m, 1.0F); + sh.x += muzzle.x; + sh.y += muzzle.y; + sh.z += muzzle.z; + return trigger.fire(s, m, sh); + }); + } finally { + if (followUser) { + vehicle.setYRot(hullYaw); + vehicle.setXRot(hullPitch); + } + } + if (!fired) { + return false; + } + String group = set.definition().group; + if (!group.isEmpty()) { + for (WeaponSet other : sets) { + if (other != set && group.equals(other.definition().group)) { + other.waitForGroup(shot.reload); + } + } + } + if (!clientSide) { + int bit = bitOf(id, mount); + if (bit < 32) { + useStat |= 1 << bit; + } + } + return true; + } + + /** World offset of a mount from the vehicle origin; turret mounts turn with the turret. */ + public Vec3 muzzleOffset(WeaponSet set, int mount, float partialTicks) { + WeaponMount m = set.mounts().get(mount); + Vector3 local = m.turret() ? vehicle.onTurret(def, m.position(), partialTicks) : m.position(); + Vector3f v = vehicle.toWorldOffset(local); + return new Vec3(v.x, v.y, v.z); + } + + private int bitOf(int id, int mount) { + int shift = 0; + for (int i = 0; i < id; i++) { + shift += sets.get(i).mounts().size(); + } + return shift + mount; + } + + /** Whether a mount is cooling down after a shot; missile parts are hidden meanwhile. */ + public boolean isMountUsed(int id, int mount) { + int bit = bitOf(id, mount); + return bit < 32 && (useStat & 1 << bit) != 0; + } + + public int useStat() { + return useStat; + } + + public void setUseStat(int stat) { + useStat = stat; + } + + public void switchMode(int seat) { + WeaponSet set = current(seat); + if (set != null) { + set.switchMode(); + } + } + + // Ticking + + /** {@code updateWeapons} and {@code updateWeaponsRotation}, once per tick on both sides. */ + public void tick() { + if (sets.isEmpty()) { + flares.tick(); + return; + } + int previous = prevUseStat; + if (!clientSide) { + useStat |= usedStat(); + vehicle.publishWeaponUse(useStat); + } else { + useStat = vehicle.syncedWeaponUse(); + } + prevUseStat = useStat; + + float yaw = Mth.wrapDegrees(vehicle.getYRot()); + float pitch = Mth.wrapDegrees(vehicle.getXRot()); + int bit = 0; + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + boolean longDelay = set.isLongDelay(); + boolean anyUsed = false; + for (int mount = 0; mount < set.mounts().size(); mount++) { + boolean wasUsed = bit < 32 && (previous & 1 << bit) != 0; + boolean used = bit < 32 && (useStat & 1 << bit) != 0; + if (longDelay && wasUsed && used) { + used = false; + } + anyUsed |= used; + if (!wasUsed && used && set.definition().recoil > 0.0F) { + recoilCount = 30; + recoilValue = set.definition().recoil; + recoilYaw = set.rotationYaw; + } + if (clientSide && used) { + muzzleEffects(set, mount, yaw); + } + set.tickRecoil(mount, used); + bit++; + } + set.tick(anyUsed); + if (!clientSide) { + ServerFiring.tick(vehicle, set); + } + updateRotation(set); + } + if (!clientSide) { + useStat = 0; + syncAmmo(); + } else { + for (Guidance g : guidance) { + if (g != null) { + g.tick(); + } + } + } + if (recoilCount > 0) { + recoilCount--; + } + tickBays(); + if (clientSide) { + smoke(); + } + flares.tick(); + } + + private int usedStat() { + int stat = 0; + int bit = 0; + for (WeaponSet set : sets) { + for (int mount = 0; mount < set.mounts().size() && bit < 32; mount++) { + stat |= set.isUsed(mount) ? 1 << bit : 0; + bit++; + } + } + return stat; + } + + /** Turret aim of one set, following whoever operates it, within the mount's traverse limits. */ + private void updateRotation(WeaponSet set) { + if (vehicle.isDestroyed()) { + return; + } + WeaponMount mount = set.firstMount(); + int seat = mount.seatIndex(); + Entity operator = vehicle.occupant(seat); + if (mount.usableByPilot() && !(operator instanceof Player)) { + operator = vehicle.occupant(0); + } + float hullYaw = Mth.wrapDegrees(vehicle.getYRot()); + float hullPitch = Mth.wrapDegrees(vehicle.getXRot()); + float turretYaw = vehicle.turretYaw(); + if (operator instanceof Player) { + if ((int) mount.minYaw() != 0 || (int) mount.maxYaw() != 0) { + float ty = mount.turret() ? turretYaw : 0.0F; + float ey = Mth.wrapDegrees(operator.getYRot() - hullYaw - mount.defaultYaw() - ty); + if (Math.abs((int) mount.minYaw()) < 360 && Math.abs((int) mount.maxYaw()) < 360) { + float targetYaw = Mth.clamp(ey, mount.minYaw(), mount.maxYaw()); + float wy = set.rotationYaw - mount.defaultYaw() - ty; + if (targetYaw < wy) { + wy = wy - targetYaw > 15.0F ? wy - 15.0F : targetYaw; + } else if (targetYaw > wy) { + wy = targetYaw - wy > 15.0F ? wy + 15.0F : targetYaw; + } + set.rotationYaw = wy + mount.defaultYaw() + ty; + } else { + set.rotationYaw = ey + ty; + } + } + float ep = Mth.wrapDegrees(operator.getXRot() - hullPitch); + set.rotationPitch = Mth.clamp(ep, mount.minPitch(), mount.maxPitch()); + set.rotationTurretYaw = 0.0F; + } else { + set.rotationTurretYaw = turretYaw; + } + } + + /** {@code updateRecoil}: the hull kick of a big gun, applied on the simulating side. */ + public float @Nullable [] recoilKick() { + if (recoilCount < 12) { + return null; + } + float rel = (float) Math.toRadians(recoilYaw - vehicle.roll()); + float kick = Mth.cos((float) Math.toRadians(recoilCount * 6)) * recoilValue; + return new float[]{kick * Mth.cos(rel), kick * Mth.sin(rel)}; + } + + /** {@code updateWeaponBay}: bays swing open while a seated crew member has their weapon selected. */ + private void tickBays() { + for (int i = 0; i < bayAngle.length; i++) { + WeaponBayPart bay = def.weaponBays.get(i); + boolean open = false; + for (String weapon : bay.weapons()) { + int id = indexOf(weapon); + for (int seat = 0; seat < selected.length && id >= 0; seat++) { + if (selected[seat] == id && vehicle.occupant(seat) != null) { + open = true; + } + } + } + prevBayAngle[i] = bayAngle[i]; + bayAngle[i] = Mth.clamp(bayAngle[i] + (open ? 3.0F : -3.0F), 0.0F, 90.0F); + } + } + + public float bayAngle(int bay, float partialTicks) { + return bay < bayAngle.length ? Mth.lerp(partialTicks, prevBayAngle[bay], bayAngle[bay]) : 0.0F; + } + + // Effects, client only + + /** {@code spawnParticleMuzzleFlash} plus the spent case of guns with {@code SetCartridge}. */ + private void muzzleEffects(WeaponSet set, int mount, float hullYaw) { + Level level = vehicle.level(); + WeaponDefinition w = set.definition(); + Vec3 offset = muzzleOffset(set, mount, 1.0F); + double px = vehicle.getX() + offset.x; + double py = vehicle.getY() + offset.y; + double pz = vehicle.getZ() + offset.z; + Vec3 dir = Vec3.directionFromRotation(vehicle.getXRot() + set.rotationPitch, hullYaw + set.rotationYaw); + for (MuzzleFlash mf : w.muzzleSmokes) { + double x = px + dir.x * mf.distance(); + double y = py + dir.y * mf.distance(); + double z = pz + dir.z * mf.distance(); + for (int i = 0; i < mf.count(); i++) { + double r = mf.range(); + level.addParticle(mf.size() >= 2.0F ? ParticleTypes.LARGE_SMOKE : ParticleTypes.SMOKE, + x + (random.nextDouble() - 0.5) * r, y + (random.nextDouble() - 0.5) * r, z + (random.nextDouble() - 0.5) * r, + (random.nextDouble() - 0.5) * 0.05, 0.02, (random.nextDouble() - 0.5) * 0.05); + } + } + for (MuzzleFlash mf : w.muzzleFlashes) { + int rgb = mf.color().argb() & 0xFFFFFF; + level.addParticle(new DustParticleOptions(rgb, Mth.clamp(mf.size() * 4.0F, 0.5F, 4.0F)), + px + dir.x * mf.distance(), py + dir.y * mf.distance(), pz + dir.z * mf.distance(), 0.0, 0.0, 0.0); + } + if (w.cartridge != null) { + Vec3 motion = vehicle.getDeltaMovement(); + Vec3 side = Vec3.directionFromRotation(0.0F, hullYaw + set.rotationYaw + w.cartridge.yaw() + 90.0F) + .scale(0.1 + w.cartridge.acceleration()); + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, Items.GOLD_NUGGET), + px, py, pz, motion.x + side.x, motion.y + 0.1, motion.z + side.z); + } + } + + /** {@code onUpdate_ParticleSmoke}: smoke generators trail while the pilot has them selected. */ + private void smoke() { + WeaponSet set = current(0); + if (set == null || set.definition().type != WeaponType.SMOKE || vehicle.visibleThrottle() <= 0.1F) { + return; + } + WeaponDefinition w = set.definition(); + Vec3 back = Vec3.directionFromRotation(vehicle.getXRot(), vehicle.getYRot()).reverse(); + int rgb = w.color.argb() & 0xFFFFFF; + for (int mount = 0; mount < set.mounts().size(); mount++) { + if (random.nextFloat() > vehicle.visibleThrottle() * 1.5F) { + continue; + } + Vec3 offset = muzzleOffset(set, mount, 1.0F); + for (int i = 0; i < w.smokeNum; i++) { + vehicle.level().addParticle(new DustParticleOptions(rgb, Mth.clamp(w.smokeSize * 2.0F, 0.5F, 4.0F)), + vehicle.getX() + offset.x + back.x, vehicle.getY() + offset.y + back.y, vehicle.getZ() + offset.z + back.z, + back.x * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.y * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.z * w.acceleration + (random.nextDouble() - 0.5) * 0.2); + } + } + } + + // Ammunition + + /** Sends magazine and reserve counts to the crew whenever they change. Server only. */ + private void syncAmmo() { + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + if (set.ammo() != sentAmmo[id] || set.restAmmo() != sentRest[id]) { + sentAmmo[id] = set.ammo(); + sentRest[id] = set.restAmmo(); + WeaponStatePacket packet = new WeaponStatePacket(vehicle.getId(), id, set.ammo(), set.restAmmo()); + for (Entity passenger : vehicle.getPassengers()) { + if (passenger instanceof ServerPlayer player) { + ModNetwork.sendToPlayer(player, packet); + } + } + } + } + } + + /** Forces a full resend, for a player who just boarded. */ + public void resendAmmo() { + Arrays.fill(sentAmmo, -1); + } + + public void applyAmmo(int id, int ammo, int rest) { + WeaponSet set = set(id); + if (set != null) { + set.setAmmo(ammo); + set.setRestAmmo(rest); + } + } + + /** {@code reloadAllWeapon}: tops up every magazine from its reserve. */ + public void reloadAll() { + for (WeaponSet set : sets) { + set.reloadMagazine(); + } + } + + public int[] saveAmmo() { + int[] out = new int[sets.size() * 2]; + for (int i = 0; i < sets.size(); i++) { + out[i * 2] = sets.get(i).ammo(); + out[i * 2 + 1] = sets.get(i).restAmmo(); + } + return out; + } + + public void loadAmmo(int[] saved) { + for (int i = 0; i < sets.size() && i * 2 + 1 < saved.length; i++) { + sets.get(i).setAmmo(Math.min(saved[i * 2], sets.get(i).magazineSize())); + sets.get(i).setRestAmmo(saved[i * 2 + 1]); + } + } +} From 9d90c6735cbdc016a20807a9c55a14a290be70df Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:01 +0800 Subject: [PATCH 0334/1461] Add weapon/WeaponSet.java to the 26.2 shared sources --- .../jnhprojects/mcheli/weapon/WeaponSet.java | 505 ++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java new file mode 100644 index 00000000..3f539030 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java @@ -0,0 +1,505 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * One selectable weapon of a vehicle: every mount that fires the same weapon, with the magazine, + * reload, heat and recoil they share. A port of {@code MCH_WeaponSet} together with the parts of + * {@code MCH_WeaponBase} and {@code MCH_WeaponCreator} that are not specific to a weapon type. + * + *

Both sides keep one. The server's is authoritative for ammunition and decides what is fired; + * the client's runs with the slightly longer intervals the original used there, so the client never + * asks to fire faster than the server accepts. + */ +public final class WeaponSet { + private static final int SWITCH_WAIT = 15; + + /** Fires one mount. Returns whether the shot happened, which consumes a round. */ + @FunctionalInterface + public interface Trigger { + boolean fire(WeaponSet set, int mount, Shot shot); + } + + private final RandomSource random = RandomSource.create(); + private final WeaponDefinition def; + private final List mounts; + private final boolean clientSide; + + public final int interval; + public final int heatCount; + private final int numMode; + + private int currentMount; + private int mode; + private int ammo; + private int restAmmo; + public int currentHeat; + private int cooldownSpeed = 1; + public int countWait; + public int countReloadWait; + private final int[] lastUsedCount; + /** Ticks since each mount last fired, {@code MCH_WeaponBase.tick}. */ + final int[] ticks; + public int soundWait; + /** Whether the shot being fired may play its sound, see {@code SoundDelay}. */ + public boolean soundAllowed = true; + /** The vehicle's own sound for this weapon, from {@code WeaponSound}. */ + public @Nullable WeaponSound ownSound; + + /** Aim relative to the hull, following the crew member who operates the weapon. */ + public float rotationYaw; + public float rotationPitch; + public float prevRotationYaw; + public float prevRotationPitch; + public final float defaultRotationYaw; + /** How far behind the pilot's view a turret weapon lags, for drawing. */ + public float rotationTurretYaw; + + public float barrelSpeed; + public float barrel; + public float prevBarrel; + private final Recoil[] recoil; + + // Close air support run, server only. See ServerFiring. + @Nullable Entity casUser; + @Nullable Entity casShooter; + double casX; + double casY; + double casZ; + int casDirection; + int casStartTick; + int casCount = 3; + + public WeaponSet(WeaponDefinition def, List mounts, boolean clientSide) { + this.def = def; + this.mounts = List.copyOf(mounts); + this.clientSide = clientSide; + this.lastUsedCount = new int[mounts.size()]; + this.ticks = new int[mounts.size()]; + this.recoil = new Recoil[mounts.size()]; + for (int i = 0; i < recoil.length; i++) { + recoil[i] = new Recoil(def.recoilBufCount, def.recoilBufCountSpeed); + } + this.defaultRotationYaw = mounts.isEmpty() ? 0.0F : mounts.getFirst().defaultYaw(); + this.rotationYaw = defaultRotationYaw; + this.prevRotationYaw = defaultRotationYaw; + + int wait = def.delay; + if (def.type == WeaponType.CAS && wait > -250) { + wait = -250; + } + int heat = def.heatCount; + if (def.maxHeatCount > 0 && heat < 2) { + heat = 2; + } + if (clientSide) { + wait = clientInterval(wait); + heat++; + } + this.interval = wait; + this.heatCount = heat; + this.numMode = def.type == WeaponType.MACHINE_GUN_2 && def.explosion == 0 ? 0 : def.modeNum; + } + + /** The original gave the client longer intervals so its requests never outran the server. */ + private static int clientInterval(int wait) { + if (wait < 4) { + return wait + 1; + } else if (wait < 7) { + return wait + 2; + } else if (wait < 10) { + return wait + 3; + } else if (wait < 20) { + return wait + 6; + } + wait += 10; + return wait >= 40 ? -wait : wait; + } + + public WeaponDefinition definition() { + return def; + } + + public String name() { + return def.name(); + } + + public List mounts() { + return mounts; + } + + public WeaponMount firstMount() { + return mounts.getFirst(); + } + + public int currentMount() { + return currentMount; + } + + // Ammunition + + public int ammo() { + return ammo; + } + + public int magazineSize() { + return def.round; + } + + public int restAmmo() { + return restAmmo; + } + + public int maxAmmo() { + return def.maxAmmo; + } + + public void setAmmo(int n) { + ammo = n; + } + + /** Everything not in the magazine, capped so magazine plus reserve never exceed {@code MaxAmmo}. */ + public void setRestAmmo(int n) { + restAmmo = Math.min(n, def.maxAmmo - ammo); + } + + /** {@code supplyRestAllAmmo}: one resupply adds {@code SuppliedNum} rounds. */ + public boolean supply() { + if (restAmmo + ammo < def.maxAmmo) { + setRestAmmo(restAmmo + ammo + def.suppliedNum); + return true; + } + return false; + } + + public boolean isInPreparation() { + return countWait < 0 || countReloadWait > 0; + } + + public boolean isReloading() { + return countReloadWait > 0; + } + + public boolean canUse() { + return countWait == 0; + } + + public boolean isLongDelay() { + return def.delay > 4; + } + + public boolean isOverheated() { + return def.maxHeatCount > 0 && currentHeat >= def.maxHeatCount; + } + + public void reload() { + if (magazineSize() > 0 && ammo < magazineSize() && def.reloadTime > 0) { + countReloadWait = def.reloadTime; + if (clientSide) { + ammo = 0; + } else { + countReloadWait = Math.max(countReloadWait - 20, 1); + } + } + } + + public void reloadMagazine() { + int needed = magazineSize() - ammo; + if (needed > 0) { + needed = Math.min(needed, restAmmo); + ammo += needed; + setRestAmmo(restAmmo - needed); + } + } + + // Modes + + public int mode() { + return def.fixMode > 0 ? def.fixMode : mode; + } + + public boolean switchMode() { + if (def.fixMode > 0) { + return false; + } + int before = mode(); + mode = numMode > 0 ? (mode() + 1) % numMode : 0; + if (before == mode()) { + return false; + } + waitAfterSwitch(SWITCH_WAIT); + return true; + } + + /** {@code getName}: the display name with the mode suffix the weapon class added. */ + public String displayName() { + String suffix = switch (def.type) { + case MACHINE_GUN_2 -> mode() == 0 ? "" : " [HE]"; + case ROCKET -> mode() == 0 ? "" : " [HEIAP]"; + case AT_MISSILE -> mode() == 1 ? " [TA]" : ""; + case TV_MISSILE -> switch (mode()) { + case 0 -> " [TV]"; + case 2 -> " [TA]"; + default -> ""; + }; + case null, default -> ""; + }; + return def.displayName + suffix; + } + + public void onSwitchWeapon(boolean infiniteAmmo) { + waitAfterSwitch(clientSide ? SWITCH_WAIT + 10 : SWITCH_WAIT); + currentMount = 0; + if (infiniteAmmo) { + ammo = magazineSize(); + } + } + + private void waitAfterSwitch(int wait) { + if (countWait >= -wait) { + countWait = countWait > wait ? -countWait : -wait; + } + } + + /** Another weapon of the same {@code Group} fired; this one waits and reloads with it. */ + public void waitForGroup(boolean reload) { + countWait = interval; + countReloadWait = 0; + if (reload && magazineSize() > 0 && def.reloadTime > 0) { + countReloadWait = clientSide ? def.reloadTime : Math.max(def.reloadTime - 20, 1); + } + } + + /** + * {@code getLandInDistance}: how far out, measured flat, a shell fired now would come down, + * stepping its arc a tick at a time as the projectile does; -1 if it would not come down in + * range. Mortars show it on the HUD. + */ + public double landingDistance(Entity shooter) { + if (def.gravity >= 0.0F || mounts.isEmpty()) { + return -1.0; + } + WeaponMount mount = mounts.get(currentMount); + float yaw = Mth.wrapDegrees(shooter.getYRot() + rotationYaw + mount.yaw()); + float pitch = Mth.wrapDegrees(shooter.getXRot() + rotationPitch + mount.pitch()); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + double acc = Math.min(def.acceleration, 4.0F); + double my = dir.y * def.acceleration; + if (my <= 0.0 || acc <= 0.0) { + return -1.0; + } + double mx = dir.x * def.acceleration; + double mz = dir.z * def.acceleration; + double gravity = def.gravity * (def.acceleration / acc); + double steps = my / def.gravity; + if (steps < -12.0) { + double f = steps / -12.0; + mx *= f; + my *= f; + mz *= f; + gravity *= f * f * 0.95; + } + Level level = shooter.level(); + double x = shooter.getX(); + double y = shooter.getY() + 3.0; + double z = shooter.getZ(); + for (int i = 0; i < 50; i++) { + Vec3 from = new Vec3(x, y, z); + Vec3 to = new Vec3(x + mx, y + my, z + mz); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shooter)); + if (hit.getType() == HitResult.Type.BLOCK) { + return Math.hypot(hit.getBlockPos().getX() - shooter.getX(), hit.getBlockPos().getZ() - shooter.getZ()); + } + my += gravity; + x += mx; + y += my; + z += mz; + if (y < shooter.getY()) { + return Math.hypot(x - shooter.getX(), z - shooter.getZ()); + } + } + return -1.0; + } + + // Firing + + /** + * Fires the next mount if the set is ready. The aim is the shooter's rotation plus this set's + * turret aim and the mount's fixed offset, scattered by {@code Accuracy}. + */ + public boolean use(Shot shot, Trigger trigger) { + if (magazineSize() > 0 && ammo <= 0 || isOverheated()) { + return false; + } + WeaponMount mount = mounts.get(currentMount); + soundAllowed = soundWait == 0; + float yaw = shot.shooter.getYRot() + rotationYaw + mount.yaw(); + float pitch = shot.shooter.getXRot() + rotationPitch + mount.pitch(); + if (def.accuracy > 0.0F) { + yaw += (random.nextFloat() - 0.5F) * def.accuracy; + pitch += (random.nextFloat() - 0.5F) * def.accuracy; + } + shot.yaw = Mth.wrapDegrees(yaw); + shot.pitch = Mth.wrapDegrees(pitch); + if (!trigger.fire(this, currentMount, shot)) { + return false; + } + ticks[currentMount] = 0; + + if (def.maxHeatCount > 0) { + cooldownSpeed = 1; + currentHeat += heatCount; + if (currentHeat >= def.maxHeatCount) { + currentHeat += 30; + } + } + if (def.soundDelay > 0 && soundWait == 0) { + soundWait = def.soundDelay; + } + lastUsedCount[currentMount] = Math.abs(interval); + if (isCooldownReloadTime() && def.reloadTime - 10 > lastUsedCount[currentMount]) { + lastUsedCount[currentMount] = def.reloadTime - 10; + } + currentMount = (currentMount + 1) % mounts.size(); + countWait = interval; + countReloadWait = 0; + if (ammo > 0) { + ammo--; + } + if (ammo <= 0) { + if (shot.infiniteAmmo && restAmmo < magazineSize()) { + setRestAmmo(magazineSize()); + } + reload(); + shot.reload = true; + } + return true; + } + + /** Missiles keep their mount empty for the whole reload, which hides the missile part meanwhile. */ + private boolean isCooldownReloadTime() { + return def.type == WeaponType.AA_MISSILE || def.type == WeaponType.AS_MISSILE || def.type == WeaponType.AT_MISSILE; + } + + /** Whether {@code mount} fired recently enough to show muzzle effects or keep a missile hidden. */ + public boolean isUsed(int mount) { + if (mount >= lastUsedCount.length) { + return false; + } + int count = lastUsedCount[mount]; + return interval >= 4 && count > interval / 2 || count >= 4; + } + + public int ticksSinceShot(int mount) { + return ticks[mount]; + } + + // Ticking + + /** {@code MCH_WeaponSet.update}, once per tick on both sides. */ + public void tick(boolean used) { + if (countReloadWait > 0) { + countReloadWait--; + if (countReloadWait == 0) { + reloadMagazine(); + } + } + for (int i = 0; i < lastUsedCount.length; i++) { + if (lastUsedCount[i] > 0) { + if (lastUsedCount[i] != 4 || currentMount == 0 && canUse() && (ammo > 0 || def.maxAmmo <= 0)) { + lastUsedCount[i]--; + } + } + } + if (currentHeat > 0) { + if (currentHeat < def.maxHeatCount) { + cooldownSpeed++; + } + currentHeat = Math.max(currentHeat - (cooldownSpeed / 20 + 1), 0); + } + if (countWait > 0) { + countWait--; + } else if (countWait < 0) { + countWait++; + } + prevRotationYaw = rotationYaw; + prevRotationPitch = rotationPitch; + if (countWait != 0) { + for (int i = 0; i < ticks.length; i++) { + ticks[i]++; + } + } + if (soundWait > 0) { + soundWait--; + } + + if (used && barrelSpeed < 75.0F) { + barrelSpeed = Math.min(barrelSpeed + 25 + random.nextInt(3), 74.0F); + } + prevBarrel = barrel; + barrel += barrelSpeed; + if (barrel >= 360.0F) { + barrel -= 360.0F; + prevBarrel -= 360.0F; + } + if (barrelSpeed > 0.0F) { + barrelSpeed = Math.max(barrelSpeed - 1.48F, 0.0F); + } + } + + /** {@code updateWeapon}: the recoil kick of one mount. */ + public void tickRecoil(int mount, boolean used) { + Recoil r = recoil[mount]; + r.prev = r.value; + if (used && r.count <= 0) { + r.count = r.max; + } + if (r.count > 0) { + if (r.max <= 1) { + r.value = 1.0F; + } else if (r.max == 2) { + r.value = r.count == 2 ? 1.0F : 0.6F; + } else { + if (r.count > r.max / 2) { + r.count -= r.speed; + } + r.value = Mth.sin((float) r.count / r.max * Mth.PI); + } + r.count--; + } else { + r.value = 0.0F; + } + } + + public float recoil(int mount, float partialTicks) { + Recoil r = recoil[Math.min(mount, recoil.length - 1)]; + return Mth.lerp(partialTicks, r.prev, r.value); + } + + private static final class Recoil { + final int max; + final int speed; + int count; + float value; + float prev; + + Recoil(int max, int speed) { + this.max = max; + this.speed = speed; + } + } +} From fb2fabc0958c893eea78d8f141928cab6d85a897 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:01 +0800 Subject: [PATCH 0335/1461] Add workshop/TestFlight.java to the 26.2 shared sources --- .../mcheli/workshop/TestFlight.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 minecraft-26.2/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java new file mode 100644 index 00000000..f5786b92 --- /dev/null +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java @@ -0,0 +1,62 @@ +package com.jnhprojects.mcheli.workshop; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.phys.Vec3; + +/** + * Flying a vehicle straight from the workshop. The pack was just saved on the machine this server + * runs on, so loading it again is enough. That only holds for the owner of a single player or LAN + * game: a dedicated server does not get a pack until an operator approves it, and then it is no + * longer a test, so the screen does not offer it there and the server refuses it too. + */ +public final class TestFlight { + /** How far up to look for room when the vehicle does not fit where the player stands. */ + private static final int MAX_RISE = 48; + + private TestFlight() { + } + + public static void start(ServerPlayer player, String vehicle) { + MinecraftServer server = player.level().getServer(); + if (!server.isSingleplayerOwner(player.nameAndId())) { + player.sendOverlayMessage(Component.translatable("message.mcheli.workshop.test_flight_owner_only")); + return; + } + if (player.isPassenger()) { + player.sendOverlayMessage(Component.translatable("message.mcheli.workshop.test_flight_riding")); + return; + } + ContentReload.reload(server); + AircraftDefinition def = MCHeli.content().any(vehicle); + if (def == null) { + player.sendSystemMessage(Component.translatable("message.mcheli.workshop.test_flight_no_vehicle", vehicle)); + MCHeli.packs().reportProblems(player::sendSystemMessage); + return; + } + ServerLevel level = player.level(); + AircraftEntity aircraft = ModEntities.typeFor(def).create(level, EntitySpawnReason.COMMAND); + if (aircraft == null) { + return; + } + aircraft.setType(def); + Vec3 start = player.position(); + for (int rise = 0; rise <= MAX_RISE; rise++) { + aircraft.snapTo(start.x, start.y + rise, start.z, player.getYRot(), 0.0F); + if (level.noCollision(aircraft, aircraft.getBoundingBox())) { + break; + } + } + aircraft.startTestFlight(start); + level.addFreshEntity(aircraft); + player.startRiding(aircraft, true, true); + } +} From d34320b6855c19261aa02b49ca2c85b139609ce6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:02 +0800 Subject: [PATCH 0336/1461] Add assets/mcheli/blockstates/drafting_table.json to the 26.2 shared sources --- .../resources/assets/mcheli/blockstates/drafting_table.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/blockstates/drafting_table.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/blockstates/drafting_table.json b/minecraft-26.2/src/main/resources/assets/mcheli/blockstates/drafting_table.json new file mode 100644 index 00000000..82992af9 --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/blockstates/drafting_table.json @@ -0,0 +1,5 @@ +{ + "multipart": [ + { "apply": { "model": "mcheli:block/drafting_table" } } + ] +} From b7b8ede839a78797b1b72c1a55abf693682b1bbd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:02 +0800 Subject: [PATCH 0337/1461] Add assets/mcheli/items/drafting_table.json to the 26.2 shared sources --- .../main/resources/assets/mcheli/items/drafting_table.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/items/drafting_table.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/items/drafting_table.json b/minecraft-26.2/src/main/resources/assets/mcheli/items/drafting_table.json new file mode 100644 index 00000000..a3afc683 --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/items/drafting_table.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "mcheli:block/drafting_table" + } +} From ee0fdad4426f35cc8767a5f74e901c871994556b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:02 +0800 Subject: [PATCH 0338/1461] Add assets/mcheli/items/helicopter.json to the 26.2 shared sources --- .../src/main/resources/assets/mcheli/items/helicopter.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/items/helicopter.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/items/helicopter.json b/minecraft-26.2/src/main/resources/assets/mcheli/items/helicopter.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/items/helicopter.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From ca1ff20b9bc63c123bfa71b6e96039ac0a161cdc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:03 +0800 Subject: [PATCH 0339/1461] Add assets/mcheli/items/plane.json to the 26.2 shared sources --- .../src/main/resources/assets/mcheli/items/plane.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/items/plane.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/items/plane.json b/minecraft-26.2/src/main/resources/assets/mcheli/items/plane.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/items/plane.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From d2676eaa73232727589e7a1d6615ab9d1598ae8e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:03 +0800 Subject: [PATCH 0340/1461] Add assets/mcheli/items/tank.json to the 26.2 shared sources --- .../src/main/resources/assets/mcheli/items/tank.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/items/tank.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/items/tank.json b/minecraft-26.2/src/main/resources/assets/mcheli/items/tank.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/items/tank.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From 42ed84230e5a0839dda19eafc27d7dbb2c50bf1f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:04 +0800 Subject: [PATCH 0341/1461] Add assets/mcheli/items/vehicle.json to the 26.2 shared sources --- .../src/main/resources/assets/mcheli/items/vehicle.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/items/vehicle.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/items/vehicle.json b/minecraft-26.2/src/main/resources/assets/mcheli/items/vehicle.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/items/vehicle.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From 5b6c916e9a2f1b51b47dd712c0828329e733d898 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:04 +0800 Subject: [PATCH 0342/1461] Add assets/mcheli/models/block/drafting_table.json to the 26.2 shared sources --- .../mcheli/models/block/drafting_table.json | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/models/block/drafting_table.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/models/block/drafting_table.json b/minecraft-26.2/src/main/resources/assets/mcheli/models/block/drafting_table.json new file mode 100644 index 00000000..8dfa5bcc --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/models/block/drafting_table.json @@ -0,0 +1,27 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/oak_planks", + "legs": "minecraft:block/iron_block", + "board": "minecraft:block/oak_planks", + "paper": "minecraft:block/white_concrete" + }, + "elements": [ + { "from": [1, 0, 2], "to": [2.5, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 2], "to": [15, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [1, 0, 12.5], "to": [2.5, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 12.5], "to": [15, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [0, 10, 1], "to": [16, 11.5, 15], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { + "north": {"texture": "#board"}, "south": {"texture": "#board"}, "east": {"texture": "#board"}, "west": {"texture": "#board"}, + "up": {"texture": "#board"}, "down": {"texture": "#board"} } }, + { "from": [2, 11.5, 2.5], "to": [14, 11.6, 13.5], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { "up": {"texture": "#paper"} } } + ] +} From 2098796776aa83978b8fffa4cc4680fea8b1bf41 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:05 +0800 Subject: [PATCH 0343/1461] Add assets/mcheli/particles/glow.json to the 26.2 shared sources --- .../src/main/resources/assets/mcheli/particles/glow.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/particles/glow.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/particles/glow.json b/minecraft-26.2/src/main/resources/assets/mcheli/particles/glow.json new file mode 100644 index 00000000..296e5397 --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/particles/glow.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "mcheli:glow" + ] +} From f8a526155005f606c413543edf3a4b7893023b6b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:06 +0800 Subject: [PATCH 0344/1461] Add assets/mcheli/particles/smoke.json to the 26.2 shared sources --- .../resources/assets/mcheli/particles/smoke.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/particles/smoke.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/particles/smoke.json b/minecraft-26.2/src/main/resources/assets/mcheli/particles/smoke.json new file mode 100644 index 00000000..977381be --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/particles/smoke.json @@ -0,0 +1,16 @@ +{ + "textures": [ + "minecraft:big_smoke_0", + "minecraft:big_smoke_1", + "minecraft:big_smoke_2", + "minecraft:big_smoke_3", + "minecraft:big_smoke_4", + "minecraft:big_smoke_5", + "minecraft:big_smoke_6", + "minecraft:big_smoke_7", + "minecraft:big_smoke_8", + "minecraft:big_smoke_9", + "minecraft:big_smoke_10", + "minecraft:big_smoke_11" + ] +} From 1a4531dc74d92677b79aa91489b53e320e44d97e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:06 +0800 Subject: [PATCH 0345/1461] Add assets/mcheli/post_effect/thermal.json to the 26.2 shared sources --- .../assets/mcheli/post_effect/thermal.json | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/post_effect/thermal.json diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/post_effect/thermal.json b/minecraft-26.2/src/main/resources/assets/mcheli/post_effect/thermal.json new file mode 100644 index 00000000..593e905d --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/post_effect/thermal.json @@ -0,0 +1,38 @@ +{ + "targets": { + "swap": {} + }, + "passes": [ + { + "vertex_shader": "minecraft:core/screenquad", + "fragment_shader": "mcheli:post/thermal", + "inputs": [ + { + "sampler_name": "In", + "target": "minecraft:main" + } + ], + "output": "swap" + }, + { + "vertex_shader": "minecraft:core/screenquad", + "fragment_shader": "minecraft:post/blit", + "inputs": [ + { + "sampler_name": "In", + "target": "swap" + } + ], + "uniforms": { + "BlitConfig": [ + { + "name": "ColorModulate", + "type": "vec4", + "value": [ 1.0, 1.0, 1.0, 1.0 ] + } + ] + }, + "output": "minecraft:main" + } + ] +} From 89ef6d83f9adb91d50768625dfac2c1f5e002e54 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:07 +0800 Subject: [PATCH 0346/1461] Write the thermal shader in the GLSL dialect of 26.2 --- .../assets/mcheli/shaders/post/thermal.fsh | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/shaders/post/thermal.fsh diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/shaders/post/thermal.fsh b/minecraft-26.2/src/main/resources/assets/mcheli/shaders/post/thermal.fsh new file mode 100644 index 00000000..24f82a83 --- /dev/null +++ b/minecraft-26.2/src/main/resources/assets/mcheli/shaders/post/thermal.fsh @@ -0,0 +1,37 @@ +#version 330 + +// Thermal view, white hot. A post shader only sees colours, so heat is faked: bright and warm +// colours (fire, lava, skin, lamps) read hot, the sky and water cold, and outlines are drawn +// where the brightness changes sharply, like the original's "pencil" shader. + +uniform sampler2D InSampler; + +in vec2 texCoord; + +layout(std140) uniform SamplerInfo { + vec2 OutSize; + vec2 InSize; +}; + +out vec4 fragColor; + +const vec3 Gray = vec3(0.3, 0.59, 0.11); + +float heat(vec3 c) { + float luma = dot(c, Gray); + float warmth = clamp(c.r - c.b, 0.0, 1.0); + return clamp(luma * 0.7 + warmth * 0.6, 0.0, 1.0); +} + +void main() { + vec2 oneTexel = 1.0 / InSize; + float c = heat(texture(InSampler, texCoord).rgb); + float l = heat(texture(InSampler, texCoord - vec2(oneTexel.x, 0.0)).rgb); + float r = heat(texture(InSampler, texCoord + vec2(oneTexel.x, 0.0)).rgb); + float u = heat(texture(InSampler, texCoord - vec2(0.0, oneTexel.y)).rgb); + float d = heat(texture(InSampler, texCoord + vec2(0.0, oneTexel.y)).rgb); + float edge = clamp(abs(r - l) + abs(d - u), 0.0, 1.0); + + float value = smoothstep(0.08, 0.85, c) + edge * 0.6; + fragColor = vec4(vec3(clamp(value, 0.0, 1.0)), 1.0); +} From 25efcf35599845942ba8c87484056990d60104a0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:07 +0800 Subject: [PATCH 0347/1461] Add assets/mcheli/textures/particle/glow.png to the 26.2 shared sources --- .../assets/mcheli/textures/particle/glow.png | Bin 0 -> 705 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-26.2/src/main/resources/assets/mcheli/textures/particle/glow.png diff --git a/minecraft-26.2/src/main/resources/assets/mcheli/textures/particle/glow.png b/minecraft-26.2/src/main/resources/assets/mcheli/textures/particle/glow.png new file mode 100644 index 0000000000000000000000000000000000000000..339b97292b3a86cec21a5a2c2d4e9159da1241c6 GIT binary patch literal 705 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFm-skIEGZjoxNt=CF&@0?BmDQ z$G05=nLjo)u{~0d=sc02G=rmQ;>?7sgruZLOlR&iZR9TC);p3q<+=OyiMy`WR_B%6 ze!zcpW%0hxt6uJ`UOHv3s-)z$2S*IQ{M+2aBkl2V^1+p-UiNMId~?@~iHS`VvGKGtbJ(?@PMX`Q_JU)-AEGyIw3c zQrRtbha-<|yVkj!cRxOeDwmZ>DLa>nOo)xwXnye(C@s6DY4^dsUB4m0R;efKDR26HaPb4r4<;3I zGNE&B-ud`Y+f9cz$|Q5W??V51i?`b=Utv|8`!Zyb_`%f=Ny@jd)@AxOp)i%w?!a<|&&Tjr8oN`JQToU~(`->fg1 zZdP9Nqf5}cIn2IwE7PsJ)h7Sugyi*n%i6MQ@`K~}e`S@^nPlVK#j7xFV z+_j8uPnwhRR~zlgnR{cs;OihI&w3VnZD^oOJGP2v)|RI;bPnXz1l3;M6Sw&D^QW!V zDOSc=cfEdS|A;M20|xneiH#;17nkTB-(PMJ>hSJ9`!Q!Lt-~I+X1}kkT&Mak7@_E? z^^gDD5jm{g_J>^m^K5+4zM_`ByS7X6x8|EK^8>{;-q`=xrFCc5-2VzEHJ@mS{q(#h zwlHh)?U((R_Hk6-{(1h>N~aR1kZ_?NOSWy;`Ple&>FE<27@gj_D0q6`GXA*nq@huc lV^{v7@AFerZkX0Ht_;k6d;0MSU0~8=@O1TaS?83{1OQn@Sb6{e literal 0 HcmV?d00001 From 0940d4ded40f3abd80e2cc3712831312c2addc51 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:08 +0800 Subject: [PATCH 0348/1461] Add data/mcheli/damage_type/bullet.json to the 26.2 shared sources --- .../src/main/resources/data/mcheli/damage_type/bullet.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/damage_type/bullet.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/damage_type/bullet.json b/minecraft-26.2/src/main/resources/data/mcheli/damage_type/bullet.json new file mode 100644 index 00000000..92ee6766 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/damage_type/bullet.json @@ -0,0 +1,5 @@ +{ + "exhaustion": 0.1, + "message_id": "mcheli.bullet", + "scaling": "when_caused_by_living_non_player" +} From ef77d894affb3a691af7aedd275d64be77647245 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:08 +0800 Subject: [PATCH 0349/1461] Add data/mcheli/loot_table/blocks/drafting_table.json to the 26.2 shared sources --- .../mcheli/loot_table/blocks/drafting_table.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json b/minecraft-26.2/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json new file mode 100644 index 00000000..610afa08 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { "type": "minecraft:item", "name": "mcheli:drafting_table" } + ], + "conditions": [ + { "condition": "minecraft:survives_explosion" } + ] + } + ] +} From e83159692ed28f6feba5c3653c311ece6cda64d9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:09 +0800 Subject: [PATCH 0350/1461] Add data/mcheli/recipe/chain.json to the 26.2 shared sources --- .../main/resources/data/mcheli/recipe/chain.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/chain.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/chain.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/chain.json new file mode 100644 index 00000000..484f5463 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/chain.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": "#c:ingots/iron" + }, + "pattern": [ + "I I", + "III", + "I I" + ], + "result": { + "id": "mcheli:chain" + } +} From 29d8fe0478fdb6b8d78054461a289abc3b391e61 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:09 +0800 Subject: [PATCH 0351/1461] Add data/mcheli/recipe/container.json to the 26.2 shared sources --- .../resources/data/mcheli/recipe/container.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/container.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/container.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/container.json new file mode 100644 index 00000000..4c3bdcd9 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/container.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "#c:chests/wooden", + "I": "#c:ingots/iron" + }, + "pattern": [ + "CCI" + ], + "result": { + "id": "mcheli:container" + } +} From 7bc82da849bbe24e03c109d4d5b126da850c162d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:09 +0800 Subject: [PATCH 0352/1461] Add data/mcheli/recipe/drafting_table.json to the 26.2 shared sources --- .../data/mcheli/recipe/drafting_table.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/drafting_table.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/drafting_table.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/drafting_table.json new file mode 100644 index 00000000..f3d6324e --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/drafting_table.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "minecraft:crafting_table", + "F": "#minecraft:wooden_fences", + "P": "#minecraft:planks", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "R ", + "PCP", + "F F" + ], + "result": { + "id": "mcheli:drafting_table" + } +} From e8a14a7bd710b935a46b784018d0c75bbca6ffb5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:10 +0800 Subject: [PATCH 0353/1461] Add data/mcheli/recipe/fgm148.json to the 26.2 shared sources --- .../resources/data/mcheli/recipe/fgm148.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/fgm148.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/fgm148.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fgm148.json new file mode 100644 index 00000000..c2c61fd6 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fgm148.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "III", + "GR " + ], + "result": { + "id": "mcheli:fgm148" + } +} From 1a5d36d8d40462a5560744469c497f9ee51865c7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:10 +0800 Subject: [PATCH 0354/1461] Add data/mcheli/recipe/fgm148_bullet.json to the 26.2 shared sources --- .../data/mcheli/recipe/fgm148_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/fgm148_bullet.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/fgm148_bullet.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fgm148_bullet.json new file mode 100644 index 00000000..c87b4800 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fgm148_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:gunpowders", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + " R ", + " I ", + " G " + ], + "result": { + "count": 2, + "id": "mcheli:fgm148_bullet" + } +} From e854de7b7cf594f8e3081350cdd0ad6dfae78e7e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:11 +0800 Subject: [PATCH 0355/1461] Add data/mcheli/recipe/fim92.json to the 26.2 shared sources --- .../resources/data/mcheli/recipe/fim92.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/fim92.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/fim92.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fim92.json new file mode 100644 index 00000000..33a068e1 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fim92.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "G ", + "III", + "RI " + ], + "result": { + "id": "mcheli:fim92" + } +} From d8b8532c3a11efeeb341cff2bb653d52dbfe3ab2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:11 +0800 Subject: [PATCH 0356/1461] Add data/mcheli/recipe/fim92_bullet.json to the 26.2 shared sources --- .../data/mcheli/recipe/fim92_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/fim92_bullet.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/fim92_bullet.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fim92_bullet.json new file mode 100644 index 00000000..c472fd6e --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fim92_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:gunpowders", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "R ", + " I ", + " G" + ], + "result": { + "count": 2, + "id": "mcheli:fim92_bullet" + } +} From 55373a8c79726ab854d904a48324d6c3f5678d78 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:12 +0800 Subject: [PATCH 0357/1461] Add data/mcheli/recipe/fuel.json to the 26.2 shared sources --- .../main/resources/data/mcheli/recipe/fuel.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/fuel.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/fuel.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fuel.json new file mode 100644 index 00000000..d076b041 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/fuel.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": "minecraft:coal", + "I": "#c:ingots/iron" + }, + "pattern": [ + "ICI", + "III" + ], + "result": { + "id": "mcheli:fuel" + } +} From b73e6171bce8b72761d034e9702dc21a92c38d58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:12 +0800 Subject: [PATCH 0358/1461] Add data/mcheli/recipe/gltd.json to the 26.2 shared sources --- .../resources/data/mcheli/recipe/gltd.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/gltd.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/gltd.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/gltd.json new file mode 100644 index 00000000..104ac108 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/gltd.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": "#c:storage_blocks/iron", + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + " B ", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:gltd" + } +} From 5fec3569e8c22cc504d51483125fccbdebdecbd7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:13 +0800 Subject: [PATCH 0359/1461] Add data/mcheli/recipe/m18_green.json to the 26.2 shared sources --- .../data/mcheli/recipe/m18_green.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_green.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_green.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_green.json new file mode 100644 index 00000000..1046d9cf --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_green.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:green_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_green", + "minecraft:item_model": "mcheli:m18_green", + "minecraft:max_stack_size": 1 + } + } +} From 32c759c5d9b3517a6a84d0e74a67106a5f2b4672 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:13 +0800 Subject: [PATCH 0360/1461] Add data/mcheli/recipe/m18_red.json to the 26.2 shared sources --- .../resources/data/mcheli/recipe/m18_red.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_red.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_red.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_red.json new file mode 100644 index 00000000..f98ac51f --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_red.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:red_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_red", + "minecraft:item_model": "mcheli:m18_red", + "minecraft:max_stack_size": 1 + } + } +} From 39cdb3cab58a2c89c1f90ebc8bb121f68e6178a4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:13 +0800 Subject: [PATCH 0361/1461] Add data/mcheli/recipe/m18_violet.json to the 26.2 shared sources --- .../data/mcheli/recipe/m18_violet.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_violet.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_violet.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_violet.json new file mode 100644 index 00000000..b05f48a7 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_violet.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:purple_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_violet", + "minecraft:item_model": "mcheli:m18_violet", + "minecraft:max_stack_size": 1 + } + } +} From 3a33b576f870b5d11012931281953ba31c6e9af6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:14 +0800 Subject: [PATCH 0362/1461] Add data/mcheli/recipe/m18_yellow.json to the 26.2 shared sources --- .../data/mcheli/recipe/m18_yellow.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_yellow.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_yellow.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_yellow.json new file mode 100644 index 00000000..57ecd202 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/m18_yellow.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:yellow_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_yellow", + "minecraft:item_model": "mcheli:m18_yellow", + "minecraft:max_stack_size": 1 + } + } +} From 1a62d5a7b4a0929052fe8a9655988fae0b6930b2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:14 +0800 Subject: [PATCH 0363/1461] Add data/mcheli/recipe/parachute.json to the 26.2 shared sources --- .../resources/data/mcheli/recipe/parachute.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/parachute.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/parachute.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/parachute.json new file mode 100644 index 00000000..69eab191 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/parachute.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "S": "#c:strings", + "W": "#minecraft:wool" + }, + "pattern": [ + "WWW", + "S S", + " W " + ], + "result": { + "id": "mcheli:parachute" + } +} From 6e512df9d36320a6b487bf45af56eb2314bf9a28 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:15 +0800 Subject: [PATCH 0364/1461] Add data/mcheli/recipe/rangefinder.json to the 26.2 shared sources --- .../data/mcheli/recipe/rangefinder.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/rangefinder.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/rangefinder.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/rangefinder.json new file mode 100644 index 00000000..58a13385 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/rangefinder.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "III", + "RGR", + "III" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From 81391fd0be2a5ddc54c8eb39cb1bd8b5b0814d60 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:15 +0800 Subject: [PATCH 0365/1461] Add data/mcheli/recipe/rangefinder_recharge.json to the 26.2 shared sources --- .../data/mcheli/recipe/rangefinder_recharge.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json new file mode 100644 index 00000000..d6bac29a --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "equipment", + "ingredients": [ + "mcheli:rangefinder", + "#c:dusts/redstone" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From b19d365c115564f26cc8206a49bf87aac54aa81b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:16 +0800 Subject: [PATCH 0366/1461] Add data/mcheli/recipe/uav_station.json to the 26.2 shared sources --- .../data/mcheli/recipe/uav_station.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/uav_station.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/uav_station.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/uav_station.json new file mode 100644 index 00000000..24b97d55 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/uav_station.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:storage_blocks/redstone" + }, + "pattern": [ + "III", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station" + } +} From 2c7a63cb4f498e1c5415556ff4508423a1d3c8e2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:16 +0800 Subject: [PATCH 0367/1461] Add data/mcheli/recipe/uav_station2.json to the 26.2 shared sources --- .../data/mcheli/recipe/uav_station2.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/uav_station2.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/uav_station2.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/uav_station2.json new file mode 100644 index 00000000..74bf0d20 --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/uav_station2.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station2" + } +} From 99b05f50e5479011df1613b59f9e64eb2c8a62fd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:17 +0800 Subject: [PATCH 0368/1461] Add data/mcheli/recipe/wrench.json to the 26.2 shared sources --- .../main/resources/data/mcheli/recipe/wrench.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/mcheli/recipe/wrench.json diff --git a/minecraft-26.2/src/main/resources/data/mcheli/recipe/wrench.json b/minecraft-26.2/src/main/resources/data/mcheli/recipe/wrench.json new file mode 100644 index 00000000..b8c9bcff --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/mcheli/recipe/wrench.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": "#c:ingots/iron" + }, + "pattern": [ + " I ", + " II", + "I " + ], + "result": { + "id": "mcheli:wrench" + } +} From 8191fc6de8fc82c9bda79dac0aef52dc0db5374b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:17 +0800 Subject: [PATCH 0369/1461] Add data/minecraft/tags/damage_type/is_projectile.json to the 26.2 shared sources --- .../data/minecraft/tags/damage_type/is_projectile.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json diff --git a/minecraft-26.2/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json b/minecraft-26.2/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json new file mode 100644 index 00000000..1042db9e --- /dev/null +++ b/minecraft-26.2/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "mcheli:bullet" + ] +} From bc4dbdd97eb9a9b288b083975512ed95ddc0cf2c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:18 +0800 Subject: [PATCH 0370/1461] Add mcheli_logo.png to the 26.2 shared sources --- .../src/main/resources/mcheli_logo.png | Bin 0 -> 23745 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-26.2/src/main/resources/mcheli_logo.png diff --git a/minecraft-26.2/src/main/resources/mcheli_logo.png b/minecraft-26.2/src/main/resources/mcheli_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..95c72198d76d40c8c513965b3c40c83712ee54cf GIT binary patch literal 23745 zcmYhi2|SeF7e9VyVeAH3vJQ%}WZ#z=Nfe=wO16}(vV_PoGbpr(RV-1m9k^W3+#H05LyW&;3lo-#Xe4geT* z2?H$5(8uLlh3?RM+^G{rwjoZ7>A@j(O+%YqnPXv(3ohVbbBWfxnA*g|DmnsC@J^w*!<}JV&wSul!dC2 zLt!s!f3yeeO~1PMF_^Jlz;>iuWCky}UX7-%PPN%Be-|7!waf1JPBX3xr#?ui9P13> z$qlu={F6Yvx3fAWU%aDK(?Y+HWfE`}YVg{{-?_QA-zRNN3C{!luNx30``K#p+_uQ> z>eQRnxLpl*lQh}^f|_$(xZUaW2}b|Ugt*Vii7|GD(eD?ATQeAdss?DCCf2+dn4{E>X`G>og9cU(oP_I^ZE-5r} zL$cd%@N9*d{n6Y1edqq}=#faPjf=N8_kVc0X><2V!``Y`1a;L;aie~C55EJA$;rD_ z&kdaGyb~*_^bJN`_<+I-f+g-_Pm+@Jz-2=K$@pazHV!pBj|MXu-z1?~3@E&s_Dxw+ z92;*-DiOx~iCAY8D2jwb{qhA0gY6RcXeH;m*Y%8rguRIWcl*o#Mkx&k+pBibQwTD2 zr-B54I&-0d<*7H!Z78CWv&IEz#3sjbIULO(;?6VZ78M??L$=jkO%I);xltZaQ~pnt zUn*JiI(<_XU$ZyjothE`?mI_|32hq;*zWfvr}Z&_@7Cn&*L=n+0eWAeoYi9C^)o3_ z&{zt$RQ0t4w8XW%tUD%-Fq~j3*Cz)%Deq+9U!cK_ojAHUn>amG+~Yn>Lcc!vsj(Wi z3`aWI51cXt-lc*06>9ogPn$e7Wl9##ZlwHvNVzV{82#kuvNgfPGQiIaHAJmXH=kOTnIgcebhPTT{t> z=YUFFVKHVIW*yI;KDQAw(>&1}pc;aBPk7rj*1(FU4H>|)QW~HRG!AIh9cv0!txf;) zIjeH+pT(%MBx8c&g+k%EmvE(DiP5-wMs|oVnVtNCi*G__Ylz+&YIY7t<-odau`1l_ z&#!M72OStVbHv`jRk(=#)x0eE>Xdh)r(FZv0fKLwvvfUbgVDcxe!=+1S+iQE`Ex(s zy}f>M>w{v5h@B=C6f(1oZBH3>oGB4Lu3@0{RPdSBt+SH?e;#H=LpI{#qB#XBTSvMu zMZ%-yxZo1N?n1^RO84&t3*W&8y@MI4%I5-FPSY+0x|AZK9B#YZi#%6vhejHL*QB&Z zzKe?^@g-`t_6-4Rzlu(^ZQrj?O8?3f#l`GogmJb0Wi*Z;J@{6=k`4_%*0Juy*%e>( z+UwzdGkfiyurI94uGg53Va9KU$^3;{wsvm+upRrgJ-q&UB`AL;-ybr+O#jmpgLM*d&o7 zQm`mNp%d&;-<1_0L)~wRr#xRW^$wTq3;6g`BqO!k`;r~&yAL@6*aAI_6bc?|bQNg+PJr@S`nVCBJZTpXKMrzl$@%#R00=F#gE<4gmOB5-}{2myYcpwac+^dih z%FCr#w{X3_H_d~oqod2iA;To=hd(a*3lglvM7PTqh@@y(iM$v`O%x{06_yYm(u%?z zep2Ct{nR|TvFVc^tg5G#XWMpuwEuOvUATna%2pX7S`3}$g0?)5=3#-~443%;lK?Mp zPGJ!mBMJ9bav)oZXyv6BO&*5yqTI;FTw~e56gg5#%Kiv7^thnA7TdS*=2=*eu(L;K zNz9CUnax7(zvTOwzb*g3jku#*jYyUPF{zx5B5*^{nJ8+5=dP`@pZr}w{V_WFKU=+) zJ9<>!ePU*l4W)f{mX zBPIJWAm{iu{{sXiJ?-V|Gq~K@0%#+?ea*TOmCNSzmC2l;3hm~bp6o!f%ynz`-qd}{T^UA=TGd&kP%M^Nu z%JrVMt)9aQ#zA-IWvMXR9w0L)7+i@!aA zq+#bnvoL;lDfV@_D|51xTH#;f(4nL^H+lKgu2lc!czAB;YV&290S2!~yEL$yxRpOz z@)qHw^`Y~-k;~~H@81UW*WY0OrRgbKK62F|+wq!6PHoSUDl?ipj{TtY$r|JY>bSmS zG1^&wx(1q}=)IUf(^`*8k|j9brbxg@(4SOCSG-b(MDfZUmo}n(JjFW{^)cYt@uGdZ zhzb28jw2D}P9f3#g%i9V${e$$K;i>ow-;*-kxvgw4}LHj;>ve4L<^e|oNOy_&wXMU}S&)`0s{Ce!Q!GLeN zDd*GG!i&{X-s08|%dVXMu8{SYc$@`wlFM7TylQsy-3ipC0=WfdE5S15A+V8 z?7Hg`S3l?>U!-T4Up)TGOXs;9F>y#!t?+=!V(!pYA#ZsyxYy3Za+s-KmvsKy#h6QM zN6gASZ_1s;VI)b@$2*XsxhZT%aLfSieZbln7R3x35UjE$lANx5%BngZ)Ho)1DEaC8 zd)me|l1o=Pq{Qz$xW#0?fpFJ!z`hCUw>5uy}R73dwF z+Q`0|)t_DW{QPi&s`<`YpP9%QwkqztZ1E|%C05Ry$FLW1^?Dn^r_=9MnQdqeXpnr4 z=S9Sv+cGph@Q0EG>Bs|D+qh8M_I;p$(oJ06Ma!|9dC>gMbaixU{N;Gy%%1B+A>yNp zEQkefx4`^Jj?%_Ir;`S!cdZ9I4GJGxo5EgAWFNEb&bq7nm*2+6Qu5k00qf99;(2+W z3N&wi(2oY_L<~R%VoIZl$Vd@n6ebS9HF!Ze-*20dT=?s;{8nYPN5iTGv95pnv+>V= zyvJUVDvTfaZ3 zwsA7D|Nec^%nL4b)Qj3*b>?xw4UD!d35V-oK7>5PQga*tzbITe68En0a>IE1xzij0 z=5DcW^L_`E)*7~NPjB@uylJ-Ypj-|2iSs+7#HoYt*S=IS?G(B(fLNaz9P3u9r~9zf zPnL%@eY&61(qn>%y?dC8Nfx(!{h=_kE*g1gVW|C6yJq6eD9!mF5k!>}@<*0V=};Yy zYP^v?oHuQA<<_U$X6WlIehFrFcL2u3)O+;$&%E``FA33+(-7Y64U-5z(Qwi-H@ECK zEb4|SOQCU;r=;~Y7*RzhbaU9pJ%O{eQe!uOP1Vfb%HN~|tLeJ{%-&|8zgF9hd zv{VYCaW9x~q2Gv!>a^I*#}0D+5O&U$FyUy+5|6<*IqqJUMY*v?%CUnC*o~UKv*r+l zw|h~x_{EfSEqFoWXGs6=4~`j&d{GySNZ;YO)15Gv55B8GYqOo@4xUpxyOa6V(L?UD z;w#ntaw`>CoZa-Yp>EAfIi8ATDz*#oD*K#`2b#kk11C+kj8ZLb-s*Ap{8*%@IrO?m zQ&3EMIpuKa$a;$QG(+*X{`FKruj2TJV(tC*44l#SR(PEMm$Qsx};E zcSNRzpGDv7Wj`wUElnse%wqN+oDFCrU+aUw2ZJ_1DPgy!bMrTLk~^ZVBy&oMP)xaY zJF{#4cgNo~(|}vk5!=8ucYLKR{_{Dv>@x zOF=x{YR**sX>iIqQ~al)ii;KdUuk1%rE)!q{n}KZx;URq`8TeMY9t4xaVEH>6-pb~ zuK^$61wIb~#cUbkn z-(4YISY7*y|B+cTxul0{f44`HUFNU9z0!C*+pzy#0Qdax`{%j>L0Mc>v7=fA#1E>g zM>Gz%+&-K&@>Oyq6jp*Rbx_ES#>F$?w9nbYHPK|xVM`9JUcIvqA06x0Ef6XHrkMAf z%iw4^N%mUsKXaYK^umY&hHZBchX1yRS$gP6Up(W6b40PknUZ}^M=RyexQDy6oqxEJ zCyw^onf+pXT>YeU9e0w*fih(w?c|vCMPE-pN}YPChlFtVN39n@3qFM&r|jhv=PVo_ zo-VX8QsJNBw|4bGeQ)!&T2%XObKYCvT3K7}`t5@;%dZdtuik&|*S>n^{fDv($8k}( ziYR3$bpkawGKz@$ropJ%pnte^wx`g~)=(H0A9>PfvNgs#V>p9%=8(&oi z87H}!imoreHzc@RBnbBh6SXGwlASb!wWQkb>`f~QOq$>PUH0na`hrq_Qq0EOyMaU} z125~XUJ~=m+XZ;Xs&f}-I;ZCQUxf+?qUntoM$3Kc%zCV&W1E(KMy$8*C|0q- z+6lMFKb4o_En)3*nt3WW<-4WI3w~?&)A=wME1ddAdSi{5rp7$)!Cr&IiSeFbgRui4 zWH_j7okJM@Np{vuUMZ?OKgIc=*+jaH*>tiEmlzp|F)GD)Mr95+6#VS2K4S3rV2C?f zIJ233-J0bbQsM6Si!Tb%B9X_U4L#vr4~V(`E>cBNWw-eAJ&<+>x?k&2J_tE`4?N)S zSCKZj!T;N=uHbp7!FSN{v$^J`avS@TSD;}2-LK;idS zfXVgn^f|l9qWrtxKCk4Re|Yool}xjyFQn*bE*4#(6KwF3U$`1cQuD=1^fGLmWY*CI z8^4NY1xnLN5@+o?oEar}q2cGN^elk2<`q z3*{hR{|;|Yfy#%v*RGyW5`TUyn;PCL(aC|9WL3 z`gL^K5Pa~}8P2|p?iE8TYVG=tr2)P&pIaqISr(G9#2utJ;jIO^pYG28Mcy-W8Gp_L z^|+l#>|(DpA|K_mhM|u^mJ6}YQzu|N)0%hnr=T6GrH3dHdeZ zSc&`*r5c>+<8Ut(L%~fd?PFJT=w9vC+h6druns2Lq3u*q`&vz^_r6K~_*AOd2fWj$2E{S_mH7N2;qD$jtk4h#|LYi1NtG zGR!(iZSeITE)PkT%@ljDo|}HFitCf8#C^H!tzb1Qg`T4VWku3Wp&mF+Z*yW@ppxDCQOXg`8oSIPV;g~ zKgT?eV>Z_u_z*ADcc0lu@%f?WqShik+c!;=%lKeV;gUyAB<4z4pR>r5CZ1yg=Ma4O zNT3EV(bh;$C)`OTeimBHFz;?Nz1&8T@)k2>}>f=v6~QOCV$*9Kaj({6IH&N9!Lhz;W^ z?=h6R+g@zh;$+EePUdj^6SIMDHU(aNRT}-(N3d^i;Mif;%+OrOC~Hj_;@N$;X}c8Et#(&BX+mb zcQ%4k!j|J?*PN-}jZ#*7cLS-TTeBYwD7{|?vDp#MdqO|bzWlM`>dAf5Q26GiCR$gE z9Euq!y&q~UiJZf=|8CE}Or1;Ngf_%q zFP$*xE(KCAe0-vfOBXPm&C@WpK}e-65Y#&2BNXZ_xtg#9fOYRIL%EA;JtMuuTmmN zL|?M7>v8xZ3lYNt-(9cWzSVXo=(Tq(;m|bY;$<`E@IMhocGrH~cgE+u@)g_k!RIo% zYLB+>F~<{{)%PwW@7=fe>Yv=5ooI2&>Pe_S?l4M!Zpm1EGexglz8HNlvIGFd$}tk2+>H@1j#L_mCAY^(L4m^j?#WHRXiy zgZFodk94#^;YIR66ljLHk8^wloviGj`{x^Xhf(7fOu$tiS|)ZWqhrmqZaY5NOHmcC z7g95=Qq>s3Yi`YB%GhRl4Ar^7|H?4;T0$f>_|>$;(RS>_Sucix`ud*{_3bZ$p52J9 zKJsp6j$KK__)hM4HKnfLVOyvOdVCF@F@uylB>>X>fy>tf5$D&t-S-1mZD$X?6ag8( z=ntk0Z0&3*&OOtV-{oqKDtLXx6uqQP%5M+W;dzq7sO|~-y6|rGz0sd31o9`3qOq4A zv&}P3Ic^Hmm0S@tR?Z_wSCO#arg!-POct~%j6Z^tTwg~}ls4v`lo-p6aRTha=Ig)e z3Z`l?=9CgZez%p-^b>n&E$MHNGK00{(e>i27j2Z>mL~1J#fq__TW@xy1~wbU8V6_* zTG%t!QZurzXN25)oS}R34#95M`Od4;0jdVZ0K}n?X<(c=QjLpv+ay9UW=41U$F3wl zg&E9NeKuF5Fx^|5wer9hjE;5C{;V@k+lmS3!Ksz}0Z6dVA&88u!>)AN-?Tl)-Y8^4?kub=Ox>i;%gSk?SOq zK{~4EoGn4P9YY@C4?b6+zz3Wp(bqxcHCUn;KzDf@0G4z0{lzm*Y(;@e;Abr*s4Yxq zgOQjtD#4$xR_{njI2jo@O-Zyr(GbH`uuxJPx%AmX!{GhGsnD(3=s?=vu9vvAYiyZ2 zspWeOm8tbJJC6O6qb+mZu*k`RMG3wu@l7w>OqnFd)m4OiSz0gj!N|whwI{*sPv5ep z<;x-x;R?CEwE)PErQ`;4-BDr(qM-y<;KT3>+xxWmp~#~IrtY&;)QVDWb_ zv2JHt&s>?hns3S#Att?sm2LOUXY|r)wO*#y?^d-G26k9ht<2FM%O72dH?Ca^Fvg6R zT~QWh0U(hJJaLCXb9Q8o|0tAkedML-ak*i3K>9Y=#ePRAS)zA?!3&;jGIXfvUdtwK zo;ze+?Zg#vvA)fN7XI(_^D8J)MQ4U=_GLWTvE_e>I=r$T|$uYD|N zBd7n`0N15jia}dY;iZ6?TXOu(6|uM*0G+V!b!2m9^ijy8KPqh-z8u9q`iAvlyey*j zmk(l6{C?S1QGZ-3+sG0t+bge_t(Hi*1pjIuUOro%AlFVyr;Pf{=2_K$xE$O6ep1%+ zDn*>1J5u=r@ide^1b`<1Wb)uBzojawR}BAHF!BTg3nX)7`hq?Rc7HEVoxSf7+}9Fo z_))#8M6EzDAzbhib?Q;QI<#yfmcqU(6G}L%3k%fuhSK}br0ivw7WRE#w7oTGZ2M{R z?9Oy0oz|YiZRi3pKhS(r_IL>9ITSH4xb2N5~5pVOL$Sw1a`t*YZ zuRkBE=ORrrNC@9rQ`0v+6@SiqxL)yD{pcHVX-bfNO|T>+@xf9!(Pwz$!PKSPbDgi8kaT6Biu^u%F=)RldQC*cD;$r}CVMgq z-NXoHUcCB=z<4=#EaxihUB>kwW650!~H?6NQZYL!s1}t0~K!PtaxX`<>wn( zwhCdiSFs=e&3$O4v*qRWCzrGXbYD#8ikur{4&;^NnNL0(1H1Tu+C*W3vst5H_rfJv ziI-t9msiwGkI0`%!AAG?-K8=i&oxnw5{a-V0FXjYo4O9cB7fXO93*c4%DyRZ_`w(- z5}6{4QYHR94M3q|3tnK-6b^JYx9rpcmv`ONRg0$yM79(2V@AnXiRt>AYE#EXPay0m zrF6N~q^*DXOP^&x)lu~9KOQ&$jj+UY_j^P!FckL|cbteI8k5d}@qdk?-{r)vs1URP zc?-7Oylh7=U!DrSA+V!evbs>WsnSRL+U}T3wNIH=x~)v;%-%h+@FTu#cT!1V5bk=| zqkcl)lqDIocEd3#3OE#~P=I;`aAe>sq9E8dAao2=kBPtYTk|+JjI%dfehc}8yXC8Y zEI?`Biu&y1VEiH^6X`Sx9BpU)yCuHF3g(01JFh=i%{5hIYhm4K_oxqZsVYm(a?SLL zn_0t$^Qj+Q84jA(%~2cAjoM3A2{{p3IZ7#DcOWUC9z zG|c3)J~c&nE%jC#%h}`Qmsg$@c&;@hILM9Jcl4z?@1a-Z>MePau@Q{g=eO5%ho~>{ zs>?5GtBX8*QeZGc$yOMt4gL>cK0?I@9{?^ApdeR~GXS}rR^JPDB_bZ+X>;0(;X5Xi zlV5Dy6?0?Y=v$f7I(g4+f5!G!b7cD%%LoexHE{?NHf~hi`T3>7f_AvJ?n+KcJwt|tc;PBc6w*~UjJ%*s>P2AAr7H$n zUeQdKwPp8ya;Dtj>cJ|~HO&E@*S}RC&X<4BX1_Pju0f6^T=F>UQ!v`^R9H5qEUbP- zpR;zGw`zXqSKvnZN6)3G!(~^0IhOq^wkzBC66=-P-PcuX7Unc+y1Q9_G8Ri8?tahr zhkjkodBD7ORdwPelOH&t#!>hQ5UE{f+CPAC+RIR&?&-)yJh}bhZmxoN%5yJ$c@0Cl zEI#||)u}A0_yoo$4S6B{csA21k<49gw2of2K@es1Vb#{GiC{uya&y8#D|?HoNA6?F zech|qg1f)#QL;I=igQfG@yWMYYH6!&Wxiwb>z#U(Zja0epVm3)qB^5~0&qs3gum-?@zhqw2=hX` z64O*qx2NB)Z5AYwDGFf}H42B>{CJ;uY`!o0{Q zDt?)Lb0uge$+^fXRsjLL|8KNlafb>2rxzVy-AmvHl5_QNu+Gp0g|+oa;?QmYAf3f| zZ~-s8wId23VW9{vfaHuq8Qpa`Zk-iuMEvHGs$H3R7!zUlwvIae+a(Oz#YMr2D6=2H z4Tytq8aG&8cZTE6>_vmpQ+sEh=+n93xHh(SyOPLTeONW8>mr{=3(fltZ<+|QBo}bwIar22&R+KB=R(DojMAA{+tjbg9 zS?{Zft+!=?J$`dAXu?LpA0zdy=H6#EZAgV#r@fMif~z8OZEmNqB&+jid%@PE3%jdZ zG3FZmZz6*_=CiXnDAKf>%q7BRmY+u`k-$E&mG-H$!3%HFVoBORW>d>|h!=q}*W z%s$bKBp$SilMgtH)=dkPUTRk$KQe7RJi+JxAjD)i1)@<^U&{g)MgX6hBG9hCb_BZK z5eDiIpN9fBhw>bt+&eSFoe2o543et&fjh;QIf@NCtl++s|D(oKdOWIQaK8`c_qZlEYOd@Uf$GQ_|n zyNgRA{{tauVKHNVFUy>_s2e&I<<@_~pTH`K-HKo4LjiZ1D>Wy{HT= zA9t7lKu$bC0DlKJKyKON1@qfuJV1e-1jZ)`wtfoDd+*KmctTjnEfRP#Z`wHq8e{uj z7#D(Suq?=Nr!3j*=C#CgzIqHMVA0-z~(%1T;z_KdG_`U>Ce7wxt_bij_zV zAj^V=J8+WNo(Nbrgb8(SJ7+Rdzclp`keH~IsugMa;YNN^`j&5gB(+E#*RM05jv(o? zmv$mZjyfQ18m0}h(^=lW2XSqD#3Qoc&JIPL5qx^Dhg}lQlW4UV(#l&}ccox4^+(@$ zR2dpnaw4? zpAY**m=y5I67pZL&vcJ-6{(vg6Rc74kQ5C`a1 zxc2}0`}>TDr8^4cRn1Bq_h9eKl;$85Z{NvK4pVfc4Oz?MaF9vg%C|dN%^uI%IVFWa zH~SvxHxsIRuN?mDF91WhgU`Ev*j~}Q3bI#En0z{UE$Z#j$O4e)Njz|tT9j?n+Y1i> zo}%iuCz+?>PDXREkW_j~BbhY;F(wv`)DeQmkbg#kqps}hi(e`ml zjjvRY5ICa588=E#^tOU??NAhv!t=Dm&3LmD*J|LnR(4>0aT-lN2zqHwAW@0M7l!+! z1u)5hVD>tK_=V}H`s7od^>-{+#DCVM1^xYKlVSWlOR{vzxxhZwCyI-AuIP>2UOXW@sFQv&*dp)Aaa|}_K=7v>J`5v5 zRAm6bh-(A1mMDl*QUbV|9~Xeo7-Ad-X6e%VaYGM=%ir?>iM3c1(PGaS=E9uD27aj( z0o=ILA@zl)4`*M@WajtPGrUEB`6wcKnL6ktPAUx#i`-9!~>kDRYrmYG_vMY9%N3`Z<6R?-s&pHOK{ynv` z)YdiEU5%Q3S8LU~#$B9`O6tm*vggh@i1;{{GU^p*r*u}w<_*T+kL67dm95qjBCC4Qd z!)Hqw$*0~N>&PyD_R*6*b+OF{ZGu=6Aw`Sj8{Jg%MCK@`? zyt&>#yt&?9v$_6{wz;0xy~e)!D05q+#7%FrfuYUSGO4dE(fM#~GGZAY;#`9*o}A^* z+GE|R6EPJTj9+MpXctdDsfNn!Td(s9bi9}X1H)QN&Tz01wPR$ERo96GXbC4k3<*FG z(@@aa1dNyl9?3#c1y}-WS&$CMnE5iNU?!N4r zMs4Vwd--RM8TeFd8vvq3l{pyCFjw`8S01lw`{3EYe=%_E#aEKeN6+6HI_up*I%SSR z!7~=Fc-N~?9fMdt!PPZvD8k&ydVgABX!wM-Xu{ffxVBPf!q%7FKM^a8%Ew!qF?j66 z=;E743(XsohJbhx4%Bv70Sk-80v{Xbk41v{QxMRXg>aImEacbKVbE0eXVfYHB4vR; z69_{JT?ez`u=)KRAPA68yksM?vVr1RCoZBDHWb^4`~A|%JRtTt!eylufaa^c>2g5k zT5s(v{hp+j@7=o=)8kHDa`8zTgdwK7PF{%xQS#s+nsi%km^2Rj?8W@-X65bXp-^Yo8 zzyv6q|JBD`h9%A;Fmj;qI+%a~9hi_6DC8sF29;%RSRk?v*l$|oq@E07r;2_lf{tUN z&fZf`fVs>Sa$FJsX85Em zqj}-yv5!DBcG6!wNXP{W1DR~Za(jWEB4_;3SYl-+e^Qe0V8U)9sLZN|6P>)8=_25W z#zGS(xSarZV+D=(xq;w-I#|{K?~&vmdOmD(2G9wDovTovwkQZb0xksI_rkFf$G5SG z40C`)hRuDxCZyr7cna#sOm zk^6H(i+z_JNOZ~qr4Af_5tx&!Fti@1q4owyHvcNed!YflKES1eMr}tpS!l28E+nmon}Zb5T%xlPpvo!8qDNBAP;z-`}ZgTIDcON7o!JS$NL5dkhdvbCE+ zU^GGU52Yl_G;N+MX;U`Lw%<5h`raBzZuw9AWvvRb-DT?lG92@Y{Vgb5ja?r=m*@~* ze3ZGDJe{h9)p``A@{tR?2;u^R(F;3_1*PLl9&KO0@oS=%TR(Mt*%~>9--4k{n-D-_ zS)ZlfUJ|I9VMAe}WPQM#0Ws(I~}zE;WU1>k2;Ssa^mS>P6S48L(_co25EzZ?;f zMHOe}2Mk@Jko+aEEf2Gqn`X`0b3g~cKB8uX<)>YeUzhQsA%Uv`u861WAJi@I9~?kT zeB$+U@fQ&54u^OydndMo@L4ckf_!wG;Q{-K06#uSm%jfYc72p*nR_I_!K}M(V>KYg z$bC}_=H~`L@~QfuyLhQ?A0fZJvQ-7(7QzoVqntti70z$5qQwsyw@W0`8t(WRRJO(cEak6P2ijfT^bH18rT7? z3w|gDMNFZ2;lf{Y!s4wy7lDwy4SQqw7DLZhwYeqvNQ1Q6$TOhBO^$_KM>GHpc5?Cv zJQA=de?fgTKgfp$!1jhe&R(S9F4PctV2e;ciWt z9GBiAYwlQRHDH8f*Y1F7SK^dEDUH3`$R$Qc7LA0dSJQ%P9xwicxZpJVMf%hv_SJtP z$d}AoUSK^!;oQ<8Ts=FBB&|Xi#5xoFjF2F2kBZG@G->sjqKuQyya}CuFV?SzHTda1 zk0h~yvI;@ax>7F!XcUACC_FmGqVDA>QRUiuAw|c_!uq3ZLY7X_tE#g!!>v^nbt_K> zznkZu7jCoXv&2Ns#~_#NGYO~Vi$%=k3lj3G@15@wHNEcsI4|g9OHYi>q~~P(q-Wo- zf7!LskReTizCo#rzCmDHNazL6mWh?JfE8ruzfY`X*XNL-SALxcU449ieLbCT_LZJ& zZ-D%MnMZq)v+R2r683ZRy|dHyPUUluqoRCUI4Wxy>y)QC>Xcxh1|b-q2-DE0iY z9FN&Sibp+Lw*dH#Bq0F-?t%gvJS0AdIoIF0Bl@(WDf=k>%k|FMjkC=iW z0^~5j#QxXEeFO;HfD=RBF(q2T!$F!3J$zn$@y@1KN)Q#<77Poat0sC5f}b!V7Jh;z z^_UaT&LrpR&Pjaz>%rcTDUHuyXKfhdoh}&>Mz?0KIk24DwZ3gYn}lAM2FE zja|1-eqD~gml8LY%BI1`{{kcT_OtbIy^p<@NW5oXWk)?bVIuo5^JeM9XZtr*KSHHmQBB!mpo#@WbqVn}eeTRevP;?w`j z;^l_~7ey`h0@}54`a;v(jjR(){?Xe;qLLZ^Veo?!oc^)f#-dW!h(Gjh|G|%7qzWO)Qv7KoLE?+P_ zy4@-jaAmpYFyCy?@8&*Bi(yuSV`U4sEqK{n&m~-c^;vA!mGIx53jqX9%N<2^S^6>q z+V8lF*#~168aF1Y{OOh4!`w^q+ElHM8sA~;JZvNKKYzD&PU4)mbPGGsP5OF3_(Jk& z1e?VMg%r+=qdmS2<=J6bBif+5&J&`Q8W9ln&3Hto}yNpBXI?sMsXl9O;RM%V%$t_^q z)E~>dv6L|BS9AHF+uJR{ue;;)<+>2UWmK^#ep6!b{z6mGQX!3vQoZxk~BU=UyNUNkSUp_jiznx$H9) z${&ePS_SdWWMv!*Sijh(KtutloC}bunLs3zJ^lBEHERZFf)zxCptxa2WPdIHnB__IYAe zXl}qU2wL7c|M3UlJ4CRka}!;9|7Y@L1rWKY!CtAG$aLA^<;lSipG(Q3u4KZ0nBPFLJhFK=MPP< z3CWl!#Z$P5{yhP2loN5|Pk(mpfq$U_Cir|B2%U7es0k*sXEbA(r<6;dsT*vz`1fvq7;J&Efca!>BMot2?fN+a zw4e1U*BeuO!`BD1wi0CM!NPmeh9Nr=4_V3HtiTMSgiesSz#o0qxT{jiPwx;tp-o2? zF`(cQDBFjetp3TtB#I%=AO$WTibYOga0&0e;^44aU5jJ)>hu2C^|gsh%&3LZd8T)l zRqCHngNm?AgBP|a@3*vIE@Qit1hd7yx! z06CKdGuyNKi<3!I64QG_M;7o+I0XMqgerXanR!_*-%3@lO<&nZPG0!Sre;IFZg|J} zgk4jE$}ZY(1a5GD6jN13a0)n~Z+{*9JcGOiv^x-fpfOaO>|;P2bS3s>W5l4da^k3u zb_`tXoiU7ROSEFU#ZqX6>X$TPVSyVXSP}dzy5Ayl=lpBDX^GDX*3=vIO|%=mN>_h4 zpP{c5)#K(V9H5qZhe9daZZY5S-KAjxFyEp0)10LNy7PpKNjpOO9V*UU2)%Tb#D|PB zj^yn-Fdt653!1yCEO3Tm*0IO{=2T`oBeLtbT+{mb$=H0_txwK_YbYov-^*;LHG41& z3K*Th52}+ovzMN+7pul7(E%(0Qp)GofaL-#TZ}m*onsviV+Do>N>jPXxvy?(qeD&< zaWjU>R*LaW)@ zQl$!>*`ch{#=K|=A;m8NF%Wd0L7r6QMt=(>7RJC}EI(nmB&HK;f|yLUWh&r@zx+d7 zjkE4x4#Zfy62(PhnzzNKr@!uJp~o#sRO*Hn+Ds#cX&+{irU&D9bLQwl(-jN!bb-AS z&lq8M7`~1ELCm;HzR271aA_y*L~o{j+e~I-m|@8C3Lk(N@(~pzpc5C6QETwiniwNY zEVC%H3_921S*ohD%@%tK9!J%77REvk!DOt3sno9ojh#&yE82>!+&4?*wkZry=(V|f zRV##eK!hB7R>Y$6YfzZm8OH|>9rON9Yw}{8M^8J5Wy7#?(uc)?mlJA z2ph;?x&W*rFA_zN+kG%YrEw(u6~CE~8OH30-?y8$obSBt`K>3EnXm;pRVsA^O65k! zZmwD#W86PPx0l$HeEdcOV8p=JbX45DzsqZW=$Ne(fz$s8l+f0)OghNFjikFcKQq!* zgecPNn-0ezbTMc0kmXNtb$zYc=56m1B^yTKFFXn=$AyZpumZL9gH6~cxqHyb&Eqw| z)c{f2S5!FsE*)r;Bry?<1;~<^=(EQ`q%b?n1#zAal95nJnUPC7kFpRla$M?jiFat4 z)pZ*ALS_MtcGg*;y-SvXDVZMJV0Z?nQ=!1+NTF!ZP4x#^z=tg+5$kIqA2~g z_P|$kMb<}NDnD1Q|2crK!t63ZJJ~9y)=ZDLHf09rrI)wcD;LCU-ZvDe0bfA2u zw6!ZH5_7oY-RY5I31`nFtbMo56v6&nXnPJ-6ZyD7J!5(I^)ez(zw?R~aEWFEC5>}n zoT1$vAv9iggyn@VikN&A#sOzhHk1@Ph-@bXM-zR_&`&v!Ko8PHvLRWS^on-Bxj_$R zHqX@7rL2aj)kn-z-0?vZU4V#MGwp-6{n7>c)P#f=zA>O2MNDdm?BPJ92{8HZuON3A z)15`Hq5d~sZPRDgr$tKog2y6E>*<>!^sV~6JOX{eIgs{KWoMke$%iE*ZZs7bq)HzO zPcnn_iT8j#^s2P)L+Y~Gvirk{S> zF46X%2gAX%X~FqPQ0hmO>uP`q+fUW!S8RR?R6q!Z6tSycMXB4J#qLqLv33`xG6^)J z;E}q!y5-P;`aJZohKZJ}i`Jq2k#1$++?kCqND?W^NcMAr{>cnu4JhajIE@v}wkKqa z72UnbV3A>@g%T=E36{?YMu29Q+caS0)HX^l>C^eTJj2$IDQjzhcSHfJ=NS|y5Y&0 zQm;SE-18_ZQ*J>LiAMIw!-t~EMBo|{7+kk5>h@Tax-tFkP6O?p<}aeMpnBtBgb>Va zVkK2sN&Pgyuc54%7>>A2~3E`${{`bX=@)j0%U}`ISsLm2=sG#(ItY+Mfa< z?tK0%y{Rh@MpTDI_j3qg8{#Ayc_vu>?`E|Eol9fu6~%_lK%xPFJ{2mO=Rd?pNFj&ALTOe)uT+ z?9X@4@!Kn=cu$MA>5c307>-CB0K9NZrZ3!lW@xgg&o81)Bx|0L>v=Au-E2qRw&uXH zlZ8^ek}NgtfqI=c!|7N6kOOkVw6p6B4RH|Q>s-VHl|}e`b~}vie_sf?ufcAreVFa2 z7@dy2_&pM_>-C40>eWqx^%vzP>;9dxE?F9xO?gw$%~vg+L)hDOrX?n>*6;1stNS!! zOrg0Wu@)Gk@xvm1a>HqIXlj07s)gSffeK9-|d|vO!#CTYe}#_z$iya zqD5JFk?c|Kt>n7(o>+bPpI10P<4y}ZYY-BM ze{1fGuIX*i&V$Bg+R7|_VNwONZ*@m1JpsFIzW$q1Z?pLKa*F~9UoGl}bqbt$c5lCE zL3LHP`#zuSw&a)9FR>EmPR(?l%*k09!C0SrZbhlzI)*({N*mjwOE~IRwzF735Q;5I zLM9PY+Qw{)*5>~|C0utrRsa9Lwv4Wk6`6%Dvb$N`8=+73u5gWzz4y5GCW-7-wnEA( z*Ua9tOE$R(ab0yUF86o({?6n4dB*#^&+DA$^Zj}~pD({UI2-g62iE6a9wPEu-AwhP z&G(q>e!ML*?9fUYlh%j_gZMLQqW6b=B-@9rWF8N#83@u3USk?$(57~v1=-R4Z`a4$ zloA9q03qgOsOXr{w0e1E^01~*iS)F3X~g)h96rmO@Vndl?8NEU_jY5sd)M#lP{Q+Q zW8|2bBDkkN%E4lu1u)y`B-{uT+CHY>hSF#{_uBN4Y( zeTkKPyErwzDOn}n5-PNA#oJrC-MFiw`Z1lcKRx?KCBD1)QCkx8ayZri^5l#BU`;}?=o zdC4Pi&tAm9tyiYEh)kP_T^mY=7)cs>$l_HHACf4&s`3OQk>gPZma5|8rMz2bHKNBM zZiU2tVa`HIvt%w7yeT~&}nn3P-CcgNK(gvtK$P1)QnieB6 z1)p~9`EI@|CvI6w1A<6$x;DSY319E{5Bz})PF+z;f;$2U4$svk9g7ua7u1fC-(do(T9U~R z4`Du$7Y}(M=BjY2t`i05v1RlYR-}kl{Hixi#0^R&3tAaU?x?F|v;v&WnGj$U1$5-a zYVd5vDge8rO84K46br_vuE&b-ZpFu%to$I!Fp7`U<{{)evR!^+G>5?oY)UxXEMTDZ zB8vdr4~JHgU#LOW!v(RkZZSxe@b!rGZgDsjyfRYgv`%9;1WWl9`CIkU`(qVI|8s;6 z9@Z7Wm_^lcN9HOXB_f^jSesVC6|xIVBzcEJIT8OlIX!P z$$@j-TP!#qQ123vHzYiVPt5YY3P^ZaDs9{)U!?!=`-^SpQgGW95Ow52ayHg-w|Ill z?-V5G_gA^&_7nXDMcVy)yt2waJ&>KplB_0cK02`dd= z?*53iSy2d)IZwtL#0VLx(2DfuAzdenz@o>bn-5ivgjXNxb+l$*PC>qrM^pxM_6D$RlAu*}RgJf0_h*8X zKs=|v#5!C)`tDB6kVpK_vKR|8PFFujy~R-GuYyWF==eRAwm(TLUQaA#k=CSFo|mAn#Yt8;i={EK>@#-=wQiRon_*H>p>;Kl_-3s~ZI z@T(tuBt$juD{0%uItd)<`yJg7n>0Y4|1tZudK=nH@Q#|O_S}5_Zr1ISoGAuP#?Ouk z*!vfn8rol0<`Ig9Fz_oh{@p9_l8+?Mj%0&j4V@CjcRj+|?L{7R?8ePk$w}GpDU9JKt_f{_u5H(xPT4Ziz3_-m=Vhr0p@T>G)(K6cM4p!y#qdn(W^6W(t;@}WZv zMBcwWfiFzTJJz%XIC1B@QDDtS4jL@!6fT>3ny5OqnCtwbf70rDO9c!nV{b;C+Mam{ zl-I}IS2WItI$Ycf^unFyqb?!iRF>~^e|{r$A{Jd0J53;|dEShxJmUVKF6i0(o7w#( zeh^9(M0QPr67s3ahq|WOV|fsWUZ-pe*T}fMX+6&Acs>?!b98-k&gwyW(T~0Nz_1>d zVFDfl#zGTN7qI~pAxGk(>3S;Vk8Acd&9Ivp>`4fBfAuK5t_lrrn2~1o#^J4k-G+YA-09X*TdE@*IL8vlv? za&^^{{M`fPD%a|K@Brk?cQ3``D!U5k`n36B#{du%MbU9wsUgjdsAsA9MmLx_8J{)2 ztEhc_yTMKA@nd?;d((QMX{3AR?1I~gj{;bnnm+r_7K z(zgfFF6?rr0@^g1(30V(?NIHE-iOZf%8|c%1{q@`ZF++lN2q-0F2Awg?#iYfnCTYx zLAlP0ginh*0ZYpa%_qo{Y(J+2*V^lLE7kOQ{vCN@1 zqsk=zZszN(4O%k4M<7}t>0p%J&3BcyAn6D9`#t)jb}FoB=jE~1UgquM$Z{%)_U z)Wr-sd*)N~bWcoRj;9oQO{LAo|8Vm- zDbw=yz5)9uz>@h4wn;h~A4qju`ntS3&w6R;?b`>Uva)kth_33YG!)lZzR!;Xz_LSoceQ=wcSo2t+X2Z^3r#dle8%!2^HH6uTpc8{5Pd^*^nKFaE1<_H6o@~ zFs@67UJaHXab@5|QFwaAvQ)kdeNq)-1~dC4MaewL%TY;Mcx?EQJpY<8flgI~=@BQu zN~=nK&FlU35o%Y&IL-uCiHgM@PcH(#Z<+E&C!POl&GmiCHplXemNjfe=Sr9hZ&c)- zb|79h$Rh3KIssV_YZ@;vtFOu6k%j-Q_hsgT$YpAV^)^oXqns&>X&n>u=@us3dDoVszgzbr=dE~?>9iz&jRw%OBX+~9xnB`3vg}7r ze|JZTfOURU#L9_-sS6z@TI!zak9+`3%d4~eCO?gT-rxQ{u%yOXBh=sXCr_@F`dz%+ z>l|xsq?f{*vz{(Pjs#rcYL#@M(0C^j#}53K7;dP1x9xY_0?tgIWsM2GYm7#qF81xD zzeL-GRySb!2=de*H(ZkVc?W7$5nLjcBdMRJ7TYu;iD{#vuMcgq$!i+%wE!O_wN@x5 z)~cwLG_LZWWq+enONllwbI9%HP`6fFdZehp)=@ueZ&Zl|fNV3%E$gE6jnO)uM_1(w z7fCyJl_o;8_7hIxe7tk)%n4@M9ZCkfd%KciRpN>MTNGJTs!-$3OEzEu7bSkP#RzIf ztKGT*IkUut#X3LzyxuX%&p_#XFLJDpacj;Z)T+8-==O<6J}D{YTx)o=fk$ZWOMsQ4 z*YVBv%kPUPU4JzlyI+^fxgWF5HiSqzFMX}G4+mIJ$R9&JMDPWjY$ws28LGsw@#P0a zNzk)qd#ewOM}|^XU1>UxCf3`il0Q6IVfqv~&h{-a4Xi+d@{4mL1Gk^c+4kIYE-ch+ z%DZWJP$40DtNzuUHjH90?jV35kQL>@)k7!Eo4`Wp9yz8{VssC)ab6R=KUDc|1+?}D zTBZA$J?r^LPlsYP_V`VLv^#q6aDo*Sxa{1k<1XUvDH5AgNV>rURmx7;b_PfEAIONG z7tP$8k`B}8_TL{!bDJK&jMj9f!cnI)zA@ETg%vCIfm5Bh!cX61!G8iIdx_HBTb2X* zuBKpP{N*0m{mY-Pr1>UG)LG^{rt*SM_#R3gd?wkNW(t&kz|punVDLA$jK%-dBycbN zx3s29CyhF56Lj}4v{N<<0a?r|_I;8(8ID8~>IA58ltj)drx5)Qd>7Ig!h$&Zft&6aseGw2)*or#;OV4(YudxP1tQm2>tsQP`aCLm;9j2 zCmYiJ&-F#*ivU79uBZKJpI+igFwLrbnRQVFABE+soY4SAcEl9QYc_6VZ#uf;@~;vy zscQH*#58%OJ@keGcyyQ!RCba>nc5ucO)zpUm>u_aL)4`o?rm`*D%W$0xfhd{Mh~e# z%Ky8A%a7?#l0O?O_|z{LB5R(F?VDv{hyA1B7p4b0mj_A&r=B|lFZrOt)IqNlBD<AZ!Fi@ep8VX+FM_69k6sC%CUcc!-rIU+_D_RG3T++&thk> z4ua$7Cv|jV5BJKn>hm72#-wuA0MpolL+te}s2U6e}?{kGb){YJn*7OOZ&YCcUGpCAo&p=9#r@x-KgsFT0Ig{!o3-kq*2NhCg?;@- zH%7RK`5wfY!M*Aif8p+O@7}Zysul$SQa7QXUVYSt-v*9n6;%;>%pG2F2bz7PM4;VynSwu88H_0t8&ovDA}^(6aKk6rH%PBSGE>L~ry` z#x+*2XcG50;-%bBV_wkz1a9k;@ z*j|6+ww)vg15{|^8;oVF9*%BOl*sLF)|Y{kaboT{zxa`9>q%&@d(%xG>Un&}Wcp+q zjODBzo{J@ul!mPv4|oz=(`#N6+8+u6A)VAxU0%qBeKM6eP+$hMM&!p*&m=-ox8Rwb zL2z|J-r4jd?hi6aRz77Lew>Mc=0CV#z?@&6z&PZHlHpTkp}pMWy2`Ywsu3{C53_TdFoW* zNkKO7SztoG{B^9)fEwzhAe)D*mJ(#6!CHjZe4TEE#7YPB>9mA-j5h7`##x*>_+4#B zNk!k;T^#mK$#jJyQM$_a>_F{d<`oU?VGcf8cRf9yW;GoA>C4T6kB+!HxPHTi=4Uco zBGeNb4}KV*oz%IWIfd0>ac_64XWRL^ZLbv)+Tm5D9u$6ZPiL87aaz0sR`GRwS$3`= z9!|ndj?2BDRIXzc6OGqT&e)uggHoN~h{0Y8^U4wG#%3N7Ha1WUdzy!fYq7k{ZAyme zHN)NNF;#N!@3b>=TKz2zl<#YH$$S=ezrL-Ns94_^*zxUyZ{r3p2zzLTyY*mxEk`bH z98NL8^xx&(kU%5EYp)}#JL3A?3g%->EvLrk|JLo~61=|p98vqTu2j0jQ*O?;(@(2K z?U~xR&RIhd1w1U7A(%?;`fT=_&&W+{+!iY|($ZGWvb@|x%Poy+<~}l)Cl$9Ob*9x% zNH+_duL&EL%Sy8Umh+g3`K~R!N81jCjUVtBQ8&vVLZ1I8EU%R^P9(0B8YB!^_AbsQ z2NEBO@qrA0s&F;#y8PYdu+F;!pT>UkpccH9gyCl=} zubU#lkDll2m&Ww>U#`{<-R-~?(|nvw`p?0?!CG5Xxy-@B?r(>bG&B=Z7zA`fb^SgC ze)p1ax6;SrduJ0C?~*Lj8wPFOKU60o-?2ek@&nDkCJyL)00N(G7Zee`u^?D|d)ep9 zLYoRya@o6JMj`N9o6|_+wM~Lo*fXw3lbo3)0{5De=;+I0f|6`)ZbO*yL{|SQ7?Evz z#n$9dTz;El5{GlsH=2rme%{uSJt5?o@n8ux8APsJn~^{L3!Zg?suDsLEGyiRkPSf& zy6u!CS%rV-O+64^K_Yse3un=hcg4dAuOl5R>&>?G`9Ny%ef*I}vZC4dOxO zGTquFsIHFm|9b6SWrrX42|5_lREoze8e>D&&opKY z3|_X_^aheC*h%i3VsS@Fu0qLc=Qx*y_|}H@gqA`MY+z!!>gXW_e<;>E>uh($;;X|W z7J26Oo3OZKzW1zPIun_4lE2^G!kH50Fz?FTOZ;J`Bjs|}xI{{)GzT_d3aFwI+Ma6p z41%z0aW7X~I`C9FC*NZDLEqJ>L8Fq|+<6bjRJ2!yo_iKgjO%bzcs#`G$bY8Mc09(X zRJK+FgUB_*K1pn9C53@vs!JZI3!&LHzBF2iona9())UF_K4lQ_6%|Ey-sy{06;Y^$ z1{ia2RiGXUe@+9k-s|AjR507#GEI|=rR* z1(pR5W2d|r4mh@#EDQ=^Ymsso#Gb`fpOG}z{IA~O<+qTH&u8@>An$(`V&ArQmvSTS z{aB+_6#ObpLE|L^$JwSCi>47K!y-cSAwl84T*4Hy(9CfVBIli;tIDx}_UT*UR>qHK zFB4+}3j^6aiamQ5?<1nnUr4Dv0Vnp7N;pKNGVBmt9c$l~?X%Mxtqy_C6c93gSr%}9 zy77(7FT~)SQft!NhHsy~ir+*tE$tMf%v%6cT#%A7JdWDOuy0nqJ{+>j21QaQ*wC&1 z-r`}*gGytVJX2fmbf~akgK5oBZUDmgq58u33<={MC!b^wpZ>W{HRJuE;5~X2c?T|E zQ8;@Mv<9a|sfmrQT)a0~I7y>dYbh4$zvN^eJP2=_2)Kk!H zbM8QV@l>IpH!U)dX=vc(`#X|)BohIXY;;s-5Yu9H+Msar#G~>FGY2k}1CD?fGeWY{ zDdH51UvgW2svM!;HqIU^_LTAdd&uoXo9=#{ z{u#97VMe$TX2OWIPKj-OS`$rB=~qQbjF+_*3qg>=`hI=UvRNArHFO(zCN1n&SVq#< z?d`r|)~X*H-PgfhpG~N-E-CSWmc>r$${4&MA^)@{c!SJ>d80q2oDgCX6If`bCBQq|vDCNs@slX#c4u~vWRKY#8`#%sYs1fEp*QU5+dk4Yn?I~jxX!iujA7A5 zy+4IRJ(AOAbL>zJKlV=iu;}j=h{5G56(ebLg4-DEHDhd#4S6pfWwyrkyEHJ91h)Og V`gRq~qyPf8|I~FK)~Q-Y{U4=vt$qLi literal 0 HcmV?d00001 From 72c66657a00c65720b2d05611a230cd996fa26d9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:18 +0800 Subject: [PATCH 0371/1461] Declare the 26.2 pack formats --- minecraft-26.2/src/main/resources/pack.mcmeta | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 minecraft-26.2/src/main/resources/pack.mcmeta diff --git a/minecraft-26.2/src/main/resources/pack.mcmeta b/minecraft-26.2/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..1be5582b --- /dev/null +++ b/minecraft-26.2/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "${mod_name} resources", + "min_format": 88, + "max_format": 107 + } +} From 6ec3a8b4087dadfc93a00f37e691b992ac3da4b8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:19 +0800 Subject: [PATCH 0372/1461] Add the Forge client glue for 26.2, with its single interaction key event --- .../jnhprojects/mcheli/forge/ForgeClient.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java diff --git a/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java new file mode 100644 index 00000000..fb861c5c --- /dev/null +++ b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -0,0 +1,117 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.client.CameraAngles; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.client.event.EntityRenderersEvent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.client.event.RegisterParticleProvidersEvent; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.client.event.RenderLivingEvent; +import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; + +/** Client events on Forge. Only loaded on a physical client. */ +final class ForgeClient { + private ForgeClient() { + } + + static void init(BusGroup modBus) { + ClientHooks.init(effect -> Minecraft.getInstance().gameRenderer.setPostEffect(effect)); + FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); + EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + RegisterParticleProvidersEvent.BUS.addListener(event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + RegisterKeyMappingsEvent.BUS.addListener(event -> ClientHooks.keyMappings().forEach(event::register)); + RegisterClientReloadListenersEvent.BUS.addListener(event -> ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); + AddGuiOverlayLayersEvent.BUS.addListener(event -> event.getLayeredDraw() + .add(ForgeLayeredDraw.POST_SLEEP_STACK, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + TickEvent.ClientTickEvent.Post.BUS.addListener(event -> ClientHooks.clientTickEnd()); + ViewportEvent.ComputeCameraAngles.BUS.addListener(event -> ClientHooks.computeCameraAngles(new Angles(event))); + ViewportEvent.ComputeFov.BUS.addListener(event -> event.setFOV(ClientHooks.fov(event.getFOV()))); + RenderHandEvent.BUS.addListener(ForgeClient::hideHand); + RenderLivingEvent.Pre.BUS.addListener(event -> { + ClientHooks.beforeRenderLiving(event.getState(), event.getPoseStack(), ClientHooks.cameraPartialTicks()); + return false; + }); + InputEvent.InteractionKeyMappingTriggered.BUS.addListener(ForgeClient::onInteractionKey); + ClientPlayerNetworkEvent.LoggingIn.BUS.addListener(event -> ClientHooks.loggedIn(event.getPlayer())); + ClientPlayerNetworkEvent.LoggingOut.BUS.addListener(event -> ClientHooks.loggedOut()); + } + + private static boolean hideHand(RenderHandEvent event) { + return ClientHooks.hidesHand(); + } + + private static boolean onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + return switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> false; + case CONSUME -> { + event.setSwingHand(false); + yield true; + } + case CONSUME_AND_SWING -> true; + }; + } + return event.isPickBlock() && ClientHooks.onPickBlockKey(); + } + + private record Angles(ViewportEvent.ComputeCameraAngles event) implements CameraAngles { + @Override + public Camera camera() { + return event.getCamera(); + } + + @Override + public float partialTick() { + return event.getPartialTick(); + } + + @Override + public float yaw() { + return event.getYaw(); + } + + @Override + public float pitch() { + return event.getPitch(); + } + + @Override + public float roll() { + return event.getRoll(); + } + + @Override + public void setYaw(float yaw) { + event.setYaw(yaw); + } + + @Override + public void setPitch(float pitch) { + event.setPitch(pitch); + } + + @Override + public void setRoll(float roll) { + event.setRoll(roll); + } + } +} From ae9163885f1d92a1b1babf4f9c5a5543f0f4bcfe Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:19 +0800 Subject: [PATCH 0373/1461] Add the Forge payload channel for 26.2 --- .../mcheli/forge/ForgeNetwork.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java diff --git a/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java new file mode 100644 index 00000000..54a48e84 --- /dev/null +++ b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraftforge.network.Channel; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.payload.PayloadFlow; + +/** + * Registers the shared packets on a Forge payload channel. Each packet travels under its own id, + * as on NeoForge and Fabric; the channel only adds Forge's version check. + */ +final class ForgeNetwork { + private static Channel channel; + + private ForgeNetwork() { + } + + static void register() { + PayloadFlow flow = ChannelBuilder + .named(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "main")) + .networkProtocolVersion(ModNetwork.PROTOCOL_VERSION) + .payloadChannel() + .play() + .serverbound(); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + flow = serverbound(flow, packet); + } + flow = flow.clientbound(); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + flow = clientbound(flow, packet); + } + channel = flow.build(); + } + + static Channel channel() { + return channel; + } + + private static PayloadFlow serverbound( + PayloadFlow flow, ModNetwork.Serverbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg, ctx.getSender())); + ctx.setPacketHandled(true); + }); + } + + private static PayloadFlow clientbound( + PayloadFlow flow, ModNetwork.Clientbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg)); + ctx.setPacketHandled(true); + }); + } +} From 2c057a8df2527a68b5d4b1755d5e79900eefd529 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:20 +0800 Subject: [PATCH 0374/1461] Add the Forge platform for 26.2 --- .../mcheli/forge/ForgePlatform.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java diff --git a/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java new file mode 100644 index 00000000..d82312f3 --- /dev/null +++ b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.network.PacketDistributor; + +import java.nio.file.Path; + +final class ForgePlatform implements Platform { + @Override + public String loaderName() { + return "forge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.SERVER.noArg()); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.PLAYER.with(player)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From a3404d96f7f30e9417a3427336dd93cde74b3304 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:20 +0800 Subject: [PATCH 0375/1461] Add the Forge vehicle ingredient for 26.2, which has no simple serializer --- .../mcheli/forge/ForgeVehicleIngredient.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java diff --git a/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java new file mode 100644 index 00000000..51a8d1a9 --- /dev/null +++ b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraftforge.common.crafting.ingredients.AbstractIngredient; +import net.minecraftforge.common.crafting.ingredients.IIngredientSerializer; +import org.jspecify.annotations.Nullable; + +/** {@link VehicleIngredient} as a Forge ingredient, read from the {@code type} key. */ +final class ForgeVehicleIngredient extends AbstractIngredient { + static final MapCodec CODEC = VehicleIngredient.codec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final StreamCodec STREAM_CODEC = + VehicleIngredient.streamCodec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final IIngredientSerializer SERIALIZER = new IIngredientSerializer<>() { + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + public void write(RegistryFriendlyByteBuf buffer, ForgeVehicleIngredient value) { + STREAM_CODEC.encode(buffer, value); + } + + @Override + public ForgeVehicleIngredient read(RegistryFriendlyByteBuf buffer) { + return STREAM_CODEC.decode(buffer); + } + }; + + private final String vehicle; + + private ForgeVehicleIngredient(String vehicle) { + super(VehicleIngredient.kindItems()); + this.vehicle = vehicle; + } + + String vehicle() { + return vehicle; + } + + @Override + public boolean test(@Nullable ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IIngredientSerializer serializer() { + return SERIALIZER; + } + + @Override + public boolean equals(Object o) { + return o instanceof ForgeVehicleIngredient other && vehicle.equals(other.vehicle); + } + + @Override + public int hashCode() { + return vehicle.hashCode(); + } +} From 735c9376853e6434814120dc42477d4e7fe31fe1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:21 +0800 Subject: [PATCH 0376/1461] Replace the Forge 26.2 placeholder entry point with the real one --- .../jnhprojects/mcheli/forge/McheliForge.java | 71 ++++++++++++++++--- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java index 0bb49087..4028f536 100644 --- a/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java +++ b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java @@ -1,19 +1,70 @@ package com.jnhprojects.mcheli.forge; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; import net.minecraftforge.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegisterEvent; -/** - * Placeholder entry point for Minecraft 26.2. It only proves that the module builds and loads; - * the Forge glue from forge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +/** The Forge entry point: everything here only connects Forge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliForge(FMLJavaModLoadingContext context) { + BusGroup modBus = context.getModBusGroup(); + Platform.install(new ForgePlatform()); + MCHeli.init(); - public McheliForge() { - LOGGER.info("MCHeli Rework placeholder loaded on Forge for Minecraft 26.2; no content yet"); + RegisterEvent.getBus(modBus).addListener(event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(ForgeRegistries.Keys.INGREDIENT_SERIALIZERS, + helper -> helper.register(VehicleIngredient.ID, ForgeVehicleIngredient.SERIALIZER)); + }); + ForgeNetwork.register(); + FMLCommonSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(CommonHooks::commonSetup)); + + AddPackFindersEvent.BUS.addListener(event -> event.addRepositorySource(CommonHooks.packSource())); + ServerAboutToStartEvent.BUS.addListener(event -> CommonHooks.serverAboutToStart(event.getServer())); + ServerStoppedEvent.BUS.addListener(event -> CommonHooks.serverStopped()); + TickEvent.ServerTickEvent.Post.BUS.addListener(event -> CommonHooks.serverTickEnd(event.server())); + RegisterCommandsEvent.BUS.addListener(event -> CommonHooks.registerCommands(event.getDispatcher())); + PlayerEvent.PlayerLoggedInEvent.BUS.addListener(event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + PlayerEvent.PlayerLoggedOutEvent.BUS.addListener(event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + ForgeClient.init(modBus); + } + } + + /** Only once Forge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } } } From 1e4469f9432619a7d8fe8aaab73feeaa4d5aff93 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:21 +0800 Subject: [PATCH 0377/1461] Open the post effect setter on Forge 26.2 for the thermal view --- forge-26.2/src/main/resources/META-INF/accesstransformer.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 forge-26.2/src/main/resources/META-INF/accesstransformer.cfg diff --git a/forge-26.2/src/main/resources/META-INF/accesstransformer.cfg b/forge-26.2/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 00000000..c9ffbad2 --- /dev/null +++ b/forge-26.2/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,2 @@ +# Vanilla keeps the post effect setter private on 26.2; the thermal view needs it. +public net.minecraft.client.renderer.GameRenderer setPostEffect(Lnet/minecraft/resources/Identifier;)V From e45a5ae7a2718c7f0b2a2a92b0bcc49f3816162b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:21 +0800 Subject: [PATCH 0378/1461] Drop the placeholder wording from the Forge 26.2 metadata --- forge-26.2/src/main/resources/META-INF/mods.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/forge-26.2/src/main/resources/META-INF/mods.toml b/forge-26.2/src/main/resources/META-INF/mods.toml index 47ec16fb..1fdc3b48 100644 --- a/forge-26.2/src/main/resources/META-INF/mods.toml +++ b/forge-26.2/src/main/resources/META-INF/mods.toml @@ -13,7 +13,6 @@ authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft Forge. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. From 60eba8c0db949e6de78d24e330e132be53c07802 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:22 +0800 Subject: [PATCH 0379/1461] Set up the Forge 26.2 runs and apply its access transformer --- forge-26.2/build.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/forge-26.2/build.gradle b/forge-26.2/build.gradle index 372dd1f6..fe4e68cd 100644 --- a/forge-26.2/build.gradle +++ b/forge-26.2/build.gradle @@ -7,10 +7,18 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + minecraft { + useDefaultAccessTransformer() + runs { configureEach { workingDir = rootProject.layout.projectDirectory.dir("run-${minecraft_version}") + systemProperty 'eventbus.api.strictRuntimeChecks', 'true' + systemProperty 'mcheli.legacyZip', legacyZip } register('client') @@ -30,4 +38,5 @@ repositories { dependencies { implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}") + annotationProcessor "net.minecraftforge:eventbus-validator:${eventbus_version}" } From 1a9bfa1762a1dfa3cd3c85146a293b38ee0a82e0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:22 +0800 Subject: [PATCH 0380/1461] Stop treating Forge 26.2 as a placeholder --- forge-26.2/gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-26.2/gradle.properties b/forge-26.2/gradle.properties index c7d56828..2b0aa635 100644 --- a/forge-26.2/gradle.properties +++ b/forge-26.2/gradle.properties @@ -1,8 +1,8 @@ -# Minecraft 26.2 on Forge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 26.2 on Forge loader=forge minecraft_version=26.2 minecraft_version_range=[26.2,26.3) forge_version=65.1.5 loader_version_range=[65,) +eventbus_version=7.0.6 java_version=25 -scaffold=true From 4a516eeb6aa11c027d1d160347d3689270221581 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:23 +0800 Subject: [PATCH 0381/1461] Replace the NeoForge 26.2 placeholder entry point with the real one --- .../mcheli/neoforge/McheliNeoForge.java | 96 ++++++++++++++++--- 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java index 1f3a01b0..b5bd9c45 100644 --- a/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java +++ b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java @@ -1,19 +1,93 @@ package com.jnhprojects.mcheli.neoforge; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Placeholder entry point for Minecraft 26.2. It only proves that the module builds and loads; - * the NeoForge glue from neoforge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.AddPackFindersEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import net.neoforged.neoforge.registries.RegisterEvent; + +/** The NeoForge entry point: everything here only connects NeoForge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliNeoForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliNeoForge(IEventBus modBus) { + Platform.install(new NeoForgePlatform()); + MCHeli.init(); + + modBus.addListener(RegisterEvent.class, event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(NeoForgeRegistries.Keys.INGREDIENT_TYPES, + helper -> helper.register(VehicleIngredient.ID, NeoForgeVehicleIngredient.TYPE)); + }); + modBus.addListener(RegisterPayloadHandlersEvent.class, McheliNeoForge::registerPackets); + modBus.addListener(FMLCommonSetupEvent.class, event -> event.enqueueWork(CommonHooks::commonSetup)); + modBus.addListener(AddPackFindersEvent.class, event -> event.addRepositorySource(CommonHooks.packSource())); + + NeoForge.EVENT_BUS.addListener(ServerAboutToStartEvent.class, event -> CommonHooks.serverAboutToStart(event.getServer())); + NeoForge.EVENT_BUS.addListener(ServerStoppedEvent.class, event -> CommonHooks.serverStopped()); + NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> CommonHooks.serverTickEnd(event.getServer())); + NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> CommonHooks.registerCommands(event.getDispatcher())); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedInEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedOutEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + NeoForgeClient.init(modBus); + } + } + + /** Only once NeoForge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } + } + + /** Handlers run on the main thread, which is NeoForge's default. */ + private static void registerPackets(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(Integer.toString(ModNetwork.PROTOCOL_VERSION)); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(registrar, packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(registrar, packet); + } + } + + private static void serverbound(PayloadRegistrar registrar, ModNetwork.Serverbound packet) { + registrar.playToServer(packet.type(), packet.codec(), + (msg, context) -> packet.handler().accept(msg, (ServerPlayer) context.player())); + } - public McheliNeoForge() { - LOGGER.info("MCHeli Rework placeholder loaded on NeoForge for Minecraft 26.2; no content yet"); + private static void clientbound(PayloadRegistrar registrar, ModNetwork.Clientbound packet) { + registrar.playToClient(packet.type(), packet.codec(), (msg, context) -> packet.handler().accept(msg)); } } From 6e86f51ebe15f2878e690e798f01e5440d1e4513 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:23 +0800 Subject: [PATCH 0382/1461] Add the NeoForge client glue for 26.2 --- .../mcheli/neoforge/NeoForgeClient.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java diff --git a/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java new file mode 100644 index 00000000..9c8d7d61 --- /dev/null +++ b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.neoforge.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Minecraft; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; +import net.neoforged.neoforge.client.event.InputEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; +import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent; +import net.neoforged.neoforge.client.event.RenderHandEvent; +import net.neoforged.neoforge.client.event.RenderLivingEvent; +import net.neoforged.neoforge.client.event.ViewportEvent; +import net.neoforged.neoforge.client.gui.VanillaGuiLayers; +import net.neoforged.neoforge.common.NeoForge; + +/** + * Client events on NeoForge. Only loaded on a physical client. The camera hook is a mixin + * ({@code GameRendererMixin}): NeoForge's camera event fires before vanilla positions the camera, + * so a cockpit eye set from it would be overwritten. + */ +final class NeoForgeClient { + private NeoForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$setPostEffect(effect)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + modBus.addListener(RegisterMenuScreensEvent.class, event -> ClientHooks.registerMenuScreens(event::register)); + modBus.addListener(RegisterParticleProvidersEvent.class, event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + modBus.addListener(RegisterKeyMappingsEvent.class, event -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener(AddClientReloadListenersEvent.class, event -> ClientHooks.reloadListeners().forEach(event::addListener)); + modBus.addListener(RegisterGuiLayersEvent.class, + event -> event.registerAbove(VanillaGuiLayers.SLEEP_OVERLAY, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post.class, event -> ClientHooks.clientTickEnd()); + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeFov.class, event -> event.setFOV(ClientHooks.fov(event.getFOV()))); + NeoForge.EVENT_BUS.addListener(RenderHandEvent.class, event -> event.setCanceled(ClientHooks.hidesHand())); + NeoForge.EVENT_BUS.addListener(RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getRenderState(), event.getPoseStack(), event.getPartialTick())); + NeoForge.EVENT_BUS.addListener(InputEvent.InteractionKeyMappingTriggered.class, NeoForgeClient::onInteractionKey); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingIn.class, event -> ClientHooks.loggedIn(event.getPlayer())); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingOut.class, event -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } +} From 4ea04a34bd6b6cf15ddccbd4485d8a22a6bab1aa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:24 +0800 Subject: [PATCH 0383/1461] Add the NeoForge platform for 26.2 --- .../mcheli/neoforge/NeoForgePlatform.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java diff --git a/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java new file mode 100644 index 00000000..6cd0660a --- /dev/null +++ b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; +import net.neoforged.neoforge.network.PacketDistributor; + +import java.nio.file.Path; + +final class NeoForgePlatform implements Platform { + @Override + public String loaderName() { + return "neoforge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.getDist() == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPacketDistributor.sendToServer(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From 179599841651e6c0150cf699230224446ac34faa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:24 +0800 Subject: [PATCH 0384/1461] Add the NeoForge vehicle ingredient for 26.2 --- .../neoforge/NeoForgeVehicleIngredient.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java diff --git a/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java new file mode 100644 index 00000000..195be00a --- /dev/null +++ b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import net.minecraft.core.Holder; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.neoforged.neoforge.common.crafting.ICustomIngredient; +import net.neoforged.neoforge.common.crafting.IngredientType; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a NeoForge ingredient, read from the {@code neoforge:ingredient_type} key. */ +record NeoForgeVehicleIngredient(String vehicle) implements ICustomIngredient { + static final IngredientType TYPE = new IngredientType<>( + VehicleIngredient.codec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle), + VehicleIngredient.streamCodec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle)); + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> items() { + return VehicleIngredient.items(); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IngredientType getType() { + return TYPE; + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } +} From 3e2f458becb19fe44a2e997573f0dc94dcf943b3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:25 +0800 Subject: [PATCH 0385/1461] Add the NeoForge camera invoker for 26.2 --- .../mcheli/neoforge/mixin/CameraInvoker.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java diff --git a/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java new file mode 100644 index 00000000..4c289d07 --- /dev/null +++ b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java @@ -0,0 +1,12 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** NeoForge adds a roll to {@code Camera#setRotation}, but leaves it protected. */ +@Mixin(Camera.class) +public interface CameraInvoker { + @Invoker("setRotation") + void mcheli$setRotation(float yRot, float xRot, float roll); +} From 52b1a5fcd898851ce5fd251e69fb8987d65959d3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:25 +0800 Subject: [PATCH 0386/1461] Invoke the private post effect setter on NeoForge 26.2 --- .../mcheli/neoforge/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java diff --git a/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..43944898 --- /dev/null +++ b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#setPostEffect} private on 26.2. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("setPostEffect") + void mcheli$setPostEffect(Identifier effect); +} From 5ec83c72f8222c31a71174a28309b0b866a2d119 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:25 +0800 Subject: [PATCH 0387/1461] Add the NeoForge camera hook for 26.2, whose extractCamera takes two partial ticks --- .../neoforge/mixin/GameRendererMixin.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java diff --git a/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java new file mode 100644 index 00000000..cac7a0b2 --- /dev/null +++ b/neoforge-26.2/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java @@ -0,0 +1,39 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, at the two places Forge fires its camera angle event: once vanilla has placed + * the camera for the frame, and again before the camera is copied into the render state. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;update(Lnet/minecraft/client/DeltaTracker;)V", + shift = At.Shift.AFTER)) + private void mcheli$afterCameraUpdate(DeltaTracker deltaTracker, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, deltaTracker.getGameTimeDeltaPartialTick(true), GameRendererMixin::setRotation); + } + + @Inject(method = "extractCamera", at = @At("HEAD")) + private void mcheli$beforeExtractCamera(DeltaTracker deltaTracker, float worldPartialTicks, float cameraEntityPartialTicks, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, worldPartialTicks, GameRendererMixin::setRotation); + } + + /** NeoForge turns the camera the other way round its roll axis than Forge does. */ + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((CameraInvoker) camera).mcheli$setRotation(yaw, pitch, -roll); + } +} From dffec64ba5422a1eec1563d92c592a3d38edd792 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:26 +0800 Subject: [PATCH 0388/1461] Drop the placeholder wording from the NeoForge 26.2 metadata --- .../src/main/resources/META-INF/neoforge.mods.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/neoforge-26.2/src/main/resources/META-INF/neoforge.mods.toml b/neoforge-26.2/src/main/resources/META-INF/neoforge.mods.toml index 2585f0b9..c8095914 100644 --- a/neoforge-26.2/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge-26.2/src/main/resources/META-INF/neoforge.mods.toml @@ -8,17 +8,19 @@ modId="${mod_id}" version="${mod_version}" displayName="${mod_name}" displayURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge" -logoFile="mcheli_logo.png" +iconFile="mcheli_logo.png" authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. ''' +[[mixins]] +config="mcheli.mixins.json" + [[dependencies.${mod_id}]] modId="neoforge" type="required" From 6a457237f78d31e420902952905e23fc6547416b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:26 +0800 Subject: [PATCH 0389/1461] List the NeoForge 26.2 mixins --- .../src/main/resources/mcheli.mixins.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 neoforge-26.2/src/main/resources/mcheli.mixins.json diff --git a/neoforge-26.2/src/main/resources/mcheli.mixins.json b/neoforge-26.2/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..77522ff3 --- /dev/null +++ b/neoforge-26.2/src/main/resources/mcheli.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.neoforge.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "CameraInvoker", + "GameRendererInvoker", + "GameRendererMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From 4d3f43f114685a4d9c4085204449ad0479356dff Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:27 +0800 Subject: [PATCH 0390/1461] Set up the NeoForge 26.2 runs --- neoforge-26.2/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neoforge-26.2/build.gradle b/neoforge-26.2/build.gradle index 6dded114..60829c34 100644 --- a/neoforge-26.2/build.gradle +++ b/neoforge-26.2/build.gradle @@ -7,12 +7,17 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + neoForge { version = neoforge_version runs { configureEach { gameDirectory = rootProject.file("run-${minecraft_version}") + systemProperty 'mcheli.legacyZip', legacyZip } client { client() From 583d8f406ec7b1eb297dbb8c2dfdc8d7bebe9b74 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:27 +0800 Subject: [PATCH 0391/1461] Stop treating NeoForge 26.2 as a placeholder and use the jspecify it pins --- neoforge-26.2/gradle.properties | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neoforge-26.2/gradle.properties b/neoforge-26.2/gradle.properties index 2af5d6a3..fe795dc7 100644 --- a/neoforge-26.2/gradle.properties +++ b/neoforge-26.2/gradle.properties @@ -1,8 +1,9 @@ -# Minecraft 26.2 on NeoForge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 26.2 on NeoForge loader=neoforge minecraft_version=26.2 minecraft_version_range=[26.2,26.3) neoforge_version=26.2.0.93 loader_version_range=[26.2,) java_version=25 -scaffold=true +# NeoForge 26.2 pins this version strictly. +jspecify_version=1.0.0 From fed1c5a4ae8647cda971113601202cc859fce0d9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:28 +0800 Subject: [PATCH 0392/1461] Add the Fabric platform for 26.2 --- .../mcheli/fabric/FabricPlatform.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java new file mode 100644 index 00000000..0e59c8dd --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.creativetab.v1.FabricCreativeModeTab; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; + +import java.nio.file.Path; + +final class FabricPlatform implements Platform { + @Override + public String loaderName() { + return "fabric"; + } + + @Override + public Path gameDir() { + return FabricLoader.getInstance().getGameDir(); + } + + @Override + public Path configDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + public String modVersion() { + return FabricLoader.getInstance().getModContainer(MCHeli.MOD_ID) + .map(c -> c.getMetadata().getVersion().getFriendlyString()).orElse(""); + } + + @Override + public boolean isClient() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPlayNetworking.send(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ServerPlayNetworking.send(player, payload); + } + + /** Fabric orders tabs by registration, which already puts ours one after another. */ + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return FabricCreativeModeTab.builder(); + } +} From 033caf94377fbd9ac54af20efcea96faed36423f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:28 +0800 Subject: [PATCH 0393/1461] Add the Fabric vehicle ingredient for 26.2 --- .../fabric/FabricVehicleIngredient.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java new file mode 100644 index 00000000..ecc8b270 --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Holder; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a Fabric ingredient, read from the {@code fabric:type} key. */ +record FabricVehicleIngredient(String vehicle) implements CustomIngredient { + static final CustomIngredientSerializer SERIALIZER = new CustomIngredientSerializer<>() { + private final MapCodec codec = + VehicleIngredient.codec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + private final StreamCodec streamCodec = + VehicleIngredient.streamCodec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + + @Override + public Identifier getIdentifier() { + return VehicleIngredient.ID; + } + + @Override + public MapCodec getCodec() { + return codec; + } + + @Override + public StreamCodec getStreamCodec() { + return streamCodec; + } + }; + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> items() { + return VehicleIngredient.items(); + } + + @Override + public boolean requiresTesting() { + return true; + } + + @Override + public CustomIngredientSerializer getSerializer() { + return SERIALIZER; + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } +} From 1e186afa5ba9677998d9cc993284825046213008 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:28 +0800 Subject: [PATCH 0394/1461] Replace the Fabric 26.2 placeholder entry point with the real one --- .../mcheli/fabric/McheliFabric.java | 69 +++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java index 0a969049..d7503501 100644 --- a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java @@ -1,19 +1,74 @@ package com.jnhprojects.mcheli.fabric; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; import net.fabricmc.api.ModInitializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; /** - * Placeholder entry point for Minecraft 26.2. It only proves that the module builds and loads; - * the Fabric glue from fabric-26.3 replaces it when this version is ported for a final release. + * The Fabric entry point: everything here only connects Fabric to the shared code. The imported + * content pack is added by {@code PackRepositoryMixin}, since Fabric has no pack finder event. */ public final class McheliFabric implements ModInitializer { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); - @Override public void onInitialize() { - LOGGER.info("MCHeli Rework placeholder loaded on Fabric for Minecraft 26.2; no content yet"); + Platform.install(new FabricPlatform()); + MCHeli.init(); + + for (ResourceKey> registry : Registration.registries()) { + register(Registration.typed(registry)); + } + CustomIngredientSerializer.register(FabricVehicleIngredient.SERIALIZER); + registerPackets(); + CommonHooks.commonSetup(); + + ServerLifecycleEvents.SERVER_STARTING.register(CommonHooks::serverAboutToStart); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> CommonHooks.serverStopped()); + ServerTickEvents.END_SERVER_TICK.register(CommonHooks::serverTickEnd); + CommandRegistrationCallback.EVENT.register((dispatcher, context, selection) -> CommonHooks.registerCommands(dispatcher)); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> CommonHooks.playerLoggedIn(handler.getPlayer())); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> CommonHooks.playerLoggedOut(handler.getPlayer())); + } + + @SuppressWarnings("unchecked") + private static void register(ResourceKey> key) { + Registry registry = (Registry) BuiltInRegistries.REGISTRY.getValue(key.identifier()); + if (registry == null) { + throw new IllegalStateException("No registry " + key.identifier()); + } + Registration.registerAll(key, (id, value) -> Registry.register(registry, id, value)); + } + + /** Client handlers are registered by {@link McheliFabricClient}; handlers run on the main thread. */ + private static void registerPackets() { + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + } + + private static void clientbound(ModNetwork.Clientbound packet) { + PayloadTypeRegistry.clientboundPlay().register(packet.type(), packet.codec()); + } + + private static void serverbound(ModNetwork.Serverbound packet) { + PayloadTypeRegistry.serverboundPlay().register(packet.type(), packet.codec()); + ServerPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg, context.player())); } } From 96d65c82a7c4c93b4a178e1258158a5cc0a80e1c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:29 +0800 Subject: [PATCH 0395/1461] Add the Fabric client glue for 26.2 --- .../mcheli/fabric/McheliFabricClient.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java new file mode 100644 index 00000000..edaeb0b4 --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java @@ -0,0 +1,56 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.fabric.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.particle.v1.ParticleProviderRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.packs.PackType; + +/** + * Client side of the Fabric entry point. The camera, hand, key and rider hooks are mixins (see + * {@code com.jnhprojects.mcheli.fabric.mixin}), since Fabric API has no events for them. + */ +public final class McheliFabricClient implements ClientModInitializer { + + // EntityRendererRegistry is deprecated in favour of a vanilla method that is not public yet. + @SuppressWarnings("deprecation") + @Override + public void onInitializeClient() { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$setPostEffect(effect)); + ClientHooks.registerEntityRenderers(EntityRendererRegistry::register); + ClientHooks.registerBlockEntityRenderers(BlockEntityRenderers::register); + ClientHooks.registerMenuScreens(MenuScreens::register); + ParticleProviderRegistry.getInstance().register(ModParticles.GLOW.get(), GlowParticle.Provider::new); + ParticleProviderRegistry.getInstance().register(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + ClientHooks.keyMappings().forEach(KeyMappingHelper::registerKeyMapping); + ClientHooks.reloadListeners().forEach(ResourceLoader.get(PackType.CLIENT_RESOURCES)::registerReloadListener); + HudElementRegistry.attachElementAfter(VanillaHudElements.SLEEP, ClientHooks.HUD_LAYER, ClientHooks::drawHud); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + + ClientTickEvents.END_CLIENT_TICK.register(client -> ClientHooks.clientTickEnd()); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> ClientHooks.loggedIn(client.player)); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ClientHooks.loggedOut()); + } + + private static void clientbound(ModNetwork.Clientbound packet) { + ClientPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg)); + } +} From 24ceccdadcc8c76829053df579edbea1d5783ed3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:29 +0800 Subject: [PATCH 0396/1461] Add the rollable camera interface for Fabric 26.2 --- .../java/com/jnhprojects/mcheli/fabric/RollableCamera.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java new file mode 100644 index 00000000..ff30f0ff --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java @@ -0,0 +1,7 @@ +package com.jnhprojects.mcheli.fabric; + +/** Implemented on {@code Camera} by {@code CameraMixin}. Outside the mixin package, which Mixin refuses to load classes from. */ +public interface RollableCamera { + /** Like vanilla's {@code setRotation}, with a roll in degrees as Forge applies it. */ + void mcheli$setRotation(float yRot, float xRot, float roll); +} From ae6b2ebce5ddeae3385b366e79a5d15248b57aee Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:30 +0800 Subject: [PATCH 0397/1461] Add the Fabric camera roll mixin for 26.2 --- .../mcheli/fabric/mixin/CameraMixin.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java new file mode 100644 index 00000000..4c6bfafe --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Vanilla's camera has no roll. This is {@code Camera#setRotation} with the roll Forge patches in, + * so a banked aircraft tilts the view the same on every loader. + */ +@Mixin(Camera.class) +abstract class CameraMixin implements RollableCamera { + @Shadow + @Final + private static Vector3fc FORWARDS; + @Shadow + @Final + private static Vector3fc UP; + @Shadow + @Final + private static Vector3fc LEFT; + + @Shadow + private float xRot; + @Shadow + private float yRot; + @Shadow + @Final + private Vector3f forwards; + @Shadow + @Final + private Vector3f up; + @Shadow + @Final + private Vector3f left; + @Shadow + @Final + private Quaternionf rotation; + @Shadow + private int matrixPropertiesDirty; + + @Override + public void mcheli$setRotation(float yRot, float xRot, float roll) { + this.xRot = xRot; + this.yRot = yRot; + this.rotation.rotationYXZ((float) Math.PI - yRot * (float) (Math.PI / 180.0), -xRot * (float) (Math.PI / 180.0), + roll * (float) (Math.PI / 180.0)); + FORWARDS.rotate(this.rotation, this.forwards); + UP.rotate(this.rotation, this.up); + LEFT.rotate(this.rotation, this.left); + this.matrixPropertiesDirty |= 3; + } + + /** Zoom through a sight, where Forge fires its field of view event. */ + @Inject(method = "calculateFov", at = @At("RETURN"), cancellable = true) + private void mcheli$calculateFov(float partialTicks, CallbackInfoReturnable cir) { + cir.setReturnValue(ClientHooks.fov(cir.getReturnValueF())); + } +} From 21148e303fabbc7bb35b59e7b36c4faf90be7350 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:30 +0800 Subject: [PATCH 0398/1461] Invoke the private post effect setter on Fabric 26.2 --- .../mcheli/fabric/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..5db23311 --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#setPostEffect} private on 26.2. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("setPostEffect") + void mcheli$setPostEffect(Identifier effect); +} From 7aff4ec7f80a1c519458c038701430407f919ad9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:31 +0800 Subject: [PATCH 0399/1461] Add the Fabric camera and hand mixin for 26.2, whose extractCamera takes two partial ticks --- .../fabric/mixin/GameRendererMixin.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java new file mode 100644 index 00000000..b04559e3 --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, at the two places Forge fires its camera angle event (once vanilla has placed + * the camera for the frame, and again before the camera is copied into the render state), and + * the first person hand, which is hidden in some seats. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;update(Lnet/minecraft/client/DeltaTracker;)V", + shift = At.Shift.AFTER)) + private void mcheli$afterCameraUpdate(DeltaTracker deltaTracker, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, deltaTracker.getGameTimeDeltaPartialTick(true), GameRendererMixin::setRotation); + } + + @Inject(method = "extractCamera", at = @At("HEAD")) + private void mcheli$beforeExtractCamera(DeltaTracker deltaTracker, float worldPartialTicks, float cameraEntityPartialTicks, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, worldPartialTicks, GameRendererMixin::setRotation); + } + + @Inject(method = "renderItemInHand", at = @At("HEAD"), cancellable = true) + private void mcheli$renderItemInHand(CallbackInfo ci) { + if (ClientHooks.hidesHand()) { + ci.cancel(); + } + } + + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((RollableCamera) camera).mcheli$setRotation(yaw, pitch, roll); + } +} From ad63447ed17093beb5d719a541e472a53ed55afd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:31 +0800 Subject: [PATCH 0400/1461] Add the Fabric rider pose mixin for 26.2 --- .../mixin/LivingEntityRendererMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java new file mode 100644 index 00000000..524f5481 --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; +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.callback.CallbackInfo; + +/** Tilts riders with their vehicle, where Forge fires its living entity render event. */ +@Mixin(LivingEntityRenderer.class) +abstract class LivingEntityRendererMixin { + + @Inject(method = "submit(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;" + + "Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/level/CameraRenderState;)V", + at = @At("HEAD")) + private void mcheli$beforeSubmit(LivingEntityRenderState state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera, + CallbackInfo ci) { + ClientHooks.beforeRenderLiving(state, pose, ClientHooks.cameraPartialTicks()); + } +} From d3535a9a51a2d35a3a87167f4a3ac4ec5ea0f63b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:32 +0800 Subject: [PATCH 0401/1461] Add the Fabric use and pick block mixin for 26.2 --- .../mcheli/fabric/mixin/MinecraftMixin.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java new file mode 100644 index 00000000..b155db7c --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.HitResult; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** The use and pick block keys, which Forge and NeoForge let mods take through an input event. */ +@Mixin(Minecraft.class) +abstract class MinecraftMixin { + @Shadow + public @Nullable LocalPlayer player; + @Shadow + public @Nullable HitResult hitResult; + @Shadow + private int rightClickDelay; + + @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) + private void mcheli$startUseItem(CallbackInfo ci) { + if (player == null) { + return; + } + ClientHooks.UseKey result = ClientHooks.onUseKey(InteractionHand.MAIN_HAND); + if (result != ClientHooks.UseKey.PASS) { + // Vanilla sets the same delay before it looks at the hands. + rightClickDelay = 4; + if (result == ClientHooks.UseKey.CONSUME_AND_SWING) { + player.swing(InteractionHand.MAIN_HAND); + } + ci.cancel(); + } + } + + @Inject(method = "pickBlockOrEntity", at = @At("HEAD"), cancellable = true) + private void mcheli$pickBlockOrEntity(CallbackInfo ci) { + if (hitResult != null && hitResult.getType() != HitResult.Type.MISS && ClientHooks.onPickBlockKey()) { + ci.cancel(); + } + } +} From d3b0e3a22d35ad30fad2b7c7c089827efd6f1c12 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:32 +0800 Subject: [PATCH 0402/1461] Add the Fabric content pack mixin for 26.2 --- .../fabric/mixin/PackRepositoryMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java new file mode 100644 index 00000000..965719b9 --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.CommonHooks; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.Arrays; + +/** + * Adds the imported original content to every pack repository, client resources and server data + * alike, which is what Forge and NeoForge do through their pack finder event. + */ +@Mixin(PackRepository.class) +abstract class PackRepositoryMixin { + + @ModifyVariable(method = "", at = @At("HEAD"), argsOnly = true) + private static RepositorySource[] mcheli$addImportedContent(RepositorySource[] sources) { + RepositorySource[] withOurs = Arrays.copyOf(sources, sources.length + 1); + withOurs[sources.length] = CommonHooks.packSource(); + return withOurs; + } +} From 500b41c682b73d38bb2eaf8683027c80608705ea Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:33 +0800 Subject: [PATCH 0403/1461] Add the Fabric runtime sound mixin for 26.2 --- .../mixin/RuntimeSoundInstanceMixin.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java diff --git a/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java new file mode 100644 index 00000000..f085c684 --- /dev/null +++ b/fabric-26.2/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java @@ -0,0 +1,20 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.concurrent.CompletableFuture; + +/** Streams content pack sounds from disk through Fabric's sound instance hook. */ +@Mixin(RuntimeSoundInstance.class) +abstract class RuntimeSoundInstanceMixin implements FabricSoundInstance { + + @Override + public CompletableFuture getAudioStream(SoundBufferLibrary loader, Identifier id, boolean repeatInstantly) { + return ((RuntimeSoundInstance) (Object) this).openStream(loader, id, repeatInstantly); + } +} From f570df1d654f9652ec1875df0038cf1f92ac4aac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:33 +0800 Subject: [PATCH 0404/1461] Declare the real entry points and mixins in the Fabric 26.2 metadata --- fabric-26.2/src/main/resources/fabric.mod.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fabric-26.2/src/main/resources/fabric.mod.json b/fabric-26.2/src/main/resources/fabric.mod.json index 1b87b793..8346f4c2 100644 --- a/fabric-26.2/src/main/resources/fabric.mod.json +++ b/fabric-26.2/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "${mod_id}", "version": "${mod_version}", "name": "${mod_name}", - "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", + "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", "authors": ["JayMar921 (rework)", "EMB4 (original author)", "Fureniku (CurseForge maintainer)"], "contact": { "homepage": "https://github.com/JnH-Projects/MCHeli-Rework-Forge", @@ -14,8 +14,10 @@ "icon": "mcheli_logo.png", "environment": "*", "entrypoints": { - "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"] + "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"], + "client": ["com.jnhprojects.mcheli.fabric.McheliFabricClient"] }, + "mixins": ["mcheli.mixins.json"], "depends": { "fabricloader": "${loader_version_range}", "fabric-api": "*", From 47dcdc7c6e2ad97a8b3affe6846fb0687ba671ec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:33 +0800 Subject: [PATCH 0405/1461] List the Fabric 26.2 mixins --- .../src/main/resources/mcheli.mixins.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-26.2/src/main/resources/mcheli.mixins.json diff --git a/fabric-26.2/src/main/resources/mcheli.mixins.json b/fabric-26.2/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..3dcbaf14 --- /dev/null +++ b/fabric-26.2/src/main/resources/mcheli.mixins.json @@ -0,0 +1,20 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "PackRepositoryMixin" + ], + "client": [ + "CameraMixin", + "GameRendererInvoker", + "GameRendererMixin", + "LivingEntityRendererMixin", + "MinecraftMixin", + "RuntimeSoundInstanceMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From 7c394816b5dbe64de3f4f25421ab9bd482baf5d7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:34 +0800 Subject: [PATCH 0406/1461] Set up the Fabric 26.2 runs --- fabric-26.2/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fabric-26.2/build.gradle b/fabric-26.2/build.gradle index dcc4eded..c4cfd3fb 100644 --- a/fabric-26.2/build.gradle +++ b/fabric-26.2/build.gradle @@ -7,10 +7,15 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + loom { runs { configureEach { runDir = "../run-${minecraft_version}" + vmArg "-Dmcheli.legacyZip=${legacyZip}" } server { programArgs '--nogui' From 97b69941c78f728a4899ab66a5a74ff81a16f9ae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:34 +0800 Subject: [PATCH 0407/1461] Stop treating Fabric 26.2 as a placeholder --- fabric-26.2/gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fabric-26.2/gradle.properties b/fabric-26.2/gradle.properties index a321987f..a6eb67eb 100644 --- a/fabric-26.2/gradle.properties +++ b/fabric-26.2/gradle.properties @@ -1,4 +1,4 @@ -# Minecraft 26.2 on Fabric (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 26.2 on Fabric loader=fabric minecraft_version=26.2 minecraft_version_range=~26.2 @@ -6,4 +6,3 @@ fabric_loader_version=0.20.1 loader_version_range=>=0.20.0 fabric_api_version=0.161.0+26.2 java_version=25 -scaffold=true From 2015ae911cc29b5d1af5c922b51fc5bd048e245c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:35 +0800 Subject: [PATCH 0408/1461] Build the three 26.2 modules --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 827d0840..9b5034fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mod_license=MCHeli Rework Licence 1.0 (see LICENSE) # Platform modules built by this project, one per Minecraft version. Each has its own # gradle.properties with the Minecraft, loader and Java versions. -platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 +platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-26.2,neoforge-26.2,fabric-26.2,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 # Libraries the common module compiles against. Every supported Minecraft version ships them. slf4j_version=2.0.17 From c3fb397b03da61e0e093d255b5ac106a06934d30 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:35 +0800 Subject: [PATCH 0409/1461] Note the 26.2 port in the changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0aab9cb..73e8903c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,11 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). the vehicle screen, the drafting table, team manager, UAV stations and the workshop. On 1.21.1 the PVP button of the team manager switches the server's PVP setting until a restart, since that version has no game rule for it, and target markers ignore view bobbing. +- Minecraft 26.2 on Forge, NeoForge and Fabric, with everything 26.3 has. ### Changed -- Forge, NeoForge and Fabric on 26.3 and 1.21.1 are all built on every pull request again. +- Forge, NeoForge and Fabric on 26.3, 26.2 and 1.21.1 are all built on every pull request. - Clients and servers on different loaders cannot play together, which the roadmap used to promise. Each loader refuses the other at its own handshake, before the mod runs. A pack zip still works on every loader when copied by hand. From f488c3a02d371cd7498e418739803fa881ec0d00 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:36 +0800 Subject: [PATCH 0410/1461] List Minecraft 26.2 in the README --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f7ebf37a..ae64be5e 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ MCHeli Rework icon -Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3** and **1.21.1**, on **Forge**, -**NeoForge** and **Fabric**. +Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3**, **26.2** and **1.21.1**, on +**Forge**, **NeoForge** and **Fabric**. MCHeli Rework is a ground-up port of MCHeli 1.0.3, a mod that stopped at Minecraft 1.7.10. Every system is rewritten for the modern game, while the original vehicle definitions, models, @@ -84,16 +84,16 @@ Reserved original, the code is source available rather than open source. ## Requirements -| | Minecraft 26.3 | Minecraft 1.21.1 | -|---|---|---| -| Forge | 66.1.0 or newer in the 66.x line | 52.1.0 or newer | -| NeoForge | 26.3.0.99 or newer | 21.1.200 or newer | -| Fabric | Loader 0.20 and Fabric API | Loader 0.16 and Fabric API | -| Java | 25 (bundled with the launcher) | 21 (bundled with the launcher) | +| | Minecraft 26.3 | Minecraft 26.2 | Minecraft 1.21.1 | +|---|---|---|---| +| Forge | 66.1.0 or newer in the 66.x line | 65.1.5 or newer in the 65.x line | 52.1.0 or newer | +| NeoForge | 26.3.0.99 or newer | 26.2.0.93 or newer | 21.1.200 or newer | +| Fabric | Loader 0.20 and Fabric API | Loader 0.20 and Fabric API | Loader 0.16 and Fabric API | +| Java | 25 (bundled with the launcher) | 25 (bundled with the launcher) | 21 (bundled with the launcher) | ## Installing -1. Install Forge, NeoForge or Fabric for Minecraft 26.3 or 1.21.1. On Fabric, also install +1. Install Forge, NeoForge or Fabric for Minecraft 26.3, 26.2 or 1.21.1. On Fabric, also install [Fabric API](https://modrinth.com/mod/fabric-api). 2. Download the jar for your loader from the [releases](https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases): @@ -118,7 +118,8 @@ zip goes. Use a separate game directory for each loader and version. Mods built for other loaders or older versions in a shared `mods` folder will stop the game from starting. Worlds and the `mcheli` folder can move between loaders on the same Minecraft version: the imported content, content packs -and configs are the same files on all three. Do not open a 26.3 world in 1.21.1. +and configs are the same files on all three. Do not open a world in an older Minecraft version than +the one that last saved it. ## Playing From 01cb248a32f853276cf4b6907d5e4e5a7d545a2c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:36 +0800 Subject: [PATCH 0411/1461] Tick the 26.2 port on the roadmap --- docs/ROADMAP.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e3f7c6f3..8fb5c6fb 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -309,8 +309,9 @@ added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest - [x] Every resource the repository holds ships in the jar (betas 4 to 8 lacked recipes, the thermal shader, the smoke particle and the drafting table model) - [x] Phases 8 to 12 ported to all three loaders on 1.21.1 -- [ ] Every module in `platforms` (26.3 and 1.21.1 are in) -- [ ] `minecraft-26.2`, `minecraft-26.1`, `minecraft-1.21.11`, `minecraft-1.21.5`, +- [ ] Every module in `platforms` (26.3, 26.2 and 1.21.1 are in) +- [x] `minecraft-26.2` with its Forge, NeoForge and Fabric glue +- [ ] `minecraft-26.1`, `minecraft-1.21.11`, `minecraft-1.21.5`, `minecraft-1.20.6`, `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its placeholder modules with real glue - [ ] `common` compiles for Java 17 (no pattern matching `switch`, no `SequencedCollection` From a3777b0aa1cd74092af1e004adf880b61d671b86 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:37 +0800 Subject: [PATCH 0412/1461] Describe what differs on 26.2 --- docs/ARCHITECTURE.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fa3860ee..51c63b8c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -51,7 +51,7 @@ loader. The top layer is a thin module per loader and Minecraft version, which c lower layers together with its own glue into one jar. See [DEVELOPMENT.md](DEVELOPMENT.md#three-layers). -Only 26.3 and 1.21.1 have the middle layer so far. The other supported versions have placeholder +Only 26.3, 26.2 and 1.21.1 have the middle layer so far. The other supported versions have placeholder loader modules (`scaffold=true`) that compile neither lower layer: just an entry point that logs, against that version's loader and toolchain. Porting a version means adding its `minecraft-` folder, copying the loader glue in and dropping the flag. @@ -82,6 +82,27 @@ fires its event, so the shared code sees the same thing on every loader: NeoForge rolls the camera the other way round from Forge, so its mixin negates the roll. +### Minecraft 26.2 + +`minecraft-26.2` is `minecraft-26.3` with these differences: + +- **Rendering API.** Pipelines live in `com.mojang.blaze3d` rather than `com.mojang.renderpearl`, + there are no order independent transparency pipelines to name, and `PoseStack` turns through + `mulPose`. `Glow` spells out vanilla's eyes pipeline, which has no snippet to build on yet, and + `ProjectileRenderer` makes its own translucent culled render type, since vanilla only offers one + drawn into the item target. +- **Interpolation.** `Entity` does not own or run a handler. Each of our entities holds an + `InterpolationHandler`, returns it from `getInterpolation` and calls `interpolate` after + `super.tick()`, the way vanilla's boat does. +- **Thermal view.** There is one post effect slot on `GameRenderer` rather than a list on the player, + and its setter is private, so each loader hands `ClientHooks.init` a way to call it: an access + transformer on Forge, a mixin invoker on NeoForge and Fabric. The shader has no explicit attribute + locations, matching vanilla's on this version. +- **Input on Forge.** `InteractionKeyMappingTriggered` is one event with `isUseItem` and `isPickBlock`, + not an event per key. +- **Rider pose.** `CameraRenderState` does not carry the camera entity's partial tick, so Forge and + Fabric ask `ClientHooks.cameraPartialTicks`. NeoForge's event has it. + ### Minecraft 1.21.1 `minecraft-1.21.1` is a copy of `minecraft-26.3` ported back, so the two read side by side. During From ab13f72680796a9687f0cdb9a62d292c710da05f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:37 +0800 Subject: [PATCH 0413/1461] List the 26.2 modules and say what a port must be checked with --- docs/DEVELOPMENT.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a655e7ec..481cf89b 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -12,6 +12,9 @@ | Forge | 66.1.0 for Minecraft 26.3 | | | NeoForge | 26.3.0.99 for Minecraft 26.3 | | | Fabric | Loader 0.20.1, Fabric API 0.165.0+26.3 | | +| Forge | 65.1.5 for Minecraft 26.2 | | +| NeoForge | 26.2.0.93 for Minecraft 26.2 | | +| Fabric | Loader 0.20.1, Fabric API 0.161.0+26.2 | | | Forge | 52.1.20 for Minecraft 1.21.1 | Built with ForgeGradle 7 and the official mappings. | | NeoForge | 21.1.265 for Minecraft 1.21.1 | | | Fabric | Fabric API 0.116.17+1.21.1 | Loom remaps the jar, which is obfuscated on 1.21.1. | @@ -41,7 +44,7 @@ Shared properties live in the root `gradle.properties`: `mod_version`, the mod m of loader modules (`platforms`) and the library versions `common` compiles against. Each loader module has its own `gradle.properties` with its loader, Minecraft and Java versions. During the betas up to beta.8, `platforms` listed only `forge-26.3`. Roadmap phase 13 adds modules back as each -one catches up, currently the three on 26.3 and the three on 1.21.1; the others stay in the repository +one catches up, currently the three each on 26.3, 26.2 and 1.21.1; the others stay in the repository until they are ported. `mods.toml`, `neoforge.mods.toml`, `fabric.mod.json` and `pack.mcmeta` are filled in from them at build time. What every loader module shares (source folders, jar contents, manifest, file @@ -60,6 +63,8 @@ minecraft-26.3/ code shared by every loader on Minecraft 26.3 (no forge-26.3/ Forge 66 glue: entry point, events, payload channel, ingredient neoforge-26.3/ NeoForge glue, plus a camera mixin fabric-26.3/ Fabric glue, plus mixins for what Fabric API has no event for +minecraft-26.2/ the same shared code on Minecraft 26.2 +forge-26.2/, neoforge-26.2/, fabric-26.2/ the loader glue for 26.2 minecraft-1.21.1/ the same shared code, backported to Minecraft 1.21.1 forge-1.21.1/, neoforge-1.21.1/, fabric-1.21.1/ the loader glue for 1.21.1 -/ placeholders for the other supported versions (see below) @@ -94,8 +99,9 @@ so each loader and version ships as one jar and runs as one module in developmen A feature that is pure logic goes in `common` with a unit test; the part that touches Minecraft goes in `minecraft-`; only what needs a loader API goes in the loader modules. The run -folder `./run` is shared by every 26.3 module and `./run-1.21.1` by every 1.21.1 module; worlds made by -one loader open on the others, but never open a 26.3 world on 1.21.1. +folder `./run` is shared by every 26.3 module, and every other version has its own, such as +`./run-26.2` and `./run-1.21.1`; worlds made by one loader open on the others, but never open a world +on an older Minecraft version than the one that last saved it. IDEs import the project as one Gradle project per module. A file under `minecraft-26.3` belongs to all three loader modules, so the IDE shows it under whichever it picked; it compiles under all @@ -104,8 +110,8 @@ of them on the command line. After pulling the split, re-import the Gradle proje ### Placeholder modules -Every [supported version](ROADMAP.md#supported-versions) has a loader module, but only 26.3 and -1.21.1 are ported. The other 19 (`forge-`, `neoforge-` and `fabric-` for 26.2, 26.1, 1.21.11, +Every [supported version](ROADMAP.md#supported-versions) has a loader module, but only 26.3, 26.2 +and 1.21.1 are ported. The other 16 (`forge-`, `neoforge-` and `fabric-` for 26.1, 1.21.11, 1.21.5 and 1.20.6, plus `forge-` and `fabric-` for 1.20.2 and 1.19.4) are placeholders: each pins its loader, mappings and Java version, and holds only an entry point that logs a line at startup and the mod metadata. Their `gradle.properties` sets `scaffold=true`, which tells @@ -124,6 +130,12 @@ Porting a version for a final release: add `minecraft-` (copied from th version), copy the loader glue from the ported module of the same loader, remove `scaffold=true` and add the module to `platforms`. +A clean compile proves little about a port. Mixin targets, the Forge access transformer, shaders and +data files only fail at runtime, so start a client and a dedicated server on every loader of that +version, and check a built jar. A loader can also pin a library more strictly than the version the +root `gradle.properties` asks for; override it in that module's `gradle.properties`, as NeoForge 26.2 +does with `jspecify_version`. + There is no `neoforge-1.20.2`: ModDevGradle needs a "moddev bundle" that NeoForge 20.2 does not publish, so NeoForge support starts at 1.20.6, the next supported version. From 4e3dcc88abd6686d1ff4533c6311f22cfc18e74c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 22:42:37 +0800 Subject: [PATCH 0414/1461] Add 26.2 checks for built jars, rider pose and Iris --- docs/TESTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/TESTING.md b/docs/TESTING.md index bea6cc61..daa84264 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -48,6 +48,14 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi another loader is refused by the loaders themselves, with a message naming the one it needs. - [ ] A `serverconfig/mcheli-server.toml` written by 1.1.0-beta.2 (Forge) keeps its values. +### 26.2 only + +- [ ] From the built jars on a launcher profile, not a dev run: the thermal view turns on and off on + Forge (its setter is opened by an access transformer, which a dev run applies differently), + NeoForge and Fabric. +- [ ] Rider pose and the camera roll look right in a banked aircraft on all three loaders. +- [ ] With Sodium and Iris for 26.2, tracers, flares and smoke look right (or note how they look). + ### 1.21.1 only - [ ] Vehicle icons show in the creative tabs and the hand (the kind model picks them by From 4df37e6b2d2630d29348a2d4e1474d05c2a82dd2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:12 +0800 Subject: [PATCH 0415/1461] Add ClientConfig.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/ClientConfig.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ClientConfig.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ClientConfig.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ClientConfig.java new file mode 100644 index 00000000..ff258847 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ClientConfig.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Per player look and feel, in {@code config/mcheli-client.toml}. Nothing here changes how the + * game plays, so each player picks for themselves and the server never sees it. + */ +public final class ClientConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue NIGHT_GLOW; + private static final ConfigSpec.IntValue PACK_CACHE_SIZE; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("effects"); + NIGHT_GLOW = builder + .comment("Whether tracers, rocket motors, flares and explosions glow. The glow is faint by day", + "and strongest at night, in caves and under water.") + .define("nightGlow", true); + builder.pop(); + + builder.push("packs"); + PACK_CACHE_SIZE = builder + .comment("How much the content packs downloaded from servers may take up in mcheli/cache, in megabytes.", + "The packs used longest ago are deleted first; the current server's packs are always kept.", + "A single pack larger than this is not downloaded.") + .defineInRange("cacheSize", 256, 16, 16384); + builder.pop(); + + SPEC = builder.build(); + } + + private ClientConfig() { + } + + /** Reads {@code config/mcheli-client.toml}, writing it with the defaults if it is new. */ + public static void load(Path configDir) { + Path file = configDir.resolve(MCHeli.MOD_ID + "-client.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + public static boolean nightGlow() { + return SPEC.isLoaded() ? NIGHT_GLOW.get() : NIGHT_GLOW.getDefault(); + } + + public static long packCacheBytes() { + return (long) (SPEC.isLoaded() ? PACK_CACHE_SIZE.get() : PACK_CACHE_SIZE.getDefault()) << 20; + } +} From 013740c5fb891fa078f48a31cd0628976f573eab Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:13 +0800 Subject: [PATCH 0416/1461] Add CommonHooks.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/CommonHooks.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java new file mode 100644 index 00000000..2cf2f6a8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java @@ -0,0 +1,83 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.network.PackSync; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.repository.RepositorySource; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.level.storage.LevelResource; + +import java.util.List; + +/** + * What the mod does on game events that both sides see. Each loader module calls these from its + * own events; nothing here knows which loader that is. + */ +public final class CommonHooks { + private CommonHooks() { + } + + /** Once the registries are frozen, on the main thread: what vanilla keeps in its own tables. */ + public static void commonSetup() { + DispenserBlock.registerBehavior(ModItems.THROWABLE.get(), new ThrowableItem.DispenseBehavior()); + } + + /** + * Adds the imported original content to the resource packs (client) and data packs (server). + * Without the original zip there is nothing to add. + */ + public static RepositorySource packSource() { + return onLoad -> { + // Fabric builds some pack repositories before our initializer has run. + LegacyContent content = MCHeli.legacyContent(); + if (content != null) { + content.pack().ifPresent(onLoad); + } + }; + } + + /** Before the worlds load: read the world's server config, then load the content packs it allows. */ + public static void serverAboutToStart(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + MCHeli.content().replaceWith(MCHeli.packs().load(true)); + } + + public static void serverStopped() { + ServerConfig.unload(); + } + + public static void serverTickEnd(MinecraftServer server) { + PackSync.pump(server); + } + + public static void registerCommands(CommandDispatcher dispatcher) { + McheliCommand.register(dispatcher); + } + + /** + * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack. Every + * MCHeli recipe goes into a player's recipe book when they join: the drafting table lists them + * from there, and they can be found in the crafting table's book too. + */ + public static void playerLoggedIn(ServerPlayer player) { + PackSync.onLogin(player); + // A remote pilot whose drone did not come back with them is shown and made mortal again. + if (!(player.getVehicle() instanceof AircraftEntity uav && uav.isRemotePilot(player))) { + AircraftEntity.clearRemotePilot(player); + } + List> ours = player.level().getServer().getRecipeManager().getRecipes().stream() + .filter(holder -> holder.id().identifier().getNamespace().equals(MCHeli.MOD_ID)) + .toList(); + player.awardRecipes(ours); + } + + public static void playerLoggedOut(ServerPlayer player) { + PackSync.onLogout(player); + } +} From 6277fa731c463217be5f633061d17292ab488d58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:13 +0800 Subject: [PATCH 0417/1461] Add ContentPacks.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/ContentPacks.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java new file mode 100644 index 00000000..d3bfdee5 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackFolder; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * The content packs in {@code /mcheli/packs}, loaded together with the imported 1.0.3 + * content, which always comes first. + * + *

At startup every pack in the folder is loaded, so the vehicles exist before any world does. + * When a server starts (single player included) and on {@code /mcheli reload}, the folder is + * scanned again and the server config applied: the allow list, the size limit per pack and the + * limit for all packs together. The packs that pass are what players get sent. + * + *

A zip pack is read in place, so packs stay open while their definitions and assets are in + * use. The set a reload replaces is closed on the reload after that, so a client in the same game + * that is still drawing from it never reads from a closed zip. + */ +public final class ContentPacks { + private static final int PROBLEMS_IN_CHAT = 8; + private static final String SOUNDS = "assets/mcheli/sounds/"; + + private final Path folder; + private final PendingPacks pending; + private final LegacyContent legacy; + private volatile List active = List.of(); + private volatile List problems = List.of(); + private volatile Set sounds = Set.of(); + private List retired = List.of(); + + ContentPacks(Path gameDir, LegacyContent legacy) { + this.folder = gameDir.resolve(MCHeli.MOD_ID).resolve("packs"); + this.pending = new PendingPacks(gameDir.resolve(MCHeli.MOD_ID).resolve("pending")); + this.legacy = legacy; + } + + /** {@code /mcheli/packs}. */ + public Path folder() { + return folder; + } + + /** The packs whose definitions are loaded, in load order. The import is not among them. */ + public List active() { + return active; + } + + /** Everything that went wrong in the last load, packs left out included. */ + public List problems() { + return problems; + } + + /** + * Scans the folder and loads the import and the packs. Never throws; whatever goes wrong is + * logged and kept in {@link #problems()}. + * + * @param serverRules whether to apply the server config; off at startup, when no world exists + * @return the new content, for the caller to swap in + */ + public synchronized ContentRegistry load(boolean serverRules) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not create {}", folder, e); + } + + PackFolder.Scan scan = PackFolder.scan(folder); + List found = new ArrayList<>(scan.problems()); + List accepted = new ArrayList<>(); + long total = 0; + for (ContentPack pack : scan.packs()) { + long size; + String refusal; + try { + size = pack.archiveSize(); + refusal = serverRules ? refusal(pack, size, total) : null; + } catch (IOException e) { + size = 0; + refusal = "left out, could not be read: " + e; + } + if (refusal == null) { + accepted.add(pack); + total += size; + } else { + found.add(new PackProblem(pack.id(), refusal)); + close(List.of(pack)); + } + } + for (PackProblem problem : found) { + MCHeli.LOGGER.warn("Content pack {}", problem); + } + + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, legacy.assets())); + for (ContentPack pack : accepted) { + sources.add(pack.contentSource()); + } + ContentRegistry registry = ContentRegistry.load(sources); + found.addAll(registry.problems()); + + close(retired); + PendingPacks.cleanReplaced(folder); + retired = active; + active = List.copyOf(accepted); + problems = List.copyOf(found); + sounds = soundsOf(accepted); + MCHeli.LOGGER.info("Content packs in use: {}", active.isEmpty() ? "none" : active); + return registry; + } + + /** Sound names as {@code RuntimeAssets.soundFile} finds them: {@code heli.ogg} and {@code heli1.ogg} are both {@code heli}. */ + private static Set soundsOf(List packs) { + Set names = new HashSet<>(); + for (ContentPack pack : packs) { + for (String file : pack.files()) { + if (file.startsWith(SOUNDS) && file.endsWith(".ogg")) { + String name = file.substring(SOUNDS.length(), file.length() - ".ogg".length()); + names.add(name); + if (name.endsWith("1")) { + names.add(name.substring(0, name.length() - 1)); + } + } + } + } + return Set.copyOf(names); + } + + /** Whether a pack in use brought this sound, so that the sound manager has not heard of it. */ + public boolean hasSound(String name) { + return sounds.contains(name); + } + + /** What players have sent this server, waiting for an operator. */ + public PendingPacks pending() { + return pending; + } + + private static @Nullable String refusal(ContentPack pack, long size, long total) { + if (!ServerConfig.isPackAllowed(pack.id())) { + return "left out, not on the server's allow list (packs.allowList in mcheli-server.toml)"; + } + if (size > ServerConfig.maxPackBytes()) { + return "left out, %.1f MB is more than the server allows for one pack (packs.maxPackSize, %d MB)" + .formatted(size / 1048576.0, ServerConfig.maxPackBytes() >> 20); + } + if (total + size > ServerConfig.maxTotalPackBytes()) { + return "left out, it would take the packs past the server's total limit (packs.maxTotalSize, %d MB)" + .formatted(ServerConfig.maxTotalPackBytes() >> 20); + } + return null; + } + + /** Sends the problems of the last load as chat lines, the first few of them. */ + public void reportProblems(Consumer out) { + List list = problems; + for (int i = 0; i < Math.min(list.size(), PROBLEMS_IN_CHAT); i++) { + out.accept(Component.translatable("message.mcheli.pack.problem", list.get(i).pack(), list.get(i).message())); + } + if (list.size() > PROBLEMS_IN_CHAT) { + out.accept(Component.translatable("message.mcheli.pack.more_problems", list.size() - PROBLEMS_IN_CHAT)); + } + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } +} From 33956f80c385c1734aea8c259878b030ffbb6852 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:13 +0800 Subject: [PATCH 0418/1461] Add ContentReload.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/ContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ContentReload.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ContentReload.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ContentReload.java new file mode 100644 index 00000000..7846b7bf --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.network.PackSync; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +/** + * Reads the vehicle and weapon definitions from disk again while the game runs, for + * {@code /mcheli reload}: the server config, then the imported content and the content packs. + * Vehicles in the world pick up their new definition on their next tick (see + * {@code AircraftEntity#definition}). Every player gets the new pack list, downloads what changed + * and rebuilds their content the same way. + */ +public final class ContentReload { + private ContentReload() { + } + + /** @return the newly loaded content, already in use */ + public static ContentRegistry reload(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + ContentRegistry fresh = MCHeli.packs().load(true); + MCHeli.content().replaceWith(fresh); + MCHeli.LOGGER.info("Content reloaded: {} vehicles, {} weapons", fresh.all().size(), fresh.weapons().size()); + PackSync.sendListToAll(server); + return fresh; + } +} From fde9487bc85d8b5e83e3b057fc3145134275898c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:14 +0800 Subject: [PATCH 0419/1461] Add LegacyContent.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/LegacyContent.java | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/LegacyContent.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/LegacyContent.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/LegacyContent.java new file mode 100644 index 00000000..495bb502 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/LegacyContent.java @@ -0,0 +1,239 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ImportTarget; +import com.jnhprojects.mcheli.content.LegacyImporter; +import net.minecraft.network.chat.Component; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PathPackResources; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackCompatibility; +import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.world.flag.FeatureFlagSet; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Stream; + +/** + * The original vehicles, models, textures and sounds, imported from the player's own copy of + * MCHeli 1.0.3. + * + *

That content is EMB4's and is not redistributed with this mod. Players download the original + * zip from the MCHeli CurseForge page and put it in {@code /mcheli}. On start the zip is + * converted once by {@link LegacyImporter} into {@code mcheli/imported}, which then serves as a + * required resource pack and data pack. The zip is converted again only when it changes or the + * importer's output format does; once imported it may be deleted. + * + *

Development runs point the {@code mcheli.legacyZip} system property at the zip instead. + */ +public final class LegacyContent { + /** The file name the original download has. Any zip with the same layout works. */ + public static final String ZIP_NAME = "MC1.7.10_mcheli_1.0.3.zip"; + + private static final String PACK_ID = "mcheli_content"; + private static final String MARKER = "import.properties"; + /** The pack layout this Minecraft version reads. */ + private static final ImportTarget TARGET = ImportTarget.MODERN; + + private final Path folder; + private final Path packRoot; + private @Nullable Component problem; + + private LegacyContent(Path folder) { + this.folder = folder; + this.packRoot = folder.resolve("imported"); + } + + /** Imports the zip if needed. Never throws; a missing or broken zip leaves the mod without content. */ + static LegacyContent prepare(Path gameDir) { + LegacyContent content = new LegacyContent(gameDir.resolve(MCHeli.MOD_ID)); + try { + content.importIfNeeded(); + } catch (IOException | UncheckedIOException e) { + MCHeli.LOGGER.error("Could not import the original MCHeli content", e); + content.problem = Component.translatable("message.mcheli.content.import_failed", e.getMessage()); + } + return content; + } + + /** The imported {@code assets/mcheli} folder. It does not exist when nothing was imported. */ + public Path assets() { + return packRoot.resolve("assets").resolve(MCHeli.MOD_ID); + } + + /** Why the vehicles are missing, for the player; empty when the content is in place. */ + public Optional problem() { + return Optional.ofNullable(problem); + } + + /** + * The imported folder as a pack, for both the client resource packs and the server data packs. + * Empty when nothing was imported. + */ + public Optional pack() { + if (!Files.isDirectory(packRoot)) { + return Optional.empty(); + } + PackLocationInfo location = new PackLocationInfo(PACK_ID, + Component.translatable("pack.mcheli.content"), PackSource.BUILT_IN, Optional.empty()); + Pack.Metadata metadata = new Pack.Metadata(Component.translatable("pack.mcheli.content.description"), + PackCompatibility.COMPATIBLE, FeatureFlagSet.of(), List.of()); + // Required, so it is enabled in every world and cannot be switched off. It sits at the + // bottom so that regular resource packs can still replace its textures and models. + return Optional.of(new Pack(location, new PathPackResources.PathResourcesSupplier(packRoot), metadata, + new PackSelectionConfig(true, Pack.Position.BOTTOM, false))); + } + + private void importIfNeeded() throws IOException { + Path zip = findZip(); + if (zip == null) { + if (Files.isDirectory(packRoot)) { + MCHeli.LOGGER.info("Using the MCHeli content imported earlier into {}", packRoot); + } else { + MCHeli.LOGGER.error("MCHeli content not found. Download {} from the MCHeli CurseForge page and put it in {}", + ZIP_NAME, folder.toAbsolutePath()); + problem = Component.translatable("message.mcheli.content.missing", ZIP_NAME, folder.toAbsolutePath().toString()); + } + return; + } + + Properties source = describe(zip); + if (source.equals(readMarker())) { + return; + } + + MCHeli.LOGGER.info("Importing MCHeli content from {}", zip); + Path staging = folder.resolve("imported.tmp"); + deleteTree(staging); + LegacyImporter.Summary summary = LegacyImporter.run(zip, staging, TARGET); + try (OutputStream out = Files.newOutputStream(staging.resolve(MARKER))) { + source.store(out, "Written by MCHeli Rework. Delete this folder to import again."); + } + swapIn(staging); + + MCHeli.LOGGER.info("Imported {} files, {} sounds, {} item models and {} recipes into {}", + summary.files(), summary.sounds(), summary.itemModels(), summary.recipes(), packRoot); + for (String skipped : summary.skippedRecipes()) { + MCHeli.LOGGER.info("Recipe left out: {}", skipped); + } + } + + /** + * Replaces the previous import with the fresh one in {@code staging}. + * + *

On Windows a folder that was just deleted keeps its name for a moment while another + * program (a file watcher, a virus scanner) still has a handle in it, and renaming onto that + * name fails with "access denied". So the old import is moved out of the way rather than + * deleted, the renames are retried for a few seconds, and the old copy is deleted last, on a + * best effort basis; a copy left behind is removed on the next import. + */ + private void swapIn(Path staging) throws IOException { + Path old = folder.resolve("imported.old"); + deleteQuietly(old); + if (Files.exists(packRoot)) { + try { + moveWithRetry(packRoot, old); + } catch (IOException e) { + deleteTree(packRoot); + } + } + moveWithRetry(staging, packRoot); + deleteQuietly(old); + } + + private static void moveWithRetry(Path from, Path to) throws IOException { + for (int attempt = 1; ; attempt++) { + try { + Files.move(from, to); + return; + } catch (FileSystemException e) { + if (attempt >= 15) { + throw e; + } + try { + Thread.sleep(50L * attempt); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw e; + } + } + } + } + + private static void deleteQuietly(Path root) { + try { + deleteTree(root); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not delete {}; it is removed on the next import", root, e); + } + } + + private @Nullable Path findZip() throws IOException { + String configured = System.getProperty("mcheli.legacyZip"); + if (configured != null && !configured.isBlank()) { + Path zip = Path.of(configured); + if (Files.isRegularFile(zip)) { + return zip; + } + MCHeli.LOGGER.warn("mcheli.legacyZip points to {}, which does not exist", zip); + } + + Files.createDirectories(folder); + if (Files.isRegularFile(folder.resolve(ZIP_NAME))) { + return folder.resolve(ZIP_NAME); + } + // Browsers rename repeated downloads, so take any zip, the same way every start. + try (Stream files = Files.list(folder)) { + return files.filter(p -> p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) + .filter(Files::isRegularFile) + .sorted() + .findFirst() + .orElse(null); + } + } + + /** What the current import was made from. Comparing size and time is enough to notice a new zip. */ + private static Properties describe(Path zip) throws IOException { + Properties p = new Properties(); + p.setProperty("format", Integer.toString(LegacyImporter.FORMAT)); + p.setProperty("target", TARGET.name()); + p.setProperty("source", zip.toAbsolutePath().normalize().toString()); + p.setProperty("size", Long.toString(Files.size(zip))); + p.setProperty("modified", Long.toString(Files.getLastModifiedTime(zip).toMillis())); + return p; + } + + private Properties readMarker() throws IOException { + Properties p = new Properties(); + Path marker = packRoot.resolve(MARKER); + if (Files.isRegularFile(marker)) { + try (InputStream in = Files.newInputStream(marker)) { + p.load(in); + } + } + return p; + } + + private static void deleteTree(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + try (Stream paths = Files.walk(root)) { + for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) { + Files.delete(p); + } + } + } +} From a46a1ada613e91441ee9c8ff5a34e8096f9b54e6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:14 +0800 Subject: [PATCH 0420/1461] Add MCHeli.java to the 26.1 shared sources --- .../java/com/jnhprojects/mcheli/MCHeli.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java new file mode 100644 index 00000000..1a890651 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.registry.ModParticles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The mod, minus the loader. Each loader module's entry point installs its {@link Platform}, calls + * {@link #init()}, registers what {@code Registration} collected and wires its events to + * {@link CommonHooks} (and, on a client, {@code ClientHooks}). + */ +public final class MCHeli { + public static final String MOD_ID = "mcheli"; + public static final Logger LOGGER = LoggerFactory.getLogger(MCHeli.class); + + private static LegacyContent legacyContent; + private static ContentPacks packs; + private static ContentRegistry content; + + private MCHeli() { + } + + /** + * Imports the original content if needed, loads the definitions and declares the registry + * entries. The vehicle list must be known here: 1.0.0 had an item per vehicle, and those items + * are still registered so old worlds keep them. + */ + public static void init() { + Platform platform = Platform.get(); + LOGGER.info("MCHeli Rework {} on {}", platform.modVersion(), platform.loaderName()); + legacyContent = LegacyContent.prepare(platform.gameDir()); + packs = new ContentPacks(platform.gameDir(), legacyContent); + content = packs.load(false); + ModBlocks.init(); + ModBlockEntities.init(); + ModDataComponents.init(); + ModEntities.init(); + ModParticles.init(); + ModMenus.init(); + ModItems.init(content); + ModCreativeTabs.init(); + } + + /** + * Vehicle and weapon definitions, from the imported original content and the content packs. + * Available from mod construction onwards. Always the same object; a reload changes what it + * holds. + */ + public static ContentRegistry content() { + return content; + } + + /** Where the original vehicle content came from, and why it is missing if it is. */ + public static LegacyContent legacyContent() { + return legacyContent; + } + + /** The content packs in {@code mcheli/packs} and the ones in use. */ + public static ContentPacks packs() { + return packs; + } +} From 2c40e705db51fa4a1c6c98338d056f3eacb14dbc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:15 +0800 Subject: [PATCH 0421/1461] Add McheliCommand.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/McheliCommand.java | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java new file mode 100644 index 00000000..25a3aea8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java @@ -0,0 +1,262 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.Definition; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.network.PackSubmissions; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * {@code /mcheli}: what content is loaded, and operator tools to hand out vehicles and reload the + * definitions and content packs. + *

+ *   /mcheli                              content source and counts
+ *   /mcheli list [helicopter|plane|...]  loaded vehicles
+ *   /mcheli packs                        content packs in use, and their problems
+ *   /mcheli give <players> <vehicle> [count]   (operators)
+ *   /mcheli reload                       re-read the definitions and packs (operators)
+ *   /mcheli pending                      packs players have sent, waiting (operators)
+ *   /mcheli approve <pack>               put a waiting pack in use (operators)
+ *   /mcheli reject <pack>                throw a waiting pack away (operators)
+ * 
+ */ +public final class McheliCommand { + private static final List KINDS = List.of("helicopter", "plane", "tank", "vehicle"); + private static final DynamicCommandExceptionType UNKNOWN_VEHICLE = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.unknown_vehicle", name)); + private static final DynamicCommandExceptionType UNKNOWN_PENDING = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.pending.unknown", name)); + + private McheliCommand() { + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal(MCHeli.MOD_ID) + .executes(McheliCommand::status) + .then(Commands.literal("list") + .executes(ctx -> list(ctx, null)) + .then(Commands.argument("kind", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest(KINDS, builder)) + .executes(ctx -> list(ctx, StringArgumentType.getString(ctx, "kind"))))) + .then(Commands.literal("packs") + .executes(McheliCommand::packs)) + .then(Commands.literal("give") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("targets", EntityArgument.players()) + .then(Commands.argument("vehicle", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest( + MCHeli.content().all().stream().map(AircraftDefinition::name), builder)) + .executes(ctx -> give(ctx, 1)) + .then(Commands.argument("count", IntegerArgumentType.integer(1, 64)) + .executes(ctx -> give(ctx, IntegerArgumentType.getInteger(ctx, "count"))))))) + .then(Commands.literal("reload") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::reload)) + .then(Commands.literal("pending") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::pending)) + .then(Commands.literal("approve") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::approve))) + .then(Commands.literal("reject") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::reject)))); + } + + private static CompletableFuture suggestPending(CommandContext ctx, SuggestionsBuilder builder) { + try { + return SharedSuggestionProvider.suggest(MCHeli.packs().pending().list().stream().map(PendingPacks.Submission::id), builder); + } catch (IOException e) { + return builder.buildFuture(); + } + } + + private static int pending(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List waiting; + try { + waiting = MCHeli.packs().pending().list(); + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + if (waiting.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.none"), false); + } + for (PendingPacks.Submission submission : waiting) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.entry", submission.id(), submission.manifest().version(), + submission.manifest().author(), submission.submitter(), "%.1f".formatted(submission.size() / 1048576.0), + String.join(", ", submission.definitions())), false); + List replaced = replaced(submission); + if (!replaced.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.replaces", String.join(", ", replaced)), false); + } + } + return waiting.size(); + } + + /** + * What going live would take the place of: a pack with the same id, and vehicles or weapons of + * another pack with the same file name. That is allowed, so it is the operator's call to make, + * but it should not come as a surprise. + */ + private static List replaced(PendingPacks.Submission submission) { + List names = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.id().equals(submission.id())) { + names.add("pack " + pack.id()); + } + } + ContentRegistry content = MCHeli.content(); + for (String path : submission.definitions()) { + String name = path.substring(path.lastIndexOf('/') + 1, path.length() - ".txt".length()); + Definition existing = path.startsWith("weapons/") ? content.weapon(name) : content.any(name); + if (existing != null && !existing.pack().equals(submission.id())) { + names.add(name + " (" + existing.pack() + ")"); + } + } + return names; + } + + private static PendingPacks.Submission waiting(CommandContext ctx) throws CommandSyntaxException { + String id = StringArgumentType.getString(ctx, "pack"); + PendingPacks.Submission submission; + try { + submission = MCHeli.packs().pending().find(id); + } catch (IOException e) { + submission = null; + } + if (submission == null) { + throw UNKNOWN_PENDING.create(id); + } + return submission; + } + + private static int approve(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + if (PackSubmissions.install(source.getServer(), submission)) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.approve.success", submission.id(), submission.submitter()), true); + return 1; + } + source.sendFailure(Component.translatable("commands.mcheli.approve.left_out", submission.id())); + MCHeli.packs().reportProblems(source::sendFailure); + } catch (IOException | PackException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.getMessage())); + } + return 0; + } + + private static int reject(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + PackSubmissions.reject(source.getServer(), submission); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reject.success", submission.id(), submission.submitter()), true); + return 1; + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + } + + private static int status(CommandContext ctx) { + ContentRegistry content = MCHeli.content(); + CommandSourceStack source = ctx.getSource(); + MCHeli.legacyContent().problem().ifPresent(source::sendFailure); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status", + content.helicopters().size(), content.planes().size(), content.tanks().size(), content.vehicles().size(), + content.weapons().size(), MCHeli.legacyContent().assets().toString()), false); + List packs = MCHeli.packs().active(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status.packs", packs.size(), + packs.stream().map(ContentPack::id).collect(Collectors.joining(", "))), false); + return content.all().size(); + } + + private static int packs(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List packs = MCHeli.packs().active(); + if (packs.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.none", MCHeli.packs().folder().toString()), false); + } else { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.header", packs.size(), MCHeli.packs().folder().toString()), false); + } + ContentRegistry content = MCHeli.content(); + for (ContentPack pack : packs) { + long vehicles = content.all().stream().filter(def -> def.pack().equals(pack.id())).count(); + long weapons = content.weapons().stream().filter(def -> def.pack().equals(pack.id())).count(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.entry", pack.id(), pack.manifest().version(), + pack.manifest().name(), vehicles, weapons, ContentPack.shortHash(pack.hash())), false); + if (!pack.manifest().author().isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.author", pack.manifest().author()), false); + } + } + MCHeli.packs().reportProblems(source::sendFailure); + return packs.size(); + } + + private static int list(CommandContext ctx, String kind) { + List defs = MCHeli.content().all().stream() + .filter(def -> kind == null || def.kind().equals(kind)) + .toList(); + String names = defs.stream().map(AircraftDefinition::name).collect(Collectors.joining(", ")); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.list", defs.size(), names), false); + return defs.size(); + } + + private static int give(CommandContext ctx, int count) throws CommandSyntaxException { + String name = StringArgumentType.getString(ctx, "vehicle"); + AircraftDefinition def = MCHeli.content().any(name); + if (def == null) { + throw UNKNOWN_VEHICLE.create(name); + } + Collection targets = EntityArgument.getPlayers(ctx, "targets"); + for (ServerPlayer player : targets) { + // Vehicle items do not stack, so each one is a separate stack. + for (int i = 0; i < count; i++) { + player.getInventory().placeItemBackInInventory(AircraftItem.stackOf(def)); + } + } + Component vehicle = AircraftItem.stackOf(def).getHoverName(); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.give", count, vehicle, targets.size()), true); + return targets.size(); + } + + private static int reload(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + ContentRegistry fresh = ContentReload.reload(source.getServer()); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reload.success", + fresh.all().size(), fresh.weapons().size(), MCHeli.packs().active().size()), true); + MCHeli.packs().reportProblems(source::sendFailure); + return fresh.all().size(); + } +} From fdcc5275914aa5a379989bd33971e5d6da0b8ada Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:15 +0800 Subject: [PATCH 0422/1461] Add ModSounds.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/ModSounds.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java new file mode 100644 index 00000000..ed6eeb7e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java @@ -0,0 +1,64 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.core.Holder; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Sound events from {@code assets/mcheli/sounds.json}. + * + *

The events are not put in the registry. Content files name their sounds freely and a content + * pack may bring its own, so they are created on demand as direct holders, which the sound packet + * and the sound manager both accept. + * + *

A sound a pack brought is in no {@code sounds.json}, so the sound manager would drop it as + * unknown. For those the server sends our own packet, and the client streams the file. + */ +public final class ModSounds { + /** How far a sound carries at a volume of one, and per volume above that, as in vanilla. */ + private static final float RANGE = 16.0F; + + private static final Map EVENTS = new ConcurrentHashMap<>(); + + private ModSounds() { + } + + public static SoundEvent get(String name) { + return EVENTS.computeIfAbsent(name, n -> SoundEvent.createVariableRangeEvent(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, n))); + } + + /** Plays {@code name} at the entity for everyone nearby. Call on the server. */ + public static void playAt(Entity entity, String name, float volume, float pitch) { + playAt(entity.level(), entity.getX(), entity.getY(), entity.getZ(), name, volume, pitch); + } + + /** Plays {@code name} at a position for everyone nearby. Call on the server. */ + public static void playAt(Level level, double x, double y, double z, String name, float volume, float pitch) { + if (level instanceof ServerLevel server && MCHeli.packs().hasSound(name)) { + ModNetwork.sendNear(server, x, y, z, RANGE * Math.max(volume, 1.0F), new PackSoundPacket(name, x, y, z, volume, pitch)); + } else { + level.playSound(null, x, y, z, get(name), SoundSource.NEUTRAL, volume, pitch); + } + } + + /** Plays {@code name} for one player only, such as a hit confirmation. */ + public static void playTo(ServerPlayer player, String name, float volume, float pitch) { + if (MCHeli.packs().hasSound(name)) { + ModNetwork.sendToPlayer(player, new PackSoundPacket(name, player.getX(), player.getY(), player.getZ(), volume, pitch)); + } else { + player.connection.send(new ClientboundSoundPacket(Holder.direct(get(name)), SoundSource.PLAYERS, + player.getX(), player.getY(), player.getZ(), volume, pitch, player.getRandom().nextLong())); + } + } +} From ab4e3d7d4801ad68d83dba100ffd8eaf711b570c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:16 +0800 Subject: [PATCH 0423/1461] Add ServerConfig.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/ServerConfig.java | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java new file mode 100644 index 00000000..9db31738 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java @@ -0,0 +1,172 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Server rules, the part of the original {@code mcheli.cfg} that decides how the game plays. + * The file lives per world in {@code serverconfig/mcheli-server.toml}, where Forge kept it, and is + * never sent to clients (only the server applies these rules). It is read when the world starts + * and again on {@code /mcheli reload}. + * + *

The values only exist while a world is loaded. Before that, and on clients, the getters return + * the defaults, which match 1.0.0. + */ +public final class ServerConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue EXPLOSIONS_BREAK_BLOCKS; + private static final ConfigSpec.BooleanValue EXPLOSIONS_START_FIRES; + private static final ConfigSpec.DoubleValue VEHICLE_DAMAGE_FACTOR; + private static final ConfigSpec.BooleanValue INFINITE_AMMO; + private static final ConfigSpec.BooleanValue INFINITE_FUEL; + private static final ConfigSpec.StringListValue DISABLED_VEHICLES; + private static final ConfigSpec.StringListValue PACK_ALLOW_LIST; + private static final ConfigSpec.IntValue MAX_PACK_SIZE; + private static final ConfigSpec.IntValue MAX_TOTAL_SIZE; + private static final ConfigSpec.BooleanValue ALLOW_SUBMISSIONS; + private static final ConfigSpec.BooleanValue REQUIRE_APPROVAL; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("explosions"); + EXPLOSIONS_BREAK_BLOCKS = builder + .comment("Whether weapons and crashing vehicles break blocks. The mobGriefing game rule also applies.", + "Original: Explosion_DestroyBlock") + .define("breakBlocks", true); + EXPLOSIONS_START_FIRES = builder + .comment("Whether incendiary weapons and crashes set blocks on fire. Original: Explosion_FlamingBlock") + .define("startFires", true); + builder.pop(); + + builder.push("vehicles"); + VEHICLE_DAMAGE_FACTOR = builder + .comment("Multiplies the damage every vehicle takes, after armour. 0.5 makes them twice as tough.") + .defineInRange("damageFactor", 1.0, 0.0, 100.0); + DISABLED_VEHICLES = builder + .comment("Vehicles that cannot be placed, by definition name (the file name, such as \"ah-64\").", + "Vehicles already in the world stay.") + .defineList("disabled", List.of()); + INFINITE_FUEL = builder + .comment("Whether vehicles never run out of fuel. Original: InfinityFuel") + .define("infiniteFuel", false); + builder.pop(); + + builder.push("weapons"); + INFINITE_AMMO = builder + .comment("Whether vehicle weapons never run out of ammunition. Original: InfinityAmmo") + .define("infiniteAmmo", false); + builder.pop(); + + builder.comment("Content packs from /mcheli/packs. Players get the packs from the server when they join.", + "Changes apply when the world starts or after /mcheli reload.") + .push("packs"); + PACK_ALLOW_LIST = builder + .comment("Pack ids the server loads, such as \"rework-example\". Empty loads every pack in the folder.") + .defineList("allowList", List.of()); + MAX_PACK_SIZE = builder + .comment("Largest pack the server loads and sends to players, in megabytes.") + .defineInRange("maxPackSize", 8, 1, 512); + MAX_TOTAL_SIZE = builder + .comment("Most the server loads and sends in all, in megabytes. Packs are taken in id order until", + "the next one would go over.") + .defineInRange("maxTotalSize", 64, 1, 4096); + ALLOW_SUBMISSIONS = builder + .comment("Whether players may send this server packs they made in the workshop. They wait in mcheli/pending", + "until an operator runs /mcheli approve. Off, players can only get packs from the server's owner.") + .define("allowSubmissions", false); + REQUIRE_APPROVAL = builder + .comment("Whether a submitted pack waits for an operator. Off, any player can add vehicles and weapons for", + "everyone, and even replace existing ones, so only turn it off among people you trust. A pack taking", + "the id of a pack already in use always waits.") + .define("requireApproval", true); + builder.pop(); + + SPEC = builder.build(); + } + + private ServerConfig() { + } + + /** Reads the world's file, writing it with the defaults if it is new. */ + public static void load(Path worldDir) { + Path file = worldDir.resolve("serverconfig").resolve(MCHeli.MOD_ID + "-server.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + /** The world closed: back to the defaults. */ + public static void unload() { + SPEC.unload(); + } + + public static boolean explosionsBreakBlocks() { + return SPEC.isLoaded() ? EXPLOSIONS_BREAK_BLOCKS.get() : EXPLOSIONS_BREAK_BLOCKS.getDefault(); + } + + public static boolean explosionsStartFires() { + return SPEC.isLoaded() ? EXPLOSIONS_START_FIRES.get() : EXPLOSIONS_START_FIRES.getDefault(); + } + + public static float vehicleDamageFactor() { + double factor = SPEC.isLoaded() ? VEHICLE_DAMAGE_FACTOR.get() : VEHICLE_DAMAGE_FACTOR.getDefault(); + return (float) factor; + } + + public static boolean infiniteAmmo() { + return SPEC.isLoaded() ? INFINITE_AMMO.get() : INFINITE_AMMO.getDefault(); + } + + public static boolean infiniteFuel() { + return SPEC.isLoaded() ? INFINITE_FUEL.get() : INFINITE_FUEL.getDefault(); + } + + /** Whether the allow list lets the server load this pack. An empty list allows every pack. */ + public static boolean isPackAllowed(String id) { + if (!SPEC.isLoaded() || PACK_ALLOW_LIST.get().isEmpty()) { + return true; + } + for (String allowed : PACK_ALLOW_LIST.get()) { + if (allowed.trim().toLowerCase(Locale.ROOT).equals(id)) { + return true; + } + } + return false; + } + + public static long maxPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_PACK_SIZE.get() : MAX_PACK_SIZE.getDefault()) << 20; + } + + public static long maxTotalPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_TOTAL_SIZE.get() : MAX_TOTAL_SIZE.getDefault()) << 20; + } + + public static boolean allowSubmissions() { + return SPEC.isLoaded() ? ALLOW_SUBMISSIONS.get() : ALLOW_SUBMISSIONS.getDefault(); + } + + public static boolean requireApproval() { + return SPEC.isLoaded() ? REQUIRE_APPROVAL.get() : REQUIRE_APPROVAL.getDefault(); + } + + public static boolean isDisabled(String vehicle) { + if (!SPEC.isLoaded()) { + return false; + } + for (String name : DISABLED_VEHICLES.get()) { + if (name.trim().toLowerCase(Locale.ROOT).equals(vehicle)) { + return true; + } + } + return false; + } +} From e647d2b7f5fce5fa8392ac863a4188fe1cd5c037 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:16 +0800 Subject: [PATCH 0424/1461] Add block/DraftingTableBlock.java to the 26.1 shared sources --- .../mcheli/block/DraftingTableBlock.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java new file mode 100644 index 00000000..8545d2b5 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java @@ -0,0 +1,100 @@ +package com.jnhprojects.mcheli.block; + +import com.mojang.serialization.MapCodec; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +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.block.state.properties.IntegerProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jspecify.annotations.Nullable; + +/** + * The drafting table, {@code MCH_DraftingTableBlock}: right-click opens the recipe browser, and + * Shift and right-click switches its lamp. It turns in eighths of a circle to face whoever placed + * it, and its block entity draws the original model. + */ +public class DraftingTableBlock extends BaseEntityBlock { + private static final MapCodec CODEC = simpleCodec(DraftingTableBlock::new); + public static final IntegerProperty ROTATION = IntegerProperty.create("rotation", 0, 7); + public static final BooleanProperty LIT = BooleanProperty.create("lit"); + private static final VoxelShape SHAPE = Block.column(16.0, 0.0, 15.0); + private static final Component TITLE = Component.translatable("block.mcheli.drafting_table"); + + public DraftingTableBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(ROTATION, 0).setValue(LIT, false)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ROTATION, LIT); + } + + /** {@code onBlockPlacedBy}: the nearest eighth of the placer's heading. */ + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + float yaw = context.getPlayer() != null ? context.getPlayer().getYRot() : 0.0F; + int rotation = (int) ((Mth.positiveModulo(yaw, 360.0F) + 22.5F) / 45.0F) % 8; + return defaultBlockState().setValue(ROTATION, rotation); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive()) { + level.setBlock(pos, state.cycle(LIT), Block.UPDATE_ALL); + level.playSound(null, pos, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.BLOCKS, 0.3F, 0.5F); + } else { + player.openMenu(state.getMenuProvider(level, pos)); + } + return InteractionResult.SUCCESS; + } + + @Override + protected @Nullable MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { + return new SimpleMenuProvider((id, inventory, player) -> new DraftingTableMenu(id, inventory, ContainerLevelAccess.create(level, pos)), TITLE); + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + /** The block entity renderer draws the table. */ + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new DraftingTableBlockEntity(pos, state); + } +} From 756e189b1772c401c870ea261415f8ac1db14800 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:16 +0800 Subject: [PATCH 0425/1461] Add block/DraftingTableBlockEntity.java to the 26.1 shared sources --- .../mcheli/block/DraftingTableBlockEntity.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java new file mode 100644 index 00000000..56238f3f --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.block; + +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** {@code MCH_DraftingTableTileEntity}: holds nothing; it is there so the table's model can be drawn. */ +public class DraftingTableBlockEntity extends BlockEntity { + public DraftingTableBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.DRAFTING_TABLE.get(), pos, state); + } +} From 7e6c04360b63ac7a95eac70388aae16e9518b695 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:17 +0800 Subject: [PATCH 0426/1461] Add client/CameraAngles.java to the 26.1 shared sources --- .../mcheli/client/CameraAngles.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java new file mode 100644 index 00000000..ab62e120 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.client; + +import net.minecraft.client.Camera; + +/** + * The camera's angles for this frame, after vanilla placed the camera and before it renders, as + * the loader module hands them to {@link ClientHooks#computeCameraAngles}. Forge and NeoForge + * back this with their viewport event; Fabric with a mixin at the same spot. + */ +public interface CameraAngles { + Camera camera(); + + float partialTick(); + + float yaw(); + + float pitch(); + + float roll(); + + void setYaw(float yaw); + + void setPitch(float pitch); + + void setRoll(float roll); +} From 9d7514e47fb44e1e6dd9b2f5f8bca1fcf709a106 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:17 +0800 Subject: [PATCH 0427/1461] Add client/ClientContentReload.java to the 26.1 shared sources --- .../mcheli/client/ClientContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java new file mode 100644 index 00000000..70bcc23e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.CreativeModeTab; + +/** + * Makes the client show the content it now holds: after the server's pack list was applied, which + * also happens after {@code /mcheli reload}. + */ +final class ClientContentReload { + private ClientContentReload() { + } + + /** Drops cached models and textures so edited and new files load, and rebuilds the creative tabs. */ + static void refresh() { + Minecraft mc = Minecraft.getInstance(); + MeshCache.INSTANCE.clear(); + RuntimeAssets.refreshTextures(); + if (mc.level != null && mc.player != null) { + boolean operator = mc.options.operatorItemsTab().get() && mc.player.canUseGameMasterBlocks(); + CreativeModeTab.ItemDisplayParameters params = + new CreativeModeTab.ItemDisplayParameters(mc.level.enabledFeatures(), operator, mc.level.registryAccess()); + ModCreativeTabs.all().forEach(tab -> tab.buildContents(params)); + } + } +} From 8e0078931f63e1d1088ea7c59294cda99c0ce413 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:18 +0800 Subject: [PATCH 0428/1461] Let the loaders load post effects and read the camera partial tick on 26.1 --- .../mcheli/client/ClientHooks.java | 333 ++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java new file mode 100644 index 00000000..bf19ca3c --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -0,0 +1,333 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.client.hud.HudScripts; +import com.jnhprojects.mcheli.client.render.A10Renderer; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import com.jnhprojects.mcheli.client.render.ChainRenderer; +import com.jnhprojects.mcheli.client.render.DraftingTableRenderer; +import com.jnhprojects.mcheli.client.render.EquipmentRenderer; +import com.jnhprojects.mcheli.client.render.FlareRenderer; +import com.jnhprojects.mcheli.client.render.GltdRenderer; +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.render.ParachuteRenderer; +import com.jnhprojects.mcheli.client.render.ProjectileRenderer; +import com.jnhprojects.mcheli.client.render.RiderPose; +import com.jnhprojects.mcheli.client.render.ThrowableRenderer; +import com.jnhprojects.mcheli.client.render.UavStationRenderer; +import com.jnhprojects.mcheli.client.screen.DraftingTableScreen; +import com.jnhprojects.mcheli.client.screen.TeamManagerScreen; +import com.jnhprojects.mcheli.client.screen.UavStationScreen; +import com.jnhprojects.mcheli.client.screen.VehicleScreen; +import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; +import com.jnhprojects.mcheli.client.workshop.WorkshopClient; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.team.Markers; +import com.jnhprojects.mcheli.team.TeamManager; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.NoopRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import java.util.List; +import java.util.Map; + +/** + * Client side counterpart of {@code CommonHooks}: what the mod does on client events. The loader + * module calls these from its own events. Kept out of the shared entry point so dedicated servers + * never load these classes. + */ +public final class ClientHooks { + /** The weapon HUD's layer, drawn above the vanilla HUD. */ + public static final Identifier HUD_LAYER = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "weapons"); + + private static boolean shaderProgramsAssigned; + + private ClientHooks() { + } + + /** Loads a post effect into the game renderer, whose own method is private on 26.1. */ + public interface PostEffects { + void load(Identifier effect); + } + + private static PostEffects postEffects = effect -> { + }; + + static PostEffects postEffects() { + return postEffects; + } + + /** During mod construction, on a client only. */ + public static void init(PostEffects effects) { + postEffects = effects; + ClientConfig.load(Platform.get().configDir()); + ClientAccess.install(() -> Minecraft.getInstance().player, ClientPacks.INSTANCE); + } + + /** How a loader registers an entity renderer. */ + public interface EntityRenderers { + void register(EntityType type, EntityRendererProvider provider); + } + + public static void registerEntityRenderers(EntityRenderers renderers) { + renderers.register(ModEntities.HELICOPTER.get(), AircraftRenderer::new); + renderers.register(ModEntities.PLANE.get(), AircraftRenderer::new); + renderers.register(ModEntities.TANK.get(), AircraftRenderer::new); + renderers.register(ModEntities.VEHICLE.get(), AircraftRenderer::new); + renderers.register(ModEntities.BULLET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.BOMB.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AA_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AS_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AT_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TV_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TORPEDO.get(), ProjectileRenderer::new); + renderers.register(ModEntities.MARKER_ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.DISPENSED_ITEM.get(), ProjectileRenderer::new); + renderers.register(ModEntities.FLARE.get(), FlareRenderer::new); + renderers.register(ModEntities.A10.get(), A10Renderer::new); + renderers.register(ModEntities.PARACHUTE.get(), ParachuteRenderer::new); + renderers.register(ModEntities.THROWABLE.get(), ThrowableRenderer::new); + renderers.register(ModEntities.GLTD.get(), GltdRenderer::new); + renderers.register(ModEntities.CHAIN.get(), ChainRenderer::new); + renderers.register(ModEntities.CONTAINER.get(), context -> new EquipmentRenderer<>(context, "container", 0.2F, 0xFFBFBFBF, 1.0F)); + renderers.register(ModEntities.UAV_STATION.get(), UavStationRenderer::new); + renderers.register(ModEntities.RAPPEL.get(), NoopRenderer::new); + } + + /** How a loader registers a block entity renderer. */ + public interface BlockEntityRenderers { + void register(BlockEntityType type, + BlockEntityRendererProvider provider); + } + + public static void registerBlockEntityRenderers(BlockEntityRenderers renderers) { + renderers.register(ModBlockEntities.DRAFTING_TABLE.get(), DraftingTableRenderer::new); + } + + /** How a loader registers the screen of a menu: vanilla's own method is not public on all of them. */ + public interface MenuScreenRegistrar { + > void register(MenuType type, + MenuScreens.ScreenConstructor screen); + } + + /** On the main thread during client setup, once our menu types are registered. */ + public static void registerMenuScreens(MenuScreenRegistrar screens) { + screens.register(ModMenus.VEHICLE.get(), VehicleScreen::new); + screens.register(ModMenus.UAV_STATION.get(), UavStationScreen::new); + screens.register(ModMenus.DRAFTING_TABLE.get(), DraftingTableScreen::new); + } + + public static List keyMappings() { + return KeyBindings.ALL; + } + + /** Client resource reload listeners, by the id Fabric wants for each. */ + public static Map reloadListeners() { + return Map.of(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "meshes"), MeshCache.INSTANCE, + Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "hud_scripts"), HudScripts.INSTANCE); + } + + public static void clientTickEnd() { + if (!shaderProgramsAssigned) { + // On the first tick rather than at construction, so Iris has surely loaded by now. + shaderProgramsAssigned = true; + Glow.assignShaderPrograms(); + } + CrewController.onClientTick(); + GltdController.onClientTick(); + RangefinderController.onClientTick(); + LauncherController.onClientTick(); + VehicleCamera.tick(); + WeaponController.onClientTick(); + HudRenderer.tick(); + EngineSoundPlayer.tick(); + TargetMarkers.tick(); + WorkshopClient.tick(); + while (KeyBindings.TEAM_MANAGER.consumeClick()) { + openTeamManager(); + } + } + + /** Operators get the team manager; anyone else is told it is theirs to open. */ + private static void openTeamManager() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.screen != null) { + return; + } + if (TeamManager.mayManage(mc.player)) { + mc.setScreen(new TeamManagerScreen()); + } else { + mc.player.sendOverlayMessage(Component.translatable("message.mcheli.team.operators_only")); + } + } + + /** Tilts the view with the hull and moves the eye into the cockpit or onto a TV missile. */ + public static void computeCameraAngles(CameraAngles angles) { + CrewController.onCameraAngles(angles); + GltdController.onCameraAngles(angles); + } + + /** Sets the camera's rotation, roll included, the way Forge's {@code Camera#setRotation} does. */ + public interface CameraRotation { + void set(Camera camera, float yaw, float pitch, float roll); + } + + /** + * For loaders without Forge's camera angle event: called where Forge fires it, right after + * vanilla placed the camera for this frame. Starts from the camera's own angles and no roll, + * and only touches the rotation when something changed it. + */ + public static void computeCameraAngles(Camera camera, float partialTick, CameraRotation rotation) { + Angles angles = new Angles(camera, partialTick, camera.yRot(), camera.xRot()); + computeCameraAngles(angles); + if (angles.yaw != camera.yRot() || angles.pitch != camera.xRot() || angles.roll != 0.0F) { + rotation.set(camera, angles.yaw, angles.pitch, angles.roll); + } + } + + private static final class Angles implements CameraAngles { + private final Camera camera; + private final float partialTick; + private float yaw; + private float pitch; + private float roll; + + Angles(Camera camera, float partialTick, float yaw, float pitch) { + this.camera = camera; + this.partialTick = partialTick; + this.yaw = yaw; + this.pitch = pitch; + } + + @Override + public Camera camera() { + return camera; + } + + @Override + public float partialTick() { + return partialTick; + } + + @Override + public float yaw() { + return yaw; + } + + @Override + public float pitch() { + return pitch; + } + + @Override + public float roll() { + return roll; + } + + @Override + public void setYaw(float yaw) { + this.yaw = yaw; + } + + @Override + public void setPitch(float pitch) { + this.pitch = pitch; + } + + @Override + public void setRoll(float roll) { + this.roll = roll; + } + } + + /** The field of view for this frame, narrowed while the player zooms through a sight. */ + public static float fov(float fov) { + return LauncherController.fov(RangefinderController.fov(VehicleCamera.fov(fov))); + } + + /** Whether to skip drawing the first person hand this frame. */ + public static boolean hidesHand() { + return CrewController.hidesHand() || GltdController.hidesHand() || RangefinderController.isScoped() || LauncherController.isRaised(); + } + + /** What happens to a press of the use key. */ + public enum UseKey { + /** Vanilla handles it. */ + PASS, + /** Taken, and the hand does not swing: it is a weapon trigger. */ + CONSUME, + /** Taken, and the hand swings as for a vanilla click: the player boards a vehicle. */ + CONSUME_AND_SWING + } + + public static UseKey onUseKey(InteractionHand hand) { + if (WeaponController.onUseItem() || GltdController.onUseItem()) { + return UseKey.CONSUME; + } + return VehiclePicker.onUseItem(hand) ? UseKey.CONSUME_AND_SWING : UseKey.PASS; + } + + /** The pick block key switches weapons in a seat that has any; true when it was taken. */ + public static boolean onPickBlockKey() { + return WeaponController.onPickBlock(); + } + + /** The partial tick the camera entity is drawn at, for the loaders whose event does not carry it. */ + public static float cameraPartialTicks() { + Minecraft mc = Minecraft.getInstance(); + return mc.gameRenderer.getMainCamera().getCameraEntityPartialTicks(mc.getDeltaTracker()); + } + + public static void beforeRenderLiving(LivingEntityRenderState state, PoseStack pose, float partialTicks) { + RiderPose.beforeRender(state, pose, partialTicks); + } + + public static void drawHud(GuiGraphicsExtractor graphics, DeltaTracker delta) { + WeaponHud.draw(graphics, delta); + GltdController.drawHud(graphics, delta); + RangefinderController.drawHud(graphics, delta); + LauncherController.drawHud(graphics, delta); + TargetMarkers.draw(graphics, delta); + } + + /** Without the original zip there are no vehicles, so say so where the player will see it. */ + public static void loggedIn(LocalPlayer player) { + MCHeli.legacyContent().problem().ifPresent(player::sendSystemMessage); + } + + public static void loggedOut() { + ClientPacks.INSTANCE.loggedOut(); + WorkshopClient.loggedOut(); + Markers.clear(); + } +} From 3a708e541c12362c6c2114cda75d3a8f7f6d2e4f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:18 +0800 Subject: [PATCH 0429/1461] Add client/ClientPacks.java to the 26.1 shared sources --- .../mcheli/client/ClientPacks.java | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java new file mode 100644 index 00000000..d933e518 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java @@ -0,0 +1,338 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackRequestPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import com.jnhprojects.mcheli.platform.Platform; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * The client half of the content pack sync (the server half is {@code PackSync}). + * + *

When the server sends its pack list, every pack this client has neither in its own + * {@code mcheli/packs} nor in {@code mcheli/cache} is requested. Each arrives in chunks, is checked + * against its hash and kept as {@code mcheli/cache/.zip}. Once all are there, the content is + * rebuilt from the client's own import plus exactly the server's packs, in the server's order, so + * the vehicle a player flies (which their client simulates) has the server's definition. + * + *

In single player, and for the host of a LAN game, client and server share one registry, so + * only the client's view of the server's packs is refreshed. + * + *

The cache is capped by {@code packs.cacheSize} in the client config: after a download, the + * packs used longest ago go first. The current server's packs are always kept. + */ +public final class ClientPacks implements ClientAccess.PackReceiver { + public static final ClientPacks INSTANCE = new ClientPacks(); + + private static final Pattern CACHED = Pattern.compile("[0-9a-f]{64}\\.zip"); + + private final Path cache = Platform.get().gameDir().resolve(MCHeli.MOD_ID).resolve("cache"); + private List wanted = List.of(); + private final Map downloads = new LinkedHashMap<>(); + /** Packs this class opened for the current server; closed when the next list replaces them. */ + private List mounted = List.of(); + private int downloaded; + + private ClientPacks() { + } + + @Override + public void onPackList(PackListPacket packet) { + if (Minecraft.getInstance().hasSingleplayerServer()) { + useRoots(MCHeli.packs().active()); + ClientContentReload.refresh(); + return; + } + + wanted = packet.packs(); + downloads.clear(); + downloaded = 0; + long cap = ClientConfig.packCacheBytes(); + List request = new ArrayList<>(); + long bytes = 0; + for (PackListPacket.Entry entry : wanted) { + if (find(entry) != null) { + continue; + } + if (entry.size() > cap || entry.size() <= 0) { + say(Component.translatable("message.mcheli.pack.too_large", entry.id(), mb(entry.size()), cap >> 20)); + continue; + } + downloads.put(entry.hash(), new Download(entry)); + request.add(entry.hash()); + bytes += entry.size(); + } + + if (request.isEmpty()) { + apply(); + } else { + say(Component.translatable("message.mcheli.pack.downloading", request.size(), mb(bytes))); + ModNetwork.sendToServer(new PackRequestPacket(request)); + } + } + + @Override + public void onPackData(PackDataPacket packet) { + Download download = downloads.get(packet.hash()); + if (download == null) { + return; + } + if (!download.accept(packet)) { + fail(download, "the server sent a chunk out of order"); + return; + } + if (!download.complete()) { + return; + } + downloads.remove(packet.hash()); + try { + store(download); + downloaded++; + } catch (IOException | PackException e) { + fail(download, e.getMessage()); + return; + } + if (downloads.isEmpty()) { + apply(); + } + } + + @Override + public void onPackSound(PackSoundPacket packet) { + Minecraft.getInstance().getSoundManager().play( + new OneShotSound(packet.sound(), packet.x(), packet.y(), packet.z(), packet.volume(), packet.pitch())); + } + + /** The packs the server sent or named, for the package browser. Empty when hosting the game. */ + public List serverPacks() { + return mounted; + } + + /** Leaving a server: its packs are no longer needed. */ + void loggedOut() { + downloads.clear(); + wanted = List.of(); + RuntimeAssets.setRoots(List.of()); + close(mounted); + mounted = List.of(); + } + + private void fail(Download download, String reason) { + downloads.remove(download.entry.hash()); + MCHeli.LOGGER.error("Content pack {} could not be downloaded: {}", download.entry.id(), reason); + say(Component.translatable("message.mcheli.pack.failed", download.entry.id(), reason)); + if (downloads.isEmpty()) { + apply(); + } + } + + /** Writes a finished download to the cache once its content matches the hash it was listed with. */ + private void store(Download download) throws IOException, PackException { + String hash = download.entry.hash(); + Files.createDirectories(cache); + Path part = cache.resolve(hash + ".download.zip"); + Files.write(part, download.data); + try (ContentPack pack = ContentPack.open(part)) { + if (!pack.hash().equals(hash)) { + throw new PackException("its files do not match the hash the server listed"); + } + } catch (IOException | PackException e) { + Files.deleteIfExists(part); + throw e; + } + Files.move(part, cache.resolve(hash + ".zip"), StandardCopyOption.REPLACE_EXISTING); + } + + /** Rebuilds the content from the import and the server's packs that are now on disk. */ + private void apply() { + List opened = new ArrayList<>(); + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, MCHeli.legacyContent().assets())); + for (PackListPacket.Entry entry : wanted) { + Path file = find(entry); + if (file == null) { + continue; + } + try { + ContentPack pack = ContentPack.open(file); + if (!pack.hash().equals(entry.hash())) { + pack.close(); + MCHeli.LOGGER.warn("Content pack {} changed on disk and no longer matches the server's", file); + continue; + } + opened.add(pack); + sources.add(pack.contentSource()); + if (file.startsWith(cache)) { + Files.setLastModifiedTime(file, FileTime.from(Instant.now())); + } + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not open content pack {}", file, e); + } + } + + ContentRegistry registry = ContentRegistry.load(sources); + MCHeli.content().replaceWith(registry); + List old = mounted; + mounted = List.copyOf(opened); + useRoots(mounted); + close(old); + ClientContentReload.refresh(); + trimCache(); + if (downloaded > 0) { + say(Component.translatable("message.mcheli.pack.ready", downloaded)); + downloaded = 0; + } + } + + /** The pack on disk with the entry's content: one of the player's own packs, or a cached download. */ + private @Nullable Path find(PackListPacket.Entry entry) { + for (ContentPack own : MCHeli.packs().active()) { + if (own.hash().equals(entry.hash())) { + return own.source(); + } + } + Path cached = cache.resolve(entry.hash() + ".zip"); + return Files.isRegularFile(cached) ? cached : null; + } + + /** Deletes the cached packs used longest ago until the cache fits its cap. */ + private void trimCache() { + Set keep = new HashSet<>(); + for (PackListPacket.Entry entry : wanted) { + keep.add(entry.hash() + ".zip"); + } + List files; + try (Stream list = Files.list(cache)) { + files = list.filter(p -> CACHED.matcher(p.getFileName().toString()).matches()) + .sorted(Comparator.comparing(ClientPacks::lastUsed)) + .toList(); + } catch (IOException e) { + return; + } + long total = 0; + for (Path file : files) { + total += size(file); + } + long cap = ClientConfig.packCacheBytes(); + for (Path file : files) { + if (total <= cap) { + break; + } + if (keep.contains(file.getFileName().toString())) { + continue; + } + try { + long size = size(file); + Files.delete(file); + total -= size; + MCHeli.LOGGER.info("Removed {} from the content pack cache", file.getFileName()); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not remove {} from the content pack cache", file, e); + } + } + } + + private static FileTime lastUsed(Path file) { + try { + return Files.getLastModifiedTime(file); + } catch (IOException e) { + return FileTime.fromMillis(0); + } + } + + private static long size(Path file) { + try { + return Files.size(file); + } catch (IOException e) { + return 0; + } + } + + /** The pack loaded last overrides the others, so it is searched first. */ + private static void useRoots(List packs) { + List roots = new ArrayList<>(); + for (ContentPack pack : packs) { + roots.addFirst(pack.assets()); + } + RuntimeAssets.setRoots(roots); + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } + + private static void say(Component message) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null) { + mc.player.sendSystemMessage(message); + } else { + MCHeli.LOGGER.info(message.getString()); + } + } + + private static String mb(long bytes) { + return "%.1f".formatted(bytes / 1048576.0); + } + + /** One pack arriving from the server. */ + private static final class Download { + private final PackListPacket.Entry entry; + private byte @Nullable [] data; + private int received; + + Download(PackListPacket.Entry entry) { + this.entry = entry; + } + + boolean accept(PackDataPacket packet) { + if (packet.total() != entry.size() || packet.offset() != received + || received + packet.data().length > entry.size()) { + return false; + } + if (data == null) { + data = new byte[entry.size()]; + } + System.arraycopy(packet.data(), 0, data, received, packet.data().length); + received += packet.data().length; + return true; + } + + boolean complete() { + return data != null && received == data.length; + } + } +} From db2e1f54a0797230a2889a3e26159bce9742a8d3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:19 +0800 Subject: [PATCH 0430/1461] Add client/CrewController.java to the 26.1 shared sources --- .../mcheli/client/CrewController.java | 482 ++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java new file mode 100644 index 00000000..32af6770 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java @@ -0,0 +1,482 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.CameraPosition; +import com.jnhprojects.mcheli.content.aircraft.SeatDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PilotStatePacket; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Input; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +/** + * Turns local player input into vehicle control, for whichever seat the player occupies. + * + *

Keys are sampled once per client tick. The mouse is handled every frame, the way the + * original did it: for helicopters and planes, vanilla first turns the player's view, then the + * change is fed to the aircraft and the view is snapped back onto the aircraft's nose. Everyone + * else looks around freely, within the limits of their seat, and turrets follow their view. + * + *

Gunner seats and camera positions move the first person eye to the point the definition + * gives, which is how the original's camera entity behaved. Every other seat keeps its eye above + * the seat along the hull's up axis, so it tilts with the vehicle. + */ +final class CrewController { + /** Two presses of jump within this many ticks eject the player. */ + private static final int EJECT_WINDOW = 10; + + private static boolean jumpWasDown; + private static int lastJumpPress = -100; + private static int ticks; + private static boolean tracking; + private static float lastYaw; + private static float lastPitch; + + private static @Nullable AircraftEntity viewVehicle; + private static int viewSeat = -1; + private static int cameraIndex; + private static boolean gunnerSight; + + private static @Nullable MethodHandle setCameraPosition; + private static boolean cameraLookupFailed; + + private CrewController() { + } + + /** + * Like the original, the first person hand stays hidden in a vehicle: always for the pilot, + * and for anyone else who looks through a camera or has a weapon to aim. + */ + static boolean hidesHand() { + LocalPlayer player = Minecraft.getInstance().player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + return false; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return false; + } + VehicleWeapons weapons = vehicle.weapons(); + boolean armed = weapons != null && weapons.selected(seat) >= 0; + return seat == 0 || armed || tvCamera(vehicle, seat) != null || eyePosition(vehicle, def, seat) != null; + } + + private static @Nullable AircraftEntity vehicleOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof AircraftEntity vehicle && vehicle.definition() != null ? vehicle : null; + } + + static void onClientTick() { + ticks++; + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + jumpWasDown = false; + tracking = false; + viewVehicle = null; + viewSeat = -1; + // The GLTD's sight shares the zoom and camera mode; only leaving it resets them. + if (GltdController.gltdOf(player) == null) { + VehicleCamera.reset(); + } + return; + } + int seat = vehicle.seatOf(player); + if (vehicle != viewVehicle || seat != viewSeat) { + // New vehicle or seat: start from the default view. + viewVehicle = vehicle; + viewSeat = seat; + cameraIndex = 0; + gunnerSight = false; + tracking = false; + VehicleCamera.reset(); + } + + Input keys = player.input.keyPresses; + boolean jumpPressed = keys.jump() && !jumpWasDown; + jumpWasDown = keys.jump(); + boolean pilot = seat == 0 && vehicle.getControllingPassenger() == player; + + if (pilot) { + AircraftEntity.ControlInput input = vehicle.input(); + input.throttleUp = keys.forward(); + input.throttleDown = keys.backward(); + input.left = keys.left(); + input.right = keys.right(); + input.freeLook = isFreeLook(vehicle); + input.brake = vehicle instanceof TankEntity && keys.jump(); + if (input.brake) { + // The hand brake also stops pivot turns once the vehicle has come to rest. + input.throttleUp = false; + input.throttleDown = false; + double dx = vehicle.getX() - vehicle.xo; + double dz = vehicle.getZ() - vehicle.zo; + if (vehicle.visibleThrottle() <= 0.03F && dx * dx + dz * dz < 0.01) { + input.left = false; + input.right = false; + } + } + if (jumpPressed && vehicle instanceof HelicopterEntity heli) { + heli.toggleHovering(); + } + ModNetwork.sendToServer(new PilotStatePacket(vehicle.getId(), vehicle.visibleThrottle(), vehicle.reverse(), + vehicle.roll(), vehicle.isHoveringMode(), input.write())); + } + + // Eject with a double tap, except for helicopter pilots whose jump key toggles hover. + if (jumpPressed && vehicle.canEject(player) && !(pilot && vehicle instanceof HelicopterEntity)) { + if (ticks - lastJumpPress <= EJECT_WINDOW) { + send(vehicle, CrewActionPacket.Action.EJECT); + lastJumpPress = -100; + } else { + lastJumpPress = ticks; + } + } + + while (KeyBindings.SWITCH_SEAT.consumeClick()) { + send(vehicle, CrewActionPacket.Action.SWITCH_SEAT); + } + while (KeyBindings.DROP_CREW.consumeClick()) { + send(vehicle, pilot ? CrewActionPacket.Action.DROP_CREW : CrewActionPacket.Action.JUMP_OUT); + } + while (KeyBindings.SWITCH_CAMERA.consumeClick()) { + switchCamera(vehicle, seat); + } + while (KeyBindings.LANDING_GEAR.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.LANDING_GEAR); + } + } + while (KeyBindings.FOLD.consumeClick()) { + if (looksThroughSight(vehicle, seat)) { + VehicleCamera.onZoomKey(player, vehicle); + } else if (pilot) { + send(vehicle, CrewActionPacket.Action.FOLD); + } + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + // As in the original, the key works the search light in vehicles that have one. + if (vehicle.canSwitchSearchLight(player)) { + send(vehicle, CrewActionPacket.Action.SEARCH_LIGHT); + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 1.0F); + } else { + VehicleCamera.onCameraModeKey(player, vehicle); + } + } + while (KeyBindings.VEHICLE_SCREEN.consumeClick()) { + if (!vehicle.isDestroyed()) { + send(vehicle, CrewActionPacket.Action.VEHICLE_SCREEN); + } + } + while (KeyBindings.VTOL.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.VTOL); + } + } + while (KeyBindings.RACK_LOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_LOAD); + } + } + while (KeyBindings.RACK_UNLOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_UNLOAD); + } + } + } + + /** + * Whether this seat sees through a sight that can zoom: a camera position or gunner sight, the + * pilot in gunner mode, or the pilot of a UAV. The zoom key folds the wings everywhere else. + */ + private static boolean looksThroughSight(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount() || def.cameraZoom <= 1) { + return false; + } + return eyePosition(vehicle, def, seat) != null || vehicle.isGunnerMode() || def.uav; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + /** Pilot cycles the camera positions; gunners that may leave their sight toggle it. */ + private static void switchCamera(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount()) { + return; + } + SeatDefinition s = def.seats.get(seat); + if (seat == 0) { + switchPilotView(vehicle, def); + } else if (s.gunner() && s.switchGunner()) { + gunnerSight = !gunnerSight; + } + } + + /** + * The pilot's switch mode key, after {@code KeySwitchMode}: leave gunner mode, otherwise step + * through the extra camera positions, otherwise enter gunner mode where the vehicle allows it, + * otherwise go to the first extra camera. Unlike the original, leaving gunner mode never + * depends on the plane still meeting the conditions for entering it. + */ + private static void switchPilotView(AircraftEntity vehicle, AircraftDefinition def) { + int cameras = def.cameraPositions.size(); + if (vehicle.isGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + cameraIndex = 0; + } else if (cameraIndex > 0) { + cameraIndex = (cameraIndex + 1) % cameras; + } else if (vehicle.canSwitchGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + } else if (cameras >= 2) { + cameraIndex = 1; + } + } + + /** + * Free look for the pilot. Helicopters and planes look around while the key is held; tanks + * start from their {@code DefaultFreelook} setting and the key swaps it while held. + */ + private static boolean isFreeLook(AircraftEntity vehicle) { + boolean held = KeyBindings.FREE_LOOK.isDown(); + if (vehicle instanceof TankEntity) { + AircraftDefinition def = vehicle.definition(); + return def != null && def.defaultFreelook != held; + } + return held; + } + + static void onCameraAngles(CameraAngles event) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null || mc.isPaused()) { + return; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return; + } + float partialTicks = event.partialTick(); + boolean firstPerson = mc.options.getCameraType().isFirstPerson(); + + if (seat == 0 && vehicle.steersWithMouse() && !vehicle.isGunnerMode() && vehicle.getControllingPassenger() == player) { + steerWithMouse(vehicle, player, event, partialTicks, firstPerson); + } else { + tracking = false; + constrainView(vehicle, def, def.seats.get(seat), seat, player); + } + + TvMissileEntity missile = tvCamera(vehicle, seat); + if (!(firstPerson && missile != null)) { + // Every seat of every kind tilts with the hull, measured along the way the rider looks. + event.setRoll(AircraftRotation.cameraRoll(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), + vehicle.roll(partialTicks), event.yaw(), event.pitch())); + } + if (firstPerson && missile != null) { + placeCamera(event.camera(), missile.getPosition(partialTicks)); + } else if (firstPerson) { + Vector3 eye = eyePosition(vehicle, def, seat); + moveCamera(event.camera(), vehicle, def, eye != null ? eye : seatEye(vehicle, def, seat, player), seat, partialTicks); + } + } + + /** + * The normal eye, in hull space. Vanilla puts it straight above the rider in world space, + * which leaves the head outside the cockpit once the hull banks or pitches; measured along the + * hull's own up axis it stays where the rider model's head is drawn. + */ + private static Vector3 seatEye(AircraftEntity vehicle, AircraftDefinition def, int seat, LocalPlayer player) { + Vector3 pos = def.seats.get(seat).position(); + double up = player.getEyeHeight() - player.getVehicleAttachmentPoint(vehicle).y; + return new Vector3(pos.x(), pos.y() + up, pos.z()); + } + + /** The phase one pilot model: mouse movement becomes aircraft rotation, the view stays on the nose. */ + private static void steerWithMouse(AircraftEntity vehicle, LocalPlayer player, CameraAngles event, + float partialTicks, boolean firstPerson) { + boolean freeLook = KeyBindings.FREE_LOOK.isDown(); + boolean ownPitch = vehicle.isHovering(); + if (!freeLook) { + if (tracking) { + float dYaw = Mth.wrapDegrees(player.getYRot() - lastYaw); + float dPitch = player.getXRot() - lastPitch; + vehicle.applyPilotLook(dYaw, dPitch); + HudRenderer.onPilotLook(dYaw, dPitch); + } + lockView(player, vehicle.getYRot(), vehicle.yRotO, ownPitch ? null : vehicle.getXRot(), vehicle.xRotO); + lastYaw = player.getYRot(); + lastPitch = player.getXRot(); + tracking = true; + } else { + // Re-acquire the nose smoothly once free look is released. + tracking = false; + } + if (firstPerson && !freeLook) { + event.setYaw(vehicle.getYRot(partialTicks)); + if (!ownPitch) { + event.setPitch(vehicle.getXRot(partialTicks)); + } + } + } + + /** Seat limits for everyone who does not fly with the mouse. */ + private static void constrainView(AircraftEntity vehicle, AircraftDefinition def, SeatDefinition seat, int index, LocalPlayer player) { + boolean freeLookHeld = KeyBindings.FREE_LOOK.isDown(); + if (seat.fixedRotation() && !freeLookHeld) { + lockView(player, vehicle.getYRot() + seat.fixedYaw(), vehicle.yRotO + seat.fixedYaw(), + vehicle.getXRot() + seat.fixedPitch(), vehicle.xRotO + seat.fixedPitch()); + return; + } + boolean pilot = index == 0; + if (pilot && vehicle instanceof TankEntity && !isFreeLook(vehicle)) { + lockView(player, vehicle.getYRot(), vehicle.yRotO, vehicle.getXRot(), vehicle.xRotO); + return; + } + float min; + float max; + if (pilot && (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity)) { + min = def.minRotationPitch; + max = def.maxRotationPitch; + } else if (seat.gunner() || (pilot && vehicle.isGunnerMode())) { + min = seat.minPitch(); + max = seat.maxPitch(); + } else { + return; + } + // Limits are relative to the hull's tilt along the view direction. + double rel = Math.toRadians(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot())); + float tilt = (float) (vehicle.getXRot() * Math.cos(rel) + vehicle.roll() * Math.sin(rel)); + float pitch = Mth.clamp(player.getXRot(), tilt + min, tilt + max); + pitch = Mth.clamp(pitch, -90.0F, 90.0F); + if (pitch != player.getXRot()) { + player.setXRot(pitch); + player.xRotO = pitch; + } + } + + private static void lockView(LocalPlayer player, float yaw, float yawO, @Nullable Float pitch, float pitchO) { + player.setYRot(yaw); + player.yRotO = yawO; + player.setYHeadRot(yaw); + player.setYBodyRot(yaw); + if (pitch != null) { + player.setXRot(pitch); + player.xRotO = pitchO; + } + } + + /** + * Where the first person eye goes, in hull space, or {@code null} for the normal eye. Mirrors + * the original's choice between the pilot's camera positions and a gunner seat's sight. + */ + private static @Nullable Vector3 eyePosition(AircraftEntity vehicle, AircraftDefinition def, int seat) { + SeatDefinition s = def.seats.get(seat); + boolean sight = s.gunner() && (!s.switchGunner() || gunnerSight); + CameraPosition camera = null; + if (seat == 0) { + if (cameraIndex > 0 && cameraIndex < def.cameraPositions.size()) { + camera = def.cameraPositions.get(cameraIndex); + } else if (def.uav || def.alwaysCameraView || sight || vehicle.isGunnerMode() || vehicle instanceof GroundVehicleEntity) { + camera = def.cameraPositions.getFirst(); + } + } else if (sight) { + camera = s.camera() != null && s.camera() != CameraPosition.DEFAULT ? s.camera() : def.cameraPositions.getFirst(); + } + // A camera at the origin means the file never set one; the normal eye is better there. + if (camera == null || camera.position().equals(Vector3.ZERO)) { + return null; + } + return camera.position(); + } + + private static void moveCamera(Camera camera, AircraftEntity vehicle, AircraftDefinition def, Vector3 eye, int seat, float partialTicks) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + SeatDefinition s = def.seats.get(seat); + Vector3 local = s.rotatingSeat() ? vehicle.onTurret(def, eye, partialTicks) : eye; + Vector3f offset = AircraftRotation.toWorld(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), vehicle.roll(partialTicks), + local.x(), local.y(), local.z()); + placeCamera(camera, new Vec3(Mth.lerp(partialTicks, vehicle.xo, vehicle.getX()) + offset.x, + Mth.lerp(partialTicks, vehicle.yo, vehicle.getY()) + offset.y, + Mth.lerp(partialTicks, vehicle.zo, vehicle.getZ()) + offset.z)); + } + + /** + * The TV missile this seat is flying, whose nose camera replaces the eye. Like the original, + * only while the seat has a TV missile selected in its guided mode. + */ + static @Nullable TvMissileEntity tvCamera(AircraftEntity vehicle, int seat) { + TvMissileEntity missile = vehicle.tvMissile(); + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + if (missile == null || set == null || set.definition().type != WeaponType.TV_MISSILE || set.mode() != 0) { + return null; + } + return missile; + } + + static void placeCamera(Camera camera, Vec3 at) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + try { + setter.invoke(camera, at); + } catch (Throwable t) { + MCHeli.LOGGER.warn("Could not move the camera; the first person eye stays where vanilla puts it", t); + setCameraPosition = null; + cameraLookupFailed = true; + } + } + + private static Method accessible(Method method) { + method.setAccessible(true); + return method; + } + + /** {@code Camera.setPosition} is protected. Looked up once; on failure the feature turns itself off. */ + private static @Nullable MethodHandle cameraSetter() { + if (setCameraPosition == null && !cameraLookupFailed) { + try { + setCameraPosition = MethodHandles.lookup().unreflect( + accessible(Camera.class.getDeclaredMethod("setPosition", Vec3.class))); + } catch (Exception e) { + MCHeli.LOGGER.warn("Camera.setPosition is not accessible; the first person eye stays where vanilla puts it", e); + cameraLookupFailed = true; + } + } + return setCameraPosition; + } +} From 8f3c06e1a82888e6b57aae171f42393370362a97 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:19 +0800 Subject: [PATCH 0431/1461] Add client/GltdController.java to the 26.1 shared sources --- .../mcheli/client/GltdController.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/GltdController.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/GltdController.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/GltdController.java new file mode 100644 index 00000000..257c0115 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/GltdController.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.network.EquipmentActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +/** + * The local player at a GLTD, {@code MCH_ClientGLTDTickHandler} and {@code MCH_GuiGLTD}: the + * view moves to the sight, the zoom and camera mode keys work as in a vehicle's sight, the use + * key calls the A-10 on the spot under the cross, and a small readout says when it can be called + * again. + */ +final class GltdController { + /** The original zoomed smoothly up to ten; the vehicle sights double each press. */ + private static final int MAX_ZOOM = 8; + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int CROSS = 0xC0FF4040; + + /** Edge of the use key, so holding it calls one strike rather than one per tick. */ + private static boolean useWasDown; + + private GltdController() { + } + + static @Nullable GltdEntity gltdOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof GltdEntity gltd ? gltd : null; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + GltdEntity gltd = gltdOf(player); + if (gltd == null) { + useWasDown = false; + return; + } + while (KeyBindings.FOLD.consumeClick()) { + VehicleCamera.zoomStep(player, MAX_ZOOM); + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + VehicleCamera.nextMode(player, on -> send(gltd, + on ? EquipmentActionPacket.Action.NIGHT_VISION_ON : EquipmentActionPacket.Action.NIGHT_VISION_OFF)); + } + boolean useDown = mc.screen == null && mc.options.keyUse.isDown(); + if (useDown && !useWasDown) { + send(gltd, EquipmentActionPacket.Action.CALL_STRIKE); + } + useWasDown = useDown; + } + + /** The use key belongs to the sight while sitting at a GLTD. */ + static boolean onUseItem() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + /** First person looks out of the sight rather than from behind the tripod. */ + static void onCameraAngles(CameraAngles angles) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd != null && mc.options.getCameraType().isFirstPerson()) { + CrewController.placeCamera(angles.camera(), gltd.sightPosition(angles.partialTick())); + } + } + + static boolean hidesHand() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + static void drawHud(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd == null || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int cx = g.guiWidth() / 2; + int cy = g.guiHeight() / 2; + g.fill(cx - 12, cy, cx - 3, cy + 1, CROSS); + g.fill(cx + 4, cy, cx + 13, cy + 1, CROSS); + g.fill(cx, cy - 12, cx + 1, cy - 3, CROSS); + g.fill(cx, cy + 4, cx + 1, cy + 13, CROSS); + + Font font = mc.font; + int x = cx + 20; + int y = cy + 20; + int wait = gltd.waitTicks(); + if (wait > 0) { + g.text(font, Component.translatable("hud.mcheli.gltd.wait", (wait + 19) / 20), x, y, AMBER); + } else { + g.text(font, Component.translatable("hud.mcheli.gltd.ready"), x, y, WHITE); + } + g.text(font, Component.translatable("hud.mcheli.zoom", Math.round(VehicleCamera.zoom())), x, y + 10, WHITE); + } + + private static void send(GltdEntity gltd, EquipmentActionPacket.Action action) { + ModNetwork.sendToServer(new EquipmentActionPacket(gltd.getId(), action)); + } +} From 5f4bb4f4813692dfdbbb383cca0f760870e1c824 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:19 +0800 Subject: [PATCH 0432/1461] Add client/KeyBindings.java to the 26.1 shared sources --- .../mcheli/client/KeyBindings.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java new file mode 100644 index 00000000..111759ac --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.KeyMapping; +import net.minecraft.resources.Identifier; + +import java.util.List; + +/** + * Mod key bindings. Throttle, steering, hover, brake and eject reuse the vanilla movement and jump + * keys (W/S, A/D and Space by default), which matches the original defaults. Firing uses the + * vanilla use key and the pick block button switches weapons, the right and middle mouse buttons + * of the original. The rest keep the original's letters where vanilla leaves them free. + */ +public final class KeyBindings { + public static final KeyMapping.Category CATEGORY = KeyMapping.Category.register(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "main")); + + /** Hold to look around without steering. The original used Left Ctrl, which now clashes with sprint. */ + public static final KeyMapping FREE_LOOK = new KeyMapping("key.mcheli.free_look", InputConstants.KEY_LALT, CATEGORY); + /** Cycles the pilot's camera positions, or toggles the gunner sight in seats that allow it. */ + public static final KeyMapping SWITCH_CAMERA = new KeyMapping("key.mcheli.switch_camera", InputConstants.KEY_H, CATEGORY); + public static final KeyMapping LANDING_GEAR = new KeyMapping("key.mcheli.landing_gear", InputConstants.KEY_B, CATEGORY); + /** Zooms through a sight, as the original Z key did; otherwise folds hatches and wings, sweeps variable geometry wings. */ + public static final KeyMapping FOLD = new KeyMapping("key.mcheli.fold", InputConstants.KEY_Z, CATEGORY); + /** The original used F, which vanilla now uses to swap hands. */ + public static final KeyMapping VTOL = new KeyMapping("key.mcheli.vtol", InputConstants.KEY_V, CATEGORY); + public static final KeyMapping SWITCH_SEAT = new KeyMapping("key.mcheli.switch_seat", InputConstants.KEY_R, CATEGORY); + public static final KeyMapping RACK_LOAD = new KeyMapping("key.mcheli.rack_load", InputConstants.KEY_J, CATEGORY); + public static final KeyMapping RACK_UNLOAD = new KeyMapping("key.mcheli.rack_unload", InputConstants.KEY_U, CATEGORY); + /** The second weapon switch key of the original; the first is the vanilla pick block button. */ + public static final KeyMapping SWITCH_WEAPON = new KeyMapping("key.mcheli.switch_weapon", InputConstants.KEY_G, CATEGORY); + public static final KeyMapping WEAPON_MODE = new KeyMapping("key.mcheli.weapon_mode", InputConstants.KEY_X, CATEGORY); + /** The original used V, which VTOL took over. */ + public static final KeyMapping FLARE = new KeyMapping("key.mcheli.flare", InputConstants.KEY_C, CATEGORY); + /** Fuel and parachute slots, reloading and the cargo chest. The original used R, which switches seats here. */ + public static final KeyMapping VEHICLE_SCREEN = new KeyMapping("key.mcheli.vehicle_screen", InputConstants.KEY_I, CATEGORY); + /** Night vision, thermal view and back in vehicles with {@code EnableNightVision}. The original used C, now the flare key. */ + public static final KeyMapping CAMERA_MODE = new KeyMapping("key.mcheli.camera_mode", InputConstants.KEY_K, CATEGORY); + /** The pilot lets the troops down; anyone else aboard slides down a rope or jumps. The original used Y for the pilot's part. */ + public static final KeyMapping DROP_CREW = new KeyMapping("key.mcheli.drop_crew", InputConstants.KEY_Y, CATEGORY); + /** Operators open the team manager, as the original's multiplayer manager key M did. */ + public static final KeyMapping TEAM_MANAGER = new KeyMapping("key.mcheli.team_manager", InputConstants.KEY_M, CATEGORY); + + public static final List ALL = List.of(FREE_LOOK, SWITCH_CAMERA, LANDING_GEAR, FOLD, VTOL, SWITCH_SEAT, RACK_LOAD, RACK_UNLOAD, + SWITCH_WEAPON, WEAPON_MODE, FLARE, VEHICLE_SCREEN, CAMERA_MODE, DROP_CREW, TEAM_MANAGER); + + private KeyBindings() { + } +} From 042a7af0577c64838d99bd7774a7fd6d953ee7c7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:20 +0800 Subject: [PATCH 0433/1461] Add client/LauncherController.java to the 26.1 shared sources --- .../mcheli/client/LauncherController.java | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java new file mode 100644 index 00000000..5a57fc58 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java @@ -0,0 +1,238 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.network.LauncherPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The Stinger and Javelin in the player's hands, {@code MCH_ClientLightWeaponTickHandler} and + * {@code MCH_GuiLightWeapon}. Raised, the launcher's sight covers the view and its guidance locks + * onto what the cross holds; the attack key fires once the lock is complete. Z steps the zoom, X + * the Javelin's direct and top attack modes. An empty launcher held down reloads itself from a + * missile in the inventory. + */ +final class LauncherController { + /** Ticks of raising before the sight and the lock start, as in the original. */ + private static final int RAISE_TICKS = 10; + private static final int RELOAD_TICKS = 40; + /** Top attack climbs first and needs this much room, measured flat. */ + private static final double TOP_ATTACK_MIN_DISTANCE = 40.0; + private static final int GREEN = 0xFF2AD848; + private static final int RED = 0xFFDF0808; + private static final int WHITE = 0xFFFFFFFF; + + private static @Nullable String weapon; + private static @Nullable Guidance guidance; + private static int zoomIndex; + private static int mode; + private static int reloadTicks; + private static int lockSoundTicks; + private static boolean attackWasDown; + + private LauncherController() { + } + + private static ItemStack launcher(@Nullable LocalPlayer player) { + if (player == null || !LauncherItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getItemInHand(InteractionHand.MAIN_HAND); + return stack.getItem() instanceof LauncherItem ? stack : ItemStack.EMPTY; + } + + /** Whether the player holds a launcher up to the eye. */ + static boolean isRaised() { + LocalPlayer player = Minecraft.getInstance().player; + ItemStack stack = launcher(player); + return !stack.isEmpty() && player.isUsingItem() && player.getUseItem() == stack && player.getTicksUsingItem() > RAISE_TICKS; + } + + private static @Nullable WeaponDefinition definition(ItemStack stack) { + return MCHeli.content().weapon(((LauncherItem) stack.getItem()).weapon()); + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = launcher(player); + WeaponDefinition def = stack.isEmpty() ? null : definition(stack); + if (def == null) { + weapon = null; + guidance = null; + reloadTicks = 0; + attackWasDown = false; + return; + } + String name = ((LauncherItem) stack.getItem()).weapon(); + if (!name.equals(weapon) || guidance == null) { + weapon = name; + boolean air = def.type != WeaponType.AT_MISSILE; + guidance = new Guidance(air, !air, def.ridableOnly, def.lockTime, e -> e != player && !e.isAlliedTo(player)); + zoomIndex = 0; + mode = 0; + } + boolean raised = isRaised(); + boolean attackDown = raised && mc.screen == null && mc.options.keyAttack.isDown(); + boolean attackPressed = attackDown && !attackWasDown; + attackWasDown = attackDown; + + if (LauncherItem.isLoaded(stack)) { + reloadTicks = 0; + if (raised) { + if (guidance.lock(player, player.getEyePosition())) { + lockSoundTicks = 0; + } + playLockSound(player); + } else { + guidance.clear(); + } + } else { + guidance.clear(); + reload(player, stack); + } + guidance.tick(); + + if (!raised) { + return; + } + while (KeyBindings.FOLD.consumeClick()) { + if (def.zoom.length > 1) { + zoomIndex = (zoomIndex + 1) % def.zoom.length; + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + if (def.modeNum > 1) { + mode = (mode + 1) % def.modeNum; + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 0.9F); + } + } + if (attackPressed) { + Entity target = guidance.lastLocked(); + if (LauncherItem.isLoaded(stack) && guidance.isLocked() && target != null && canFire(player, target)) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.FIRE, target.getId(), mode)); + guidance.clear(); + } else { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + } + + /** Top attack needs room to climb and dive. */ + private static boolean canFire(LocalPlayer player, @Nullable Entity target) { + if (mode == 0 || target == null) { + return true; + } + double dx = target.getX() - player.getX(); + double dz = target.getZ() - player.getZ(); + return Math.sqrt(dx * dx + dz * dz) >= TOP_ATTACK_MIN_DISTANCE; + } + + /** The tone quickens as the lock builds. */ + private static void playLockSound(LocalPlayer player) { + if (guidance == null || guidance.lockCount() <= 0) { + lockSoundTicks = 0; + return; + } + if (lockSoundTicks > 0) { + lockSoundTicks--; + return; + } + lockSoundTicks = Math.max(2, (int) (7 * (1.0 - (double) guidance.lockCount() / guidance.lockCountMax()))); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + + /** Held down and empty, with a missile to hand: two seconds to load it. */ + private static void reload(LocalPlayer player, ItemStack stack) { + LauncherItem item = (LauncherItem) stack.getItem(); + boolean hasMissile = player.getAbilities().instabuild || player.getInventory().findSlotMatchingItem(new ItemStack(item.missile())) >= 0; + if (!hasMissile || player.isUsingItem()) { + reloadTicks = 0; + return; + } + reloadTicks++; + if (reloadTicks == 10) { + player.level().playLocalSound(player, ModSounds.get("fim92_reload"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (reloadTicks == RELOAD_TICKS) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.RELOAD, 0, 0)); + } + } + + static float fov(float fov) { + if (!isRaised()) { + return fov; + } + WeaponDefinition def = definition(launcher(Minecraft.getInstance().player)); + if (def == null || def.zoom.length == 0) { + return fov; + } + float zoom = def.zoom[Math.min(zoomIndex, def.zoom.length - 1)]; + return zoom > 1.0F ? fov / zoom : fov; + } + + static void drawHud(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (!mc.options.getCameraType().isFirstPerson()) { + return; + } + ItemStack stack = launcher(player); + if (stack.isEmpty()) { + return; + } + Font font = mc.font; + if (!isRaised()) { + if (!LauncherItem.isLoaded(stack) && reloadTicks > 0) { + g.text(font, Component.translatable("hud.mcheli.launcher.reloading"), g.guiWidth() / 2 - 30, g.guiHeight() / 2 + 20, WHITE); + } + return; + } + WeaponDefinition def = definition(stack); + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + String texture = def != null && def.type == WeaponType.AT_MISSILE ? "javelin" : "stinger"; + g.blit(RenderPipelines.GUI_TEXTURED, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + texture + ".png"), + (w - size) / 2, (h - size) / 2, 0.0F, 0.0F, size, size, 256, 256, 256, 256); + + int x = w / 2 + 20; + int y = h / 2 + 20; + if (!LauncherItem.isLoaded(stack)) { + g.text(font, Component.translatable("hud.mcheli.launcher.empty"), x, y, RED); + } else if (guidance != null && guidance.isLocked()) { + Entity target = guidance.lastLocked(); + g.text(font, Component.translatable("hud.mcheli.locked"), x, y, canFire(player, target) ? GREEN : RED); + if (target != null) { + g.text(font, String.format("[%.0f m]", target.distanceTo(player)), x, y + 10, GREEN); + } + } else if (guidance != null && guidance.lockCount() > 0) { + g.text(font, Component.translatable("hud.mcheli.locking", guidance.lockCount() * 100 / Math.max(guidance.lockCountMax(), 1)), x, y, WHITE); + } + if (def != null && def.modeNum > 1) { + g.text(font, Component.translatable(mode == 0 ? "hud.mcheli.launcher.direct" : "hud.mcheli.launcher.top_attack"), x, y + 20, WHITE); + } + if (def != null && def.zoom.length > 0) { + g.text(font, Component.translatable("hud.mcheli.zoom", String.format("%.1f", def.zoom[Math.min(zoomIndex, def.zoom.length - 1)])), + x, y + 30, WHITE); + } + } +} From 866915aa25cf39df9715f4c824a8408b40b30169 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:20 +0800 Subject: [PATCH 0434/1461] Add client/RangefinderController.java to the 26.1 shared sources --- .../mcheli/client/RangefinderController.java | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java new file mode 100644 index 00000000..80aafc99 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java @@ -0,0 +1,155 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.SpotPacket; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.Locale; + +/** + * Looking through the rangefinder, {@code MCH_ClientToolTickHandler} and {@code MCH_GuiRangeFinder}. + * Once it has been held up for a moment the scope covers the view and reads the range; the zoom + * key and the weapon mode key zoom in and out while held, the flare key picks what to spot, and + * the attack key spots. + */ +final class RangefinderController { + private static final Identifier SCOPE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/rangefinder.png"); + /** Ticks of holding it up before the scope shows, as in the original. */ + private static final int RAISE_TICKS = 8; + private static final int COOLDOWN = 60; + private static final float MIN_ZOOM = 1.2F; + private static final float MAX_ZOOM = 10.0F; + private static final double RANGE = 300.0; + private static final int GREEN = 0xFF10FF20; + private static final int RED = 0xFFFF1010; + private static final int WHITE = 0xFFFFFFFF; + private static final int GREY = 0xFF404040; + + private static float zoom = 2.0F; + private static SpotPacket.Mode mode = SpotPacket.Mode.PLAYERS_AND_VEHICLES; + private static int cooldown; + + private RangefinderController() { + } + + /** The rangefinder the player is looking through, or empty. */ + private static ItemStack scope(LocalPlayer player) { + if (player == null || !player.isUsingItem() || player.getTicksUsingItem() <= RAISE_TICKS || !RangefinderItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getUseItem(); + return stack.getItem() instanceof RangefinderItem ? stack : ItemStack.EMPTY; + } + + static boolean isScoped() { + return !scope(Minecraft.getInstance().player).isEmpty(); + } + + static void onClientTick() { + if (cooldown > 0) { + cooldown--; + } + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty()) { + zoom = 2.0F; + return; + } + if (KeyBindings.FOLD.isDown() && zoom < MAX_ZOOM) { + zoom = Math.min(zoom + zoom / 10.0F, MAX_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 1.0F); + } + if (KeyBindings.WEAPON_MODE.isDown() && zoom > MIN_ZOOM) { + zoom = Math.max(zoom - zoom / 10.0F, MIN_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 0.9F); + } + while (KeyBindings.FLARE.consumeClick()) { + mode = SpotPacket.Mode.values()[(mode.ordinal() + 1) % SpotPacket.Mode.values().length]; + // Alone in a world there is no one else to spot, as the original reasoned. + boolean alone = mc.hasSingleplayerServer() && !mc.getSingleplayerServer().isPublished(); + if (alone && mode == SpotPacket.Mode.PLAYERS_AND_VEHICLES) { + mode = SpotPacket.Mode.MOBS; + } + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 0.9F); + } + if (mc.options.keyAttack.isDown() && cooldown == 0) { + if (mode != SpotPacket.Mode.MARK_POINT && RangefinderItem.charges(stack) <= 0) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 1.0F, 1.0F); + } else { + cooldown = COOLDOWN; + ModNetwork.sendToServer(new SpotPacket(mode)); + } + } + } + + static float fov(float fov) { + return isScoped() ? fov / zoom : fov; + } + + static void drawHud(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty() || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + g.blit(RenderPipelines.GUI_TEXTURED, SCOPE, (w - size) / 2, (h - size) / 2, 0.0F, 0.0F, size, size, 256, 256, 256, 256); + + // The readouts sit on the scope texture's own labels: lay them out in its 512 wide space, + // centred, as the original did, and scale that with the scope. + float k = size / 512.0F; + g.pose().pushMatrix(); + g.pose().translate(w / 2.0F, h / 2.0F); + g.pose().scale(k, k); + drawReadouts(g, mc.font, player, stack); + g.pose().popMatrix(); + } + + /** In scope space: the centre of the view is at 0, 0. */ + private static void drawReadouts(GuiGraphicsExtractor g, Font font, LocalPlayer player, ItemStack stack) { + int charge = RangefinderItem.charges(stack) * 100 / RangefinderItem.CHARGES; + // Over BATT. + g.text(font, String.format("%3d%%", charge), -100, 55, charge > 0 ? GREEN : RED); + if (charge <= 0) { + g.text(font, Component.translatable("hud.mcheli.rangefinder.empty"), -40, 55, RED); + } else { + // Over RANGE METERS. + Vec3 eye = player.getEyePosition(); + HitResult hit = player.level().clip(new ClipContext(eye, eye.add(player.getLookAngle().scale(RANGE)), + ClipContext.Block.OUTLINE, ClipContext.Fluid.ANY, player)); + String range = hit.getType() == HitResult.Type.MISS ? "----" : String.format("%4d", Math.round(hit.getLocation().distanceTo(eye))); + g.text(font, range, 40, 55, hit.getType() == HitResult.Type.MISS ? RED : GREEN); + } + // The spot cooldown bar, and the zoom above it. + g.fill(-100, 51, -70, 53, GREEN); + g.fill(-100, 51, -100 + cooldown / 2, 53, RED); + g.text(font, String.format("x%.1f", zoom), -100, 31, WHITE); + int y = 21; + for (SpotPacket.Mode m : SpotPacket.Mode.values()) { + if (m == mode) { + g.text(font, ">", 30, y, WHITE); + } + g.text(font, Component.translatable("hud.mcheli.rangefinder." + m.name().toLowerCase(Locale.ROOT)), 40, y, m == mode ? WHITE : GREY); + y += 10; + } + } +} From c024be5e43156a387a3850ad8f1f594026db7954 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:21 +0800 Subject: [PATCH 0435/1461] Add client/RuntimeAssets.java to the 26.1 shared sources --- .../mcheli/client/RuntimeAssets.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java new file mode 100644 index 00000000..7f7afd84 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java @@ -0,0 +1,165 @@ +package com.jnhprojects.mcheli.client; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Vehicle textures, models and sounds that can change while the game runs, without a resource + * reload (F3 + T), which would stall the game for every pack loaded and is not available on a + * server at all. + * + *

Lookups try the resource manager first, so resource packs keep the last word, then the + * runtime roots: folders laid out like {@code assets/mcheli} that content can add at any time. + * Content packs and, later, the workshop register their folders here. + *

    + *
  • Textures found only in a runtime root are uploaded as a {@link DynamicTexture} the first + * time a vehicle draws them. {@link #refreshTextures} uploads every vehicle texture again + * from disk, which is how a content reload shows edited textures.
  • + *
  • Models are read through {@link #open} by the mesh cache, which a reload clears.
  • + *
  • Sounds found only in a runtime root are streamed from the file + * ({@link #soundFile}); see {@code RuntimeSoundInstance}.
  • + *
+ */ +public final class RuntimeAssets { + private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeAssets.class); + + /** Searched in order, so the pack loaded last comes first. Replaced as a whole. */ + private static volatile List roots = List.of(); + /** The pack open in the workshop, searched first whether the game has loaded it or not. */ + private static volatile @Nullable Path workshopRoot; + /** Every vehicle texture asked for, and whether we uploaded it ourselves. */ + private static final Map TEXTURES = new ConcurrentHashMap<>(); + + private RuntimeAssets() { + } + + /** + * Sets the folders laid out like {@code assets/mcheli} that are searched after the resource + * packs, the first one first. Content packs set theirs whenever the packs in use change. + */ + public static void setRoots(List folders) { + roots = List.copyOf(folders); + } + + /** Sets the {@code assets/mcheli} folder of the pack open in the workshop, or {@code null} for none. */ + public static void setWorkshopRoot(@Nullable Path folder) { + workshopRoot = folder; + } + + /** Opens an {@code mcheli} asset from the resource packs, or else from a runtime root. */ + public static @Nullable InputStream open(Identifier id) throws IOException { + Optional resource = Minecraft.getInstance().getResourceManager().getResource(id); + if (resource.isPresent()) { + return resource.get().open(); + } + Path file = find(id.getPath()); + return file != null ? Files.newInputStream(file) : null; + } + + /** + * A texture ready to bind. The first request for a texture no resource pack has, but a runtime + * root does, uploads it. Cheap after the first call, so renderers can call it every frame. + */ + public static Identifier texture(Identifier id) { + TEXTURES.computeIfAbsent(id, key -> { + if (Minecraft.getInstance().getResourceManager().getResource(key).isPresent()) { + return false; + } + return upload(key); + }); + return id; + } + + /** + * Uploads every texture vehicles have drawn so far again, read fresh from disk. Resource pack + * textures are replaced too, until the next resource reload loads them the normal way. One that + * no longer exists anywhere, because its pack is gone, is released. + */ + public static void refreshTextures() { + for (Map.Entry entry : TEXTURES.entrySet()) { + boolean ours = upload(entry.getKey()); + if (!ours && entry.getValue()) { + Minecraft.getInstance().getTextureManager().release(entry.getKey()); + } + entry.setValue(ours); + } + } + + /** Uploads one texture again, read fresh from disk: the workshop just replaced its file. */ + public static void refreshTexture(Identifier id) { + TEXTURES.put(id, upload(id)); + } + + private static boolean upload(Identifier id) { + try (InputStream in = open(id)) { + if (in == null) { + return false; + } + NativeImage image = NativeImage.read(in); + Minecraft.getInstance().getTextureManager().register(id, new DynamicTexture(id::toString, image)); + return true; + } catch (IOException e) { + LOGGER.warn("Could not load texture {}", id, e); + return false; + } + } + + /** + * Drops the textures we uploaded, so the texture manager loads them again the normal way. + * Called on resource reload. + */ + public static void clearTextures() { + TEXTURES.forEach((id, ours) -> { + if (ours) { + Minecraft.getInstance().getTextureManager().release(id); + } + }); + TEXTURES.clear(); + } + + /** + * The OGG file for a sound event of ours that no resource pack defines, such as {@code heli} + * from {@code sounds/heli.ogg} in a runtime root. Numbered variants ({@code heli1.ogg}) count. + */ + public static @Nullable Path soundFile(String event) { + Path file = find("sounds/" + event + ".ogg"); + return file != null ? file : find("sounds/" + event + "1.ogg"); + } + + private static @Nullable Path find(String path) { + Path workshop = workshopRoot; + if (workshop != null) { + Path file = inRoot(workshop, path); + if (file != null) { + return file; + } + } + for (Path root : roots) { + Path file = inRoot(root, path); + if (file != null) { + return file; + } + } + return null; + } + + private static @Nullable Path inRoot(Path root, String path) { + Path file = root.resolve(path).normalize(); + return file.startsWith(root) && Files.isRegularFile(file) ? file : null; + } +} From e9922a46b146ba89c86c8e5379a281a32880f7e7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:21 +0800 Subject: [PATCH 0436/1461] Add client/TargetMarkers.java to the 26.1 shared sources --- .../mcheli/client/TargetMarkers.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java new file mode 100644 index 00000000..eabf0bba --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java @@ -0,0 +1,87 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3fc; +import org.jspecify.annotations.Nullable; + +import java.util.Map; + +/** + * {@code MCH_GuiTargetMarker}: the team's spotted targets and marked point on screen. A target + * gets a triangle over its head with its distance; one off screen gets a diamond at the edge on + * its side. The marked point gets a diamond with its distance and coordinates. + */ +final class TargetMarkers { + private static final int TARGET = 0xFFFF4040; + private static final int POINT = 0xFFFFD000; + private static final int EDGE = 12; + + private TargetMarkers() { + } + + static void tick() { + Markers.tick(); + } + + static void draw(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || mc.player == null) { + return; + } + float partialTicks = delta.getGameTimeDeltaPartialTick(false); + for (Map.Entry spotted : Markers.spotted().entrySet()) { + Entity entity = mc.level.getEntity(spotted.getKey()); + if (entity == null || entity == mc.player || !entity.isAlive()) { + continue; + } + Vec3 top = entity.getPosition(partialTicks).add(0.0, entity.getBbHeight() + 0.5, 0.0); + mark(g, mc, top, "â–¼", TARGET, "%.0fm".formatted(mc.player.distanceTo(entity)), null); + } + BlockPos point = Markers.point(); + if (point != null) { + Vec3 at = Vec3.atCenterOf(point).add(0.0, 1.0, 0.0); + String coordinates = "%d %d %d".formatted(point.getX(), point.getY(), point.getZ()); + mark(g, mc, at, "â—†", POINT, "%.0fm".formatted(Math.sqrt(mc.player.distanceToSqr(at))), coordinates); + } + } + + private static void mark(GuiGraphicsExtractor g, Minecraft mc, Vec3 at, String symbol, int color, String distance, @Nullable String extra) { + Camera camera = mc.gameRenderer.getMainCamera(); + Vector3fc forward = camera.forwardVector(); + Vec3 offset = at.subtract(camera.position()); + boolean ahead = offset.x * forward.x() + offset.y * forward.y() + offset.z * forward.z() > 0.0; + Vec3 ndc = mc.gameRenderer.projectPointToScreen(at); + int w = g.guiWidth(); + int h = g.guiHeight(); + double x = (ndc.x + 1.0) / 2.0 * w; + double y = (1.0 - ndc.y) / 2.0 * h; + Font font = mc.font; + if (ahead && x >= 0 && x <= w && y >= 0 && y <= h) { + g.centeredText(font, symbol, (int) x, (int) y - 4, color); + g.centeredText(font, distance, (int) x, (int) y + 6, color); + if (extra != null) { + g.centeredText(font, extra, (int) x, (int) y + 16, color); + } + return; + } + // Behind or off to the side: point the way along the screen edge. + double dx = ahead ? x - w / 2.0 : w / 2.0 - x; + double dy = ahead ? y - h / 2.0 : h / 2.0 - y; + double scale = Math.max(Math.abs(dx) / (w / 2.0 - EDGE), Math.abs(dy) / (h / 2.0 - EDGE)); + if (scale <= 0.0) { + return; + } + int ex = Mth.clamp((int) (w / 2.0 + dx / scale), EDGE, w - EDGE); + int ey = Mth.clamp((int) (h / 2.0 + dy / scale), EDGE, h - EDGE); + g.centeredText(font, "â—†", ex, ey - 4, color); + } +} From 5df657dcff5697584b39e1b16d5a476933b1a55d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:22 +0800 Subject: [PATCH 0437/1461] Drive the thermal view through the single post effect slot of 26.1 --- .../mcheli/client/VehicleCamera.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java new file mode 100644 index 00000000..de7ef3c4 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java @@ -0,0 +1,127 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; + +import java.util.function.Consumer; + +/** + * The local crew member's sight, {@code MCH_Camera}: zoom for vehicles with a {@code CameraZoom} + * above one, and night vision and thermal view for those with {@code EnableNightVision}. Both + * reset whenever the player changes seat or leaves the vehicle. + */ +public final class VehicleCamera { + /** The original's camera modes; the HUD reads the ordinal as {@code cam_mode}. */ + public enum Mode { + NORMAL, + NIGHT_VISION, + /** Night vision seen through a white hot filter, the original's "pencil" shader mode. */ + THERMAL + } + + /** The post effect of the thermal view, {@code assets/mcheli/post_effect/thermal.json}. */ + private static final Identifier THERMAL_EFFECT = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "thermal"); + + private static float zoom = 1.0F; + private static Mode mode = Mode.NORMAL; + + private VehicleCamera() { + } + + public static float zoom() { + return zoom; + } + + public static Mode mode() { + return mode; + } + + /** Back to the plain view, on a seat change or after leaving the vehicle. */ + static void reset() { + zoom = 1.0F; + mode = Mode.NORMAL; + } + + /** + * Keeps the thermal filter in the player's post effects while the thermal view is on. The list + * is the one a server's post effect packet fills, so ours is put back if that replaces it. + */ + static void tick() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + return; + } + boolean active = THERMAL_EFFECT.equals(mc.gameRenderer.currentPostEffect()); + if (mode == Mode.THERMAL && !active) { + ClientHooks.postEffects().load(THERMAL_EFFECT); + } else if (mode != Mode.THERMAL && active) { + mc.gameRenderer.clearPostEffect(); + } + } + + /** + * {@code zoomCamera}: each press doubles the zoom up to the vehicle's {@code CameraZoom}, and + * the press after that goes back to one. The caller checks that the seat looks through a sight. + */ + static void onZoomKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + zoomStep(player, def != null && !vehicle.isDestroyed() ? def.cameraZoom : 1); + } + + /** One press of the zoom key on a sight that zooms up to {@code max}. */ + static void zoomStep(LocalPlayer player, int max) { + if (max <= 1) { + refuse(player); + return; + } + zoom = zoom >= max - 0.01F ? 1.0F : Math.min(zoom * 2.0F, max); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + + /** + * {@code switchCameraMode}: steps from the plain view to night vision, to thermal view and + * back. The server applies night vision as an effect in both of the last two; the thermal + * filter is drawn here. + */ + static void onCameraModeKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + if (def == null || !def.nightVision || vehicle.isDestroyed()) { + refuse(player); + return; + } + nextMode(player, on -> ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), + on ? CrewActionPacket.Action.NIGHT_VISION_ON : CrewActionPacket.Action.NIGHT_VISION_OFF))); + } + + /** + * Steps to the next camera mode on a sight that has night vision. {@code nightVision} asks + * the server to switch the effect on or off, only when that changes. + */ + static void nextMode(LocalPlayer player, Consumer nightVision) { + Mode next = Mode.values()[(mode.ordinal() + 1) % Mode.values().length]; + if (mode == Mode.NORMAL || next == Mode.NORMAL) { + nightVision.accept(next != Mode.NORMAL); + } + mode = next; + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + + /** The view's field of view, narrowed by the zoom. */ + public static float fov(float fov) { + return zoom > 1.0F ? fov / zoom : fov; + } + + /** {@code playSoundNG}: the key does nothing in this seat. */ + private static void refuse(LocalPlayer player) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } +} From 0268db721fd0bba9b604e8e0081a47cca5cd2825 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:22 +0800 Subject: [PATCH 0438/1461] Add client/VehiclePicker.java to the 26.1 shared sources --- .../mcheli/client/VehiclePicker.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java new file mode 100644 index 00000000..9507412f --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.BoardVehiclePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Right-clicking anywhere on a vehicle's hull boards it. + * + *

Vanilla only picks an entity by its main hit box, which for most MCHeli vehicles is the + * default two by 0.7 block box at their feet; the hull's real shape lives in its extra + * {@code BoundingBox} lines. In the original those boxes were clickable too. Inflating the main box + * instead would change how the vehicle collides with terrain, so the use key looks for them here. + */ +final class VehiclePicker { + private VehiclePicker() { + } + + static boolean onUseItem(InteractionHand hand) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || hand != InteractionHand.MAIN_HAND || player.getVehicle() instanceof AircraftEntity) { + return false; + } + // Vanilla already handles a click on the main box. + if (mc.hitResult instanceof EntityHitResult hit && hit.getEntity() instanceof AircraftEntity) { + return false; + } + Hit hit = pick(player, mc.hitResult); + if (hit == null) { + return false; + } + ModNetwork.sendToServer(new BoardVehiclePacket(hit.vehicle.getId(), hit.location)); + return true; + } + + private record Hit(AircraftEntity vehicle, Vec3 location) { + } + + /** The nearest vehicle hull along the view, closer than the block or entity vanilla picked. */ + private static @Nullable Hit pick(Player player, @Nullable HitResult vanilla) { + double range = player.entityInteractionRange(); + Vec3 eye = player.getEyePosition(); + Vec3 to = eye.add(player.getViewVector(1.0F).scale(range)); + double limit = vanilla != null && vanilla.getType() != HitResult.Type.MISS ? eye.distanceToSqr(vanilla.getLocation()) : range * range; + Hit best = null; + for (AircraftEntity vehicle : player.level().getEntitiesOfClass(AircraftEntity.class, new AABB(eye, to).inflate(16.0))) { + Optional at = vehicle.clipHitBoxes(eye, to, 0.2); + vehicle.consumeHitBoxFactor(); + if (at.isPresent() && eye.distanceToSqr(at.get()) < limit) { + limit = eye.distanceToSqr(at.get()); + best = new Hit(vehicle, at.get()); + } + } + return best; + } +} From cc044bccb62362fb058a3b1f98681f29118e645d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:22 +0800 Subject: [PATCH 0439/1461] Add client/WeaponController.java to the 26.1 shared sources --- .../mcheli/client/WeaponController.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java new file mode 100644 index 00000000..c0ab2443 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.FireWeaponPacket; +import com.jnhprojects.mcheli.network.LockWarningPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The crew member's trigger: fires, switches weapons and modes, drops flares and resupplies. + * + *

Like the original, the client fires first. Its copy of the weapon decides whether the shot + * can happen (ready, loaded, locked on, a target in sight) and works out the option values; only + * then is the server asked, which fires the real projectile. + */ +final class WeaponController { + private WeaponController() { + } + + private record Seat(AircraftEntity vehicle, VehicleWeapons weapons, int seat) { + } + + private static @Nullable Seat seat(@Nullable LocalPlayer player) { + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle) || vehicle.definition() == null) { + return null; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (weapons == null || seat < 0 || seat >= vehicle.definition().seatCount()) { + return null; + } + return new Seat(vehicle, weapons, seat); + } + + /** The use key is the trigger while the seat has a weapon, so it must not also use items. */ + static boolean onUseItem() { + Seat s = seat(Minecraft.getInstance().player); + return s != null && s.weapons.selected(s.seat) >= 0; + } + + static boolean onPickBlock() { + Seat s = seat(Minecraft.getInstance().player); + if (s == null || s.weapons.sets().isEmpty()) { + return false; + } + send(s.vehicle, Minecraft.getInstance().player != null && Minecraft.getInstance().player.isShiftKeyDown() + ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + return true; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + Seat s = seat(player); + if (s == null) { + return; + } + while (KeyBindings.SWITCH_WEAPON.consumeClick()) { + send(s.vehicle, player.isShiftKeyDown() ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + WeaponSet set = s.weapons.current(s.seat); + if (set != null && set.switchMode()) { + send(s.vehicle, CrewActionPacket.Action.WEAPON_MODE); + click(); + } + } + while (KeyBindings.FLARE.consumeClick()) { + if (s.seat <= 1 && s.weapons.flares.use()) { + send(s.vehicle, CrewActionPacket.Action.FLARE); + click(); + } + } + + if (mc.screen == null && mc.options.keyUse.isDown() && !s.vehicle.isDestroyed()) { + fire(s, player); + } + } + + private static void fire(Seat s, LocalPlayer player) { + int id = s.weapons.selected(s.seat); + if (id < 0 || s.weapons.operatorSeat(id) != s.seat) { + return; + } + Shot shot = new Shot(s.vehicle, player); + boolean fired = s.weapons.use(player, s.seat, shot, (set, mount, sh) -> clientShot(s, id, set, sh, player)); + if (fired) { + ModNetwork.sendToServer(new FireWeaponPacket(s.vehicle.getId(), id, shot.option1, shot.option2)); + } + } + + /** The client half of each weapon's {@code shot}: can it fire, and with which options. */ + private static boolean clientShot(Seat s, int id, WeaponSet set, Shot shot, LocalPlayer player) { + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_2, ROCKET, MARKER_ROCKET -> { + shot.option1 = set.mode(); + yield true; + } + case AA_MISSILE, AT_MISSILE -> { + Guidance g = s.weapons.guidance(id); + if (g == null) { + yield false; + } + boolean locked = g.lock(player, viewPoint(player)); + if (g.shouldNotifyTarget() && g.target() != null) { + ModNetwork.sendToServer(new LockWarningPacket(g.target().getId())); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (!locked || g.lastLocked() == null) { + yield false; + } + shot.option1 = g.lastLocked().getId(); + shot.option2 = set.mode(); + yield true; + } + case TV_MISSILE -> { + shot.option1 = set.mode(); + shot.option2 = 0; + yield true; + } + case AS_MISSILE -> landAhead(s.vehicle, player, 1.62, 200.0, false) != null; + case CAS -> landAhead(s.vehicle, player, 2.0, 80.0, false) != null; + case TORPEDO -> !w.guidedTorpedo || landAhead(player, player, 0.0, 100.0, true) != null; + case DUMMY, SMOKE -> false; + case null -> false; + default -> true; + }; + } + + /** + * Where the player sees from: in first person the camera, which CrewController moves to gunner + * sights and launcher cameras; otherwise the player's eyes. + */ + private static Vec3 viewPoint(LocalPlayer player) { + Minecraft mc = Minecraft.getInstance(); + return mc.options.getCameraType().isFirstPerson() ? mc.gameRenderer.getMainCamera().position() : player.getEyePosition(); + } + + /** A block along the player's view: dry ground for strike weapons, water for guided torpedoes. */ + private static @Nullable BlockHitResult landAhead(Entity origin, LocalPlayer player, double eyeHeight, double range, boolean water) { + Vec3 from = origin.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(Vec3.directionFromRotation(player.getXRot(), player.getYRot()).scale(range)); + BlockHitResult hit = player.level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, player)); + if (hit.getType() != HitResult.Type.BLOCK) { + return null; + } + boolean wet = false; + BlockPos pos = hit.getBlockPos(); + for (Direction d : Direction.values()) { + wet |= player.level().getFluidState(pos.relative(d)).is(FluidTags.WATER); + } + return wet == water ? hit : null; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + private static void click() { + LocalPlayer player = Minecraft.getInstance().player; + if (player != null) { + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + } +} From dfe89fab7051dd1ea0d9ea07e41338622d4d23f2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:23 +0800 Subject: [PATCH 0440/1461] Add client/WeaponHud.java to the 26.1 shared sources --- .../jnhprojects/mcheli/client/WeaponHud.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java new file mode 100644 index 00000000..c742003d --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java @@ -0,0 +1,123 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; + +/** + * The vehicle HUD layer. In first person it runs the seat's HUD script (the {@code HUD} line of + * the definition, or the kind's default), or {@code tv_missile} while flying a TV missile, as the + * original did; a seat whose HUD is {@code none} gets nothing. Where no script draws (third + * person, a destroyed vehicle, a script that does not exist) it shows a plain text readout of hit + * points, ammunition, reload, heat, lock and flares. + */ +final class WeaponHud { + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int RED = 0xFFFF5050; + private static final int GREEN = 0xFF60FF60; + private static final String NO_HUD = "none"; + + private WeaponHud() { + } + + /** A pilot in gunner mode gets the gunner seat's HUD, as in the original. */ + private static int hudSeat(AircraftEntity vehicle, int seat) { + return seat == 0 && vehicle.isGunnerMode() ? 1 : seat; + } + + static void draw(GuiGraphicsExtractor g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + AircraftDefinition def = vehicle.definition(); + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (def == null || weapons == null || seat < 0 || seat >= def.seatCount()) { + return; + } + if (mc.options.getCameraType().isFirstPerson() && !vehicle.isDestroyed()) { + String hud = CrewController.tvCamera(vehicle, seat) != null ? "tv_missile" + : hudSeat(vehicle, seat) < def.huds.size() ? def.huds.get(hudSeat(vehicle, seat)) : null; + if (NO_HUD.equals(hud)) { + // The original's way of saying a seat has no HUD at all. + return; + } + if (hud != null && HudRenderer.draw(g, vehicle, def, player, seat, hud, delta.getGameTimeDeltaPartialTick(false))) { + return; + } + } + Font font = mc.font; + int x = g.guiWidth() - 160; + int y = g.guiHeight() - 92; + + float hp = (float) vehicle.hp() / def.maxHp; + int hpColor = vehicle.isDestroyed() ? RED : hp < 0.5F ? AMBER : GREEN; + g.text(font, Component.translatable("hud.mcheli.hp", vehicle.hp(), def.maxHp), x, y, hpColor); + y += 11; + + WeaponSet set = weapons.current(seat); + if (set != null) { + g.text(font, set.displayName(), x, y, WHITE); + y += 11; + Component ammo = vehicle.hasInfiniteAmmo(player) && set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo_infinite", set.ammo()) + : set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo", set.ammo(), set.restAmmo()) + : Component.translatable("hud.mcheli.ammo_none"); + g.text(font, ammo, x, y, set.ammo() == 0 && set.magazineSize() > 0 ? RED : WHITE); + y += 11; + Component status = null; + int color = AMBER; + if (set.isReloading()) { + status = Component.translatable("hud.mcheli.reloading"); + } else if (set.isOverheated()) { + status = Component.translatable("hud.mcheli.overheat"); + color = RED; + } else if (set.isInPreparation()) { + status = Component.translatable("hud.mcheli.preparing"); + } else if (set.magazineSize() > 0 && set.ammo() == 0 && set.restAmmo() == 0) { + status = Component.translatable("hud.mcheli.resupply", KeyBindings.VEHICLE_SCREEN.getTranslatedKeyMessage()); + color = RED; + } + Guidance lock = weapons.guidance(weapons.selected(seat)); + if (status == null && lock != null && lock.lockCount() > 0) { + status = lock.isLocked() + ? Component.translatable("hud.mcheli.locked") + : Component.translatable("hud.mcheli.locking", lock.lockCount() * 100 / Math.max(lock.lockCountMax(), 1)); + color = lock.isLocked() ? RED : AMBER; + } + if (status != null) { + g.text(font, status, x, y, color); + y += 11; + } + if (set.definition().maxHeatCount > 0) { + int width = 100; + int filled = Math.min(width, set.currentHeat * width / set.definition().maxHeatCount); + g.fill(x, y + 2, x + width, y + 5, 0x80000000); + g.fill(x, y + 2, x + filled, y + 5, set.isOverheated() ? RED : AMBER); + y += 8; + } + } else if (!weapons.sets().isEmpty()) { + g.text(font, Component.translatable("hud.mcheli.no_weapon"), x, y, WHITE); + y += 11; + } + if (seat <= 1 && weapons.flares.isFitted()) { + Component flare = vehicle.isFlareActive() ? Component.translatable("hud.mcheli.flare_active") + : weapons.flares.isPreparing() ? Component.translatable("hud.mcheli.flare_wait") + : Component.translatable("hud.mcheli.flare_ready", KeyBindings.FLARE.getTranslatedKeyMessage()); + g.text(font, flare, x, y, vehicle.isFlareActive() ? GREEN : WHITE); + } + } +} From e569b5172112902885f14e703dec3639f0752d9b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:23 +0800 Subject: [PATCH 0441/1461] Add client/hud/HudCanvas.java to the 26.1 shared sources --- .../mcheli/client/hud/HudCanvas.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java new file mode 100644 index 00000000..d45c753b --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java @@ -0,0 +1,165 @@ +package com.jnhprojects.mcheli.client.hud; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import org.joml.Matrix3x2fStack; + +/** + * The drawing primitives of {@code MCH_HudItem}, on the 26.x GUI. + * + *

The original drew GL lines, points and quads at fractional GUI coordinates. The GUI now only + * takes whole numbers, but every element keeps a copy of the pose, so each primitive is a unit + * square stretched and turned into place. Lines are one GUI unit thick, which is what + * {@code glLineWidth(scaleFactor)} gave; blips are two units square. + */ +final class HudCanvas { + private static final float LINE_WIDTH = 1.0F; + private static final float POINT_SIZE = 2.0F; + + private final GuiGraphicsExtractor g; + private final Font font; + + HudCanvas(GuiGraphicsExtractor g, Font font) { + this.g = g; + this.font = font; + } + + Matrix3x2fStack pose() { + return g.pose(); + } + + /** Turns everything drawn until the matching {@code pop} by {@code degrees} about a point. */ + void pushRotation(double x, double y, float degrees) { + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) x, (float) y); + pose.rotate((float) Math.toRadians(degrees)); + pose.translate((float) -x, (float) -y); + } + + void pop() { + g.pose().popMatrix(); + } + + void line(double x0, double y0, double x1, double y1, int color) { + double dx = x1 - x0; + double dy = y1 - y0; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + return; + } + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) x0, (float) y0); + pose.rotate((float) Math.atan2(dy, dx)); + pose.translate(0.0F, -LINE_WIDTH / 2.0F); + pose.scale((float) length, LINE_WIDTH); + g.fill(0, 0, 1, 1, color); + pose.popMatrix(); + } + + /** {@code GL_LINES}: x, y pairs taken two points at a time. */ + void lines(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 4) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_STRIP}. */ + void strip(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 2) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_LOOP}. */ + void loop(double[] p, int color) { + strip(p, color); + if (p.length >= 4) { + line(p[p.length - 2], p[p.length - 1], p[0], p[1], color); + } + } + + /** + * {@code GL_LINES} with {@code glLineStipple(factor, pattern)}: each segment starts the 16 bit + * pattern afresh, lowest bit first, every bit covering {@code factor} GUI units. + */ + void stippled(double[] p, int color, int factor, int pattern) { + double step = Math.max(factor, 1); + for (int i = 0; i + 3 < p.length; i += 4) { + double dx = p[i + 2] - p[i]; + double dy = p[i + 3] - p[i + 1]; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + continue; + } + double ux = dx / length; + double uy = dy / length; + double runStart = -1.0; + int bit = 0; + for (double t = 0.0; t < length; t += step, bit = (bit + 1) & 15) { + boolean on = (pattern >> bit & 1) != 0; + if (on && runStart < 0.0) { + runStart = t; + } else if (!on && runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i] + ux * t, p[i + 1] + uy * t, color); + runStart = -1.0; + } + } + if (runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i + 2], p[i + 3], color); + } + } + } + + void rect(double x0, double y0, double x1, double y1, int color) { + double left = Math.min(x0, x1); + double top = Math.min(y0, y1); + double width = Math.abs(x1 - x0); + double height = Math.abs(y1 - y0); + if (width <= 0.0 || height <= 0.0) { + return; + } + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) left, (float) top); + pose.scale((float) width, (float) height); + g.fill(0, 0, 1, 1, color); + pose.popMatrix(); + } + + void point(double x, double y, int color) { + rect(x - POINT_SIZE / 2.0, y - POINT_SIZE / 2.0, x + POINT_SIZE / 2.0, y + POINT_SIZE / 2.0, color); + } + + /** + * {@code drawTexture}: texels {@code u, v, uWidth, vHeight} of a {@code textureWidth} by + * {@code textureHeight} image, drawn into a box and turned about its centre. Drawn untinted, + * as the original reset the colour to white first. + */ + void texture(Identifier texture, double left, double top, double width, double height, double u, double v, + double uWidth, double vHeight, float rotation, int textureWidth, int textureHeight) { + if (width == 0.0 || height == 0.0) { + return; + } + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) (left + width / 2.0), (float) (top + height / 2.0)); + pose.rotate((float) Math.toRadians(rotation)); + pose.translate((float) (-width / 2.0), (float) (-height / 2.0)); + pose.scale((float) width, (float) height); + g.blit(texture, 0, 0, 1, 1, + (float) (u / textureWidth), (float) ((u + uWidth) / textureWidth), + (float) (v / textureHeight), (float) ((v + vHeight) / textureHeight)); + pose.popMatrix(); + } + + void text(String text, int x, int y, int color, boolean centered) { + if (centered) { + g.centeredText(font, text, x, y, color); + } else { + g.text(font, text, x, y, color); + } + } +} From 9a2e3617d200e877b2507814346655e0630ff578 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:24 +0800 Subject: [PATCH 0442/1461] Add client/hud/HudRenderer.java to the 26.1 shared sources --- .../mcheli/client/hud/HudRenderer.java | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java new file mode 100644 index 00000000..0115b4c3 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java @@ -0,0 +1,354 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.hud.Expression; +import com.jnhprojects.mcheli.hud.HudCommand; +import com.jnhprojects.mcheli.hud.HudScript; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.IllegalFormatException; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Runs HUD scripts, the drawing half of {@code MCH_Hud} and the {@code MCH_HudItem} subclasses. + * + *

An {@code If} hides every line up to its {@code EndIf}; {@code Exit} ends the current script + * only, so a {@code Call}er carries on; the colour carries into called scripts and over to the next + * frame. A script cannot call itself, directly or through another, the same guard the original had. + * A line that fails to format is skipped and reported once, where the original crashed. + */ +public final class HudRenderer { + private static final Logger LOGGER = LoggerFactory.getLogger(HudRenderer.class); + private static final int AZIMUTH_RED = 0xFFFF0000; + private static final String[] AZIMUTH_8 = {"S", "SW", "W", "NW", "N", "NE", "E", "SE"}; + /** {@code glLineStipple} pattern of the original's dashed lines. */ + private static final int DASHES = 0xCCCC; + + private static final HudState STATE = new HudState(); + private static final Set REPORTED = new HashSet<>(); + + private final HudCanvas canvas; + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final LocalPlayer player; + private final double centerX; + private final double centerY; + private final Set running = new HashSet<>(); + + private HudRenderer(HudCanvas canvas, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, double width, double height) { + this.canvas = canvas; + this.vehicle = vehicle; + this.def = def; + this.player = player; + this.centerX = width / 2.0; + this.centerY = height / 2.0; + } + + public static void tick() { + LocalPlayer player = Minecraft.getInstance().player; + STATE.tick(player != null && player.getVehicle() instanceof AircraftEntity vehicle ? vehicle : null); + } + + /** Fed by the pilot's mouse steering; moves the control stick indicator. */ + public static void onPilotLook(float dYaw, float dPitch) { + STATE.onPilotLook(dYaw, dPitch); + } + + /** + * Draws a HUD script for the local player's seat. + * + * @return false if no script by that name is loaded, so the caller can draw something else + */ + public static boolean draw(GuiGraphicsExtractor g, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, + int seat, String hud, float partialTicks) { + HudScript script = HudScripts.INSTANCE.get(hud); + if (script == null) { + return false; + } + double width = g.guiWidth(); + double height = g.guiHeight(); + STATE.update(vehicle, def, player, seat, width, height, partialTicks); + HudRenderer renderer = new HudRenderer(new HudCanvas(g, Minecraft.getInstance().font), vehicle, def, player, width, height); + renderer.run(script); + return true; + } + + private double eval(Expression e) { + return e.eval(STATE.vars); + } + + private double x(Expression e) { + return centerX + eval(e); + } + + private double y(Expression e) { + return centerY + eval(e); + } + + private double[] points(List coordinates) { + double[] out = new double[coordinates.size()]; + for (int i = 0; i + 1 < out.length; i += 2) { + out[i] = x(coordinates.get(i)); + out[i + 1] = y(coordinates.get(i + 1)); + } + return out; + } + + private void run(HudScript script) { + if (!running.add(script.name())) { + return; + } + try { + boolean hidden = false; + for (HudCommand command : script.commands()) { + if (command instanceof HudCommand.If c) { + hidden = eval(c.condition()) == 0.0; + continue; + } + if (command instanceof HudCommand.EndIf) { + hidden = false; + continue; + } + if (hidden) { + continue; + } + if (command instanceof HudCommand.Exit) { + return; + } + execute(script, command); + } + } finally { + running.remove(script.name()); + } + } + + private void execute(HudScript script, HudCommand command) { + int color = STATE.color; + switch (command) { + case HudCommand.Color c -> { + STATE.color = (int) (long) eval(c.argb()); + STATE.vars.set(HudVariable.COLOR, STATE.color & 0xFFFFFFFFL); + } + case HudCommand.Call c -> { + HudScript called = HudScripts.INSTANCE.get(c.hud()); + if (called != null) { + run(called); + } + } + case HudCommand.Text c -> text(script, c, color); + case HudCommand.Texture c -> { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + c.name() + ".png"); + int[] size = HudScripts.INSTANCE.textureSize(Minecraft.getInstance().getResourceManager(), texture); + canvas.texture(texture, x(c.left()), y(c.top()), eval(c.width()), eval(c.height()), + eval(c.u()), eval(c.v()), eval(c.uWidth()), eval(c.vHeight()), (float) eval(c.rotation()), size[0], size[1]); + } + case HudCommand.Rect c -> { + // The original truncated the size, not the position. + double left = x(c.left()); + double top = y(c.top()); + canvas.rect(left, top, left + (int) eval(c.width()), top + (int) eval(c.height()), color); + } + case HudCommand.Line c -> canvas.strip(points(c.points()), color); + case HudCommand.LineStipple c -> canvas.stippled(points(c.points()), color, (int) eval(c.factor()), (int) eval(c.pattern())); + case HudCommand.Radar c -> radar(c, color); + case HudCommand.Graduation c -> graduation(c, color); + case HudCommand.CameraRot c -> cameraRot(x(c.x()), y(c.y()), color); + case HudCommand.If c -> { + } + case HudCommand.EndIf c -> { + } + case HudCommand.Exit c -> { + } + } + } + + /** {@code MCH_HudItemString}: skipped while a weapon value is shown and no weapon (or magazine) applies. */ + private void text(HudScript script, HudCommand.Text c, int color) { + Object[] args = new Object[c.args().size()]; + for (int i = 0; i < args.length; i++) { + TextArg arg = c.args().get(i); + if ((arg.needsWeapon() && !STATE.hasWeapon()) || (arg.needsMagazine() && !STATE.hasMagazine())) { + return; + } + args[i] = STATE.text(arg, vehicle, def, player); + } + String text; + try { + text = String.format(Locale.ROOT, c.format(), args); + } catch (IllegalFormatException e) { + String key = script.name() + ':' + c.line(); + if (REPORTED.add(key)) { + LOGGER.warn("HUD {}.txt line {}: cannot format \"{}\": {}", script.name(), c.line(), c.format(), e.getMessage()); + } + return; + } + canvas.text(text, (int) x(c.x()), (int) y(c.y()), color, c.centered()); + } + + /** {@code MCH_HudItemRadar}: blips at half scale for a 64 unit box, turned with the view. */ + private void radar(HudCommand.Radar c, int color) { + List blips = c.enemies() ? STATE.enemyBlips : STATE.entityBlips; + if (blips.isEmpty()) { + return; + } + double rotation = Math.toRadians(eval(c.rotation())); + double left = x(c.left()); + double top = y(c.top()); + double w = eval(c.width()); + double h = eval(c.height()); + double cos = Math.cos(rotation); + double sin = Math.sin(rotation); + for (double[] blip : blips) { + double px = blip[0] / 2.0 * w / 64.0; + double py = blip[1] / 2.0 * h / 64.0; + double rx = px * cos - py * sin; + double ry = px * sin + py * cos; + if (rx > -w / 2.0 && rx < w / 2.0 && ry > -h / 2.0 && ry < h / 2.0) { + canvas.point(rx + left + w / 2.0, ry + top + h / 2.0, color); + } + } + } + + private void graduation(HudCommand.Graduation c, int color) { + int x = (int) x(c.x()); + int y = (int) y(c.y()); + double angle = eval(c.angle()); + canvas.pushRotation(x, y, (float) eval(c.roll())); + switch (c.kind()) { + case YAW -> headingTape(angle, color, x, y); + case PITCH_1 -> pitchScale(angle, color, x, y); + case PITCH_2 -> pitchLadder(angle, color, x, y, true); + case PITCH_3 -> pitchLadder(angle, color, x, y, false); + } + canvas.pop(); + } + + /** {@code drawCommonGraduationYaw}: a tick every degree, labels every three, compass points every 45. */ + private void headingTape(double playerYaw, int color, int posX, int posY) { + double yaw = HudState.rotate360(playerYaw); + posX -= 90; + double[] line = new double[76]; + int offset = (int) (yaw * 10.0) % 10; + int degree = (int) yaw - 9; + for (int i = 0; i < line.length / 4; i++, degree++) { + int azimuthX = posX + i * 10 - offset; + line[i * 4] = azimuthX; + line[i * 4 + 1] = posY; + line[i * 4 + 2] = azimuthX; + line[i * 4 + 3] = posY + (degree % 45 == 0 ? 15 : degree % 3 == 0 ? 10 : 5); + if (degree % 45 == 0) { + canvas.text(AZIMUTH_8[Math.floorMod(degree, 360) / 45], azimuthX, posY - 10, AZIMUTH_RED, true); + } else if (degree % 3 == 0) { + int heading = degree + 180; + if (heading < 0) { + heading += 360; + } + if (heading > 360) { + heading -= 360; + } + canvas.text(Integer.toString(heading), azimuthX, posY - 10, color, true); + } + } + canvas.lines(line, color); + } + + /** {@code drawCommonGraduationPitch1}: a scale of ticks on both sides that slides with the pitch. */ + private void pitchScale(double playerPitch, int color, int posX, int posY) { + int pitch = (int) playerPitch % 360; + int y = (int) (playerPitch * 10.0 % 10.0); + if (y < 0) { + y += 10; + } + int left = posX - 100; + int right = posX + 100; + posY -= 80; + double[] line = new double[144]; + int p = playerPitch < 0.0 && y != 0 ? pitch - 9 : pitch - 8; + for (int i = 0; i < line.length / 8; i++, p++) { + int outer = p % 3 == 0 ? 15 : 5; + double rowY = posY + i * 10 - y; + line[i * 8] = left - outer; + line[i * 8 + 1] = rowY; + line[i * 8 + 2] = left; + line[i * 8 + 3] = rowY; + line[i * 8 + 4] = right + outer; + line[i * 8 + 5] = rowY; + line[i * 8 + 6] = right; + line[i * 8 + 7] = rowY; + } + canvas.lines(line, color); + canvas.strip(new double[]{left - 25, posY - 90, left, posY - 90, left, posY + 90, left - 25, posY + 90}, color); + canvas.strip(new double[]{right + 25, posY - 90, right, posY - 90, right, posY + 90, right + 25, posY + 90}, color); + } + + /** + * {@code drawCommonGraduationPitch2}: a ladder with a rung every five degrees, dashed below + * the horizon. {@code wide} is {@code Pitch2} with five rungs; {@code Pitch3} has three, twice as far apart. + */ + private void pitchLadder(double playerPitch, int color, int posX, int posY, boolean wide) { + playerPitch = -playerPitch; + int pitchStep = (int) playerPitch / 5 * 5; + int start = wide ? 0 : 1; + int end = wide ? 5 : 4; + int spacing = wide ? 1 : 2; + double rest = playerPitch % 5.0; + double[] line = new double[8]; + for (int i = start; i < end; i++) { + int pitch = -(-pitchStep - 10 + i * 5); + int halfWidth = pitch != 0 ? 50 : 100; + int y = posY + (int) (-60 * spacing + rest * 6.0 * spacing + i * 30 * spacing); + line[0] = posX - halfWidth; + line[1] = y + (pitch == 0 ? 0 : pitch > 0 ? 2 : -2); + line[2] = posX - 50; + line[3] = y; + line[4] = posX + halfWidth; + line[5] = line[1]; + line[6] = posX + 50; + line[7] = y; + canvas.lines(line, color); + line[0] = posX - 50; + line[1] = y; + line[2] = posX - 30; + line[3] = y; + line[4] = posX + 50; + line[5] = y; + line[6] = posX + 30; + line[7] = y; + if (pitch >= 0) { + canvas.lines(line, color); + } else { + canvas.stippled(line, color, 1, DASHES); + } + if (pitch != 0) { + canvas.text(Integer.toString(pitch), posX - 60, y - 4, color, true); + canvas.text(Integer.toString(pitch), posX + 60, y - 4, color, true); + } + } + } + + /** {@code MCH_HudItemCameraRot}: a frame with a box showing where the crew's view points against the hull. */ + private void cameraRot(double posX, double posY, int color) { + canvas.loop(new double[]{posX - 21, posY - 11, posX + 21, posY - 11, posX + 21, posY + 11, posX - 21, posY + 11}, color); + canvas.stippled(new double[]{posX - 21, posY, posX, posY, posX + 21, posY, posX, posY, + posX, posY - 11, posX, posY, posX, posY + 11, posX, posY}, color, 1, DASHES); + float pitch = Mth.clamp(player.getXRot(), -30.0F, 70.0F); + pitch = (float) ((pitch - 20.0F) * 0.16); + float yaw = Mth.clamp(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot()) * 2.0F, -50.0F, 50.0F); + yaw = (float) (yaw * 0.34); + canvas.loop(new double[]{posX + yaw - 3, posY + pitch - 2, posX + yaw + 3, posY + pitch - 2, + posX + yaw + 3, posY + pitch + 2, posX + yaw - 3, posY + pitch + 2}, color); + } +} From 903964da63475e41f11160015d592b1a99ef34a9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:24 +0800 Subject: [PATCH 0443/1461] Add client/hud/HudScripts.java to the 26.1 shared sources --- .../mcheli/client/hud/HudScripts.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java new file mode 100644 index 00000000..6e611a23 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java @@ -0,0 +1,86 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.hud.HudFormatException; +import com.jnhprojects.mcheli.hud.HudScript; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +/** + * The HUD scripts in {@code assets/mcheli/hud}, read through the resource manager so resource + * packs can replace or add them, and reloaded with F3 + T. A script that fails to parse is logged + * and left out; the vehicles using it fall back to the plain readout. + */ +public final class HudScripts implements ResourceManagerReloadListener { + public static final HudScripts INSTANCE = new HudScripts(); + private static final Logger LOGGER = LoggerFactory.getLogger(HudScripts.class); + private static final String FOLDER = "hud"; + private static final int DEFAULT_TEXTURE_SIZE = 256; + + private Map scripts = Map.of(); + private final Map textureSizes = new HashMap<>(); + + private HudScripts() { + } + + public @Nullable HudScript get(String name) { + return scripts.get(name.toLowerCase(Locale.ROOT)); + } + + @Override + public void onResourceManagerReload(ResourceManager resources) { + Map loaded = new HashMap<>(); + resources.listResources(FOLDER, id -> id.getNamespace().equals(MCHeli.MOD_ID) && id.getPath().endsWith(".txt")) + .forEach((id, resource) -> { + String path = id.getPath(); + String name = path.substring(FOLDER.length() + 1, path.length() - 4).toLowerCase(Locale.ROOT); + try (Reader in = resource.openAsReader()) { + loaded.put(name, HudScript.parse(name, in)); + } catch (IOException | HudFormatException e) { + LOGGER.error("Skipping HUD {}: {}", id, e.getMessage()); + } + }); + scripts = Map.copyOf(loaded); + textureSizes.clear(); + LOGGER.info("Loaded {} HUD scripts", scripts.size()); + } + + /** + * Width and height of a GUI texture in pixels, read from the PNG header once. The original + * fell back to 256 when it could not tell, and so does this. + */ + int[] textureSize(ResourceManager resources, Identifier texture) { + return textureSizes.computeIfAbsent(texture, id -> readSize(resources.getResource(id))); + } + + private static int[] readSize(Optional resource) { + if (resource.isPresent()) { + try (InputStream in = resource.get().open(); DataInputStream data = new DataInputStream(in)) { + // 8 byte signature, IHDR length and type, then width and height as big endian ints. + data.skipNBytes(16); + int width = data.readInt(); + int height = data.readInt(); + if (width > 0 && height > 0) { + return new int[]{width, height}; + } + } catch (IOException e) { + LOGGER.warn("Could not read the size of a HUD texture", e); + } + } + return new int[]{DEFAULT_TEXTURE_SIZE, DEFAULT_TEXTURE_SIZE}; + } +} From 4af49d72c5613e944eb73a5bd714f5923eaced0a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:25 +0800 Subject: [PATCH 0444/1461] Report 26.1 as the Minecraft version to HUD scripts --- .../mcheli/client/hud/HudState.java | 373 ++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java new file mode 100644 index 00000000..cecf5018 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java @@ -0,0 +1,373 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.client.VehicleCamera; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * What the HUD scripts read, the static state of {@code MCH_HudItem}. Ticked with the client, so + * the radar sweep, altitude and stick behave as in the original; {@link #update} then fills in the + * per frame values before a script runs. + * + *

Systems that arrive later read as their idle state for now: the vehicle inventory is empty + * and the mortar distance is unknown. + */ +final class HudState { + private static final int RADAR_RANGE = 64; + private static final int ALTITUDE_INTERVAL = 10; + private static final double STICK_MAX = 40.0; + /** Roughly the mouse units of the original per degree of pilot look. */ + private static final double STICK_PER_DEGREE = 6.0; + private static final int STICK_SAMPLES = 4; + + final HudVariable.Values vars = new HudVariable.Values(); + final List entityBlips = new ArrayList<>(); + final List enemyBlips = new ArrayList<>(); + + private @Nullable AircraftEntity vehicle; + /** Where the selected mortar would land, or -1. */ + private float mortarDistance = -1.0F; + private int radarRotate; + private int radarRotateO; + private int altitude; + private int altitudeCountdown; + private double stickX; + private double stickY; + private final double[] stickXSamples = new double[STICK_SAMPLES]; + private final double[] stickYSamples = new double[STICK_SAMPLES]; + private int stickSample; + private int fuelWarnCount; + + /** Colour set by the last {@code Color} line. Like the original it carries over between frames. */ + int color = 0xFF000000; + + // Per frame, for the text arguments. + private @Nullable WeaponSet weapon; + private String weaponName = ""; + private String weaponAmmo = ""; + private String weaponRestAmmo = ""; + private float reloadPercent; + private float reloadSeconds; + + /** Mouse look of the pilot this frame, in degrees; drives the control stick indicator. */ + void onPilotLook(float dYaw, float dPitch) { + stickX += dYaw * STICK_PER_DEGREE; + stickY += dPitch * STICK_PER_DEGREE; + double length = Math.sqrt(stickX * stickX + stickY * stickY); + if (length > STICK_MAX) { + stickX *= STICK_MAX / length; + stickY *= STICK_MAX / length; + } + } + + void tick(@Nullable AircraftEntity riding) { + if (riding != vehicle) { + vehicle = riding; + radarRotate = 0; + radarRotateO = 0; + altitudeCountdown = 0; + entityBlips.clear(); + enemyBlips.clear(); + } + stickX *= 0.85; + stickY *= 0.85; + if (riding == null || riding.definition() == null) { + return; + } + if (riding.definition().entityRadar) { + radarRotateO = radarRotate; + radarRotate += radarSpeed(riding); + if (radarRotate >= 360) { + radarRotate = 0; + radarRotateO = 0; + sweepRadar(riding); + } + } + if (--altitudeCountdown <= 0) { + altitude = measureAltitude(riding); + altitudeCountdown = ALTITUDE_INTERVAL; + } + } + + /** Degrees per tick, from each kind's client tick handler. */ + private static int radarSpeed(AircraftEntity vehicle) { + return vehicle instanceof PlaneEntity || vehicle instanceof TankEntity ? 10 : 5; + } + + /** {@code MCH_Radar.updateXZ}: mobs within 64 blocks that are not buried under five or more blocks. */ + private void sweepRadar(AircraftEntity center) { + entityBlips.clear(); + enemyBlips.clear(); + Level level = center.level(); + for (Mob mob : level.getEntitiesOfClass(Mob.class, center.getBoundingBox().inflate(RADAR_RANGE))) { + double x = mob.getX() - center.getX(); + double z = mob.getZ() - center.getZ(); + if (x * x + z * z >= RADAR_RANGE * RADAR_RANGE || isBuried(level, mob)) { + continue; + } + (mob instanceof Enemy ? enemyBlips : entityBlips).add(new double[]{x, z}); + } + } + + private static boolean isBuried(Level level, Mob mob) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(mob.getBlockX(), Math.max(mob.getBlockY() + 1, level.getMinY()), mob.getBlockZ()); + int blocks = 0; + for (; pos.getY() < level.getMaxY(); pos.move(0, 1, 0)) { + if (!level.getBlockState(pos).isAir() && ++blocks >= 5) { + return true; + } + } + return false; + } + + /** {@code updateAltitude}: blocks of air below the vehicle, looking at most 256 down. */ + private static int measureAltitude(AircraftEntity vehicle) { + Level level = vehicle.level(); + BlockPos.MutableBlockPos pos = vehicle.blockPosition().mutable(); + int top = pos.getY(); + for (int i = 0; i < 256 && top - i > level.getMinY(); i++) { + pos.setY(top - i); + if (!level.getBlockState(pos).isAir()) { + return i; + } + } + return 0; + } + + float radarRotation(float partialTicks) { + float rot = radarRotate; + float prev = radarRotateO; + if (rot < prev) { + rot += 360.0F; + } + return prev + (rot - prev) * partialTicks; + } + + void update(AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, int seat, double width, double height, + float partialTicks) { + HudVariable.Values v = vars; + v.set(HudVariable.COLOR, color & 0xFFFFFFFFL); + v.set(HudVariable.CENTER_X, width / 2.0); + v.set(HudVariable.CENTER_Y, height / 2.0); + v.set(HudVariable.WIDTH, width); + v.set(HudVariable.HEIGHT, height); + v.set(HudVariable.TIME, Math.floorMod(vehicle.level().getOverworldClockTime(), 24000L)); + v.set(HudVariable.TEST_MODE, 0.0); + v.set(HudVariable.PLYR_YAW, Mth.wrapDegrees(player.getViewYRot(partialTicks))); + v.set(HudVariable.PLYR_PITCH, player.getViewXRot(partialTicks)); + v.set(HudVariable.YAW, Mth.wrapDegrees(vehicle.getYRot(partialTicks))); + v.set(HudVariable.PITCH, vehicle.getXRot(partialTicks)); + v.set(HudVariable.ROLL, Mth.wrapDegrees(vehicle.roll(partialTicks))); + v.set(HudVariable.ALTITUDE, altitude); + v.set(HudVariable.SEA_ALT, Math.max(vehicle.getY() - vehicle.level().getSeaLevel(), 0.0)); + v.set(HudVariable.HAVE_RADAR, def.entityRadar); + v.set(HudVariable.RADAR_ROT, radarRotation(partialTicks)); + v.set(HudVariable.HP, vehicle.hp()); + v.set(HudVariable.MAX_HP, def.maxHp); + v.set(HudVariable.HP_RTO, def.maxHp > 0 ? (double) vehicle.hp() / def.maxHp : 0.0); + v.set(HudVariable.THROTTLE, vehicle.visibleThrottle()); + v.set(HudVariable.POS_X, vehicle.getX()); + v.set(HudVariable.POS_Y, vehicle.getY()); + v.set(HudVariable.POS_Z, vehicle.getZ()); + // Only the pilot's client simulates the hull, so read motion from the position instead. + double mx = vehicle.getX() - vehicle.xo; + double my = vehicle.getY() - vehicle.yo; + double mz = vehicle.getZ() - vehicle.zo; + v.set(HudVariable.MOTION_X, mx); + v.set(HudVariable.MOTION_Y, my); + v.set(HudVariable.MOTION_Z, mz); + v.set(HudVariable.SPEED, Math.sqrt(mx * mx + my * my + mz * mz)); + v.set(HudVariable.FUEL, vehicle.fuelRatio()); + v.set(HudVariable.LOW_FUEL, lowFuel(vehicle)); + updateStick(seat == 0 && vehicle.steersWithMouse()); + updateWeapon(vehicle, seat); + v.set(HudVariable.VTOL_STAT, vehicle.vtolMode()); + v.set(HudVariable.FREE_LOOK, seat == 0 && vehicle.input().freeLook); + v.set(HudVariable.GUNNER_MODE, vehicle.isGunnerMode(player)); + v.set(HudVariable.CAM_MODE, VehicleCamera.mode().ordinal()); + v.set(HudVariable.CAM_ZOOM, VehicleCamera.zoom()); + v.set(HudVariable.AUTO_PILOT, vehicle instanceof PlaneEntity && vehicle.isGunnerMode(player)); + VehicleWeapons weapons = vehicle.weapons(); + boolean flares = weapons != null && weapons.flares.isFitted(); + v.set(HudVariable.HAVE_FLARE, flares); + v.set(HudVariable.CAN_FLARE, flares && !weapons.flares.isPreparing() && !weapons.flares.isActive()); + v.set(HudVariable.INVENTORY, 0.0); + v.set(HudVariable.HOVERING, vehicle instanceof HelicopterEntity && vehicle.isHoveringMode()); + v.set(HudVariable.IS_UAV, def.uav); + double uavDistance = vehicle.stationDistance(); + // {@code getUAV_Fs}: signal strength, full at the station and gone 120 blocks out. + v.set(HudVariable.UAV_FS, uavDistance < 0.0 ? 0.0 : 1.0 - Math.min(uavDistance, 120.0) / 120.0); + } + + /** + * {@code isLowFuel}: below 10% the warning blinks, on for eight seconds out of every fourteen. + * Ticks with each update, as the original counted per frame. + */ + private boolean lowFuel(AircraftEntity vehicle) { + if (fuelWarnCount <= 0) { + fuelWarnCount = 280; + } + fuelWarnCount--; + return fuelWarnCount < 160 && vehicle.maxFuel() > 0 && vehicle.fuelRatio() < 0.1F; + } + + /** The original averaged the last four stick positions, scaled to -1..1, with up positive. */ + private void updateStick(boolean pilot) { + stickXSamples[stickSample] = pilot ? stickX / STICK_MAX : 0.0; + stickYSamples[stickSample] = pilot ? -stickY / STICK_MAX : 0.0; + stickSample = (stickSample + 1) % STICK_SAMPLES; + double x = 0.0; + double y = 0.0; + for (int i = 0; i < STICK_SAMPLES; i++) { + x += stickXSamples[i]; + y += stickYSamples[i]; + } + vars.set(HudVariable.STICK_X, x / STICK_SAMPLES); + vars.set(HudVariable.STICK_Y, y / STICK_SAMPLES); + } + + /** {@code updateWeapon} and {@code updateVarMap_Weapon}. */ + private void updateWeapon(AircraftEntity vehicle, int seat) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + weapon = set; + double reloadTime = 0.0; + double heat = 0.0; + double lock = 0.0; + int sightType = 0; + if (set != null) { + WeaponDefinition def = set.definition(); + weaponName = set.displayName(); + weaponAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.ammo()) : ""; + weaponRestAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.restAmmo()) : ""; + float wait; + if (def.delay > def.reloadTime) { + reloadSeconds = Math.abs(set.countWait); + wait = Math.min(Math.abs((float) set.countWait / Math.max(def.delay, 1)), 1.0F); + } else { + reloadSeconds = set.countReloadWait; + wait = (float) set.countReloadWait / Math.max(def.reloadTime, 1); + } + reloadTime = wait; + reloadSeconds /= 20.0F; + reloadPercent = (1.0F - wait) * 100.0F; + if (def.maxHeatCount > 0) { + heat = Math.min((double) set.currentHeat / def.maxHeatCount, 1.0); + } + Guidance guidance = weapons.guidance(weapons.selected(seat)); + if (def.sight == WeaponDefinition.Sight.LOCK && guidance != null && guidance.lockCountMax() > 0) { + lock = (double) guidance.lockCount() / guidance.lockCountMax(); + sightType = 2; + } else if (def.sight == WeaponDefinition.Sight.ROCKET) { + sightType = 1; + } + vars.set(HudVariable.IS_HEAT_WPN, def.maxHeatCount > 0); + vars.set(HudVariable.RELOADING, set.isInPreparation()); + vars.set(HudVariable.DSP_MT_DIST, def.displayMortarDistance); + // The original worked the arc out every fifth tick; it is cheap enough to follow every update. + mortarDistance = def.displayMortarDistance ? (float) set.landingDistance(vehicle) : -1.0F; + } else { + vars.set(HudVariable.IS_HEAT_WPN, 0.0); + vars.set(HudVariable.RELOADING, 0.0); + vars.set(HudVariable.DSP_MT_DIST, 0.0); + mortarDistance = -1.0F; + } + vars.set(HudVariable.RELOAD_TIME, reloadTime); + vars.set(HudVariable.WPN_HEAT, heat); + vars.set(HudVariable.SIGHT_TYPE, sightType); + vars.set(HudVariable.LOCK, lock); + vars.set(HudVariable.MT_DIST, mortarDistance); + } + + boolean hasWeapon() { + return weapon != null; + } + + boolean hasMagazine() { + return weapon != null && weapon.magazineSize() > 0; + } + + /** A {@code DrawString} argument, typed the way the original passed it to {@code String.format}. */ + @Nullable Object text(TextArg arg, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player) { + int worldTime = (int) Math.floorMod(vehicle.level().getOverworldClockTime() + 6000L, 24000L); + return switch (arg) { + case NONE -> null; + // The vehicle inventory screen and its key come later in phase 4. + case KEY_GUI -> ""; + case NAME -> def.displayName; + case ALTITUDE -> altitude; + case DATE -> new Date(); + case MC_THOR -> worldTime / 1000; + case MC_TMIN -> worldTime % 1000 * 36 / 10 / 60; + case MC_TSEC -> worldTime % 1000 * 36 / 10 % 60; + case MAX_HP -> def.maxHp; + case HP -> vehicle.hp(); + case HP_PER -> def.maxHp > 0 ? 100.0 * vehicle.hp() / def.maxHp : 0.0; + case POS_X -> vehicle.getX(); + case POS_Y -> vehicle.getY(); + case POS_Z -> vehicle.getZ(); + case MOTION_X -> vars.get(HudVariable.MOTION_X); + case MOTION_Y -> vars.get(HudVariable.MOTION_Y); + case MOTION_Z -> vars.get(HudVariable.MOTION_Z); + case INVENTORY -> 0; + case WPN_NAME -> weaponName; + case WPN_AMMO -> weaponAmmo; + case WPN_RM_AMMO -> weaponRestAmmo; + case RELOAD_PER -> reloadPercent; + case RELOAD_SEC -> reloadSeconds; + case MORTAR_DIST -> mortarDistance; + case MC_VER -> "26.1"; + case MOD_VER -> Platform.get().modVersion(); + case MOD_NAME -> "MCHeli Rework"; + case YAW -> rotate360(vehicle.getYRot() + 180.0); + case PITCH -> -vehicle.getXRot(); + case ROLL -> Mth.wrapDegrees(vehicle.roll()); + case PLYR_YAW -> rotate360(player.getYRot() + 180.0); + case PLYR_PITCH -> -player.getXRot(); + case TVM_POS_X, TVM_POS_Y, TVM_POS_Z, TVM_DIFF -> tvMissile(arg, vehicle); + case CAM_ZOOM -> VehicleCamera.zoom(); + case UAV_DIST -> Math.max(vehicle.stationDistance(), 0.0); + case THROTTLE -> vehicle.visibleThrottle() * 100.0; + }; + } + + private static double tvMissile(TextArg arg, AircraftEntity vehicle) { + TvMissileEntity missile = vehicle.tvMissile(); + if (missile == null) { + return 0.0; + } + return switch (arg) { + case TVM_POS_X -> missile.getX(); + case TVM_POS_Y -> missile.getY(); + case TVM_POS_Z -> missile.getZ(); + default -> missile.position().distanceTo(vehicle.position()); + }; + } + + static double rotate360(double degrees) { + double r = degrees % 360.0; + return r >= 0.0 ? r : r + 360.0; + } +} From aa26385a965bb21214596a6ac316e1cd569cf99f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:25 +0800 Subject: [PATCH 0445/1461] Add client/particle/GlowParticle.java to the 26.1 shared sources --- .../mcheli/client/particle/GlowParticle.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java new file mode 100644 index 00000000..a09d9b2e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java @@ -0,0 +1,103 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.renderer.state.level.QuadParticleRenderState; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; +import net.minecraft.util.ARGB; +import net.minecraft.util.LightCoordsUtil; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import org.jspecify.annotations.Nullable; + +/** + * {@code mcheli:glow}: a ball of light added on top of the scene. A flash swells quickly and fades + * out; a fire holds its brightness, wavering, and only fades towards the end. How bright it gets + * depends on the light where it starts, so the same explosion is a flicker at noon and lights up + * the sky at night. + */ +public class GlowParticle extends SingleQuadParticle { + private final float size; + private final float peak; + private final boolean flicker; + private float waver = 1.0F; + + GlowParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, GlowParticleOptions options, + TextureAtlasSprite sprite, float strength) { + super(level, x, y, z, sprite); + setParticleSpeed(xd, yd, zd); + this.size = options.size(); + this.peak = strength; + this.flicker = options.flicker(); + this.quadSize = size; + this.lifetime = options.lifetime(); + this.friction = 0.9F; + this.gravity = 0.0F; + this.hasPhysics = false; + setColor(ARGB.redFloat(options.color()), ARGB.greenFloat(options.color()), ARGB.blueFloat(options.color())); + this.alpha = 0.0F; + } + + @Override + public void tick() { + super.tick(); + if (flicker) { + waver = 0.7F + random.nextFloat() * 0.3F; + } + } + + @Override + public void extract(QuadParticleRenderState state, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + if (flicker) { + // Burning: hold, then die down over the last third. + alpha = peak * waver * Math.min(1.0F, (1.0F - t) * 3.0F); + } else { + // A flash: brightest at once, gone quickly. + float left = 1.0F - t; + alpha = peak * left * left; + } + super.extract(state, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return flicker ? size * (0.85F + 0.15F * waver) : size * (0.6F + 0.4F * Math.min(1.0F, t * 4.0F)); + } + + @Override + protected int getLightCoords(float partialTicks) { + return LightCoordsUtil.FULL_BRIGHT; + } + + @Override + protected Layer getLayer() { + return Glow.PARTICLE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public @Nullable Particle createParticle(GlowParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd, RandomSource random) { + float strength = Glow.strength(level, BlockPos.containing(x, y, z)); + if (strength <= 0.0F) { + return null; + } + return new GlowParticle(level, x, y, z, xd, yd, zd, options, sprites.first(), strength); + } + } +} From dbb50c24b11052c120ac3cb4cb456666838b36fb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:26 +0800 Subject: [PATCH 0446/1461] Add client/particle/SmokeParticle.java to the 26.1 shared sources --- .../mcheli/client/particle/SmokeParticle.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java new file mode 100644 index 00000000..ff6c3e73 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java @@ -0,0 +1,83 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.renderer.state.level.QuadParticleRenderState; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; + +/** + * {@code mcheli:smoke}: a puff of coloured smoke, {@code MCH_EntityParticleSmoke}. It swells to its + * full size over the first part of its life, its colour drifts towards white, it rises slowly and + * fades out at the end. The sprites are vanilla's campfire smoke. + */ +public class SmokeParticle extends SingleQuadParticle { + private final float size; + + SmokeParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, SmokeParticleOptions options, + SpriteSet sprites, RandomSource random) { + super(level, x, y, z, sprites.get(random)); + setParticleSpeed(xd, yd, zd); + // The original drew a puff 0.1 x its size out from the middle, so size 10 is two blocks across. + this.size = options.size() * 0.1F * (0.8F + random.nextFloat() * 0.4F); + this.quadSize = size * 0.4F; + this.lifetime = options.lifetime() > 0 ? options.lifetime() : 60 + random.nextInt(40); + this.friction = 0.96F; + this.gravity = -0.002F; + this.hasPhysics = true; + setColor(ARGB.redFloat(options.color()), ARGB.greenFloat(options.color()), ARGB.blueFloat(options.color())); + this.alpha = 0.9F; + } + + @Override + public void tick() { + super.tick(); + // toWhite: pale towards the brightest channel while the colour is still strong. + float max = Math.max(rCol, Math.max(gCol, bCol)); + float min = Math.min(rCol, Math.min(gCol, bCol)); + if (max - min > 0.2F) { + rCol += (max - rCol) * 0.016F; + gCol += (max - gCol) * 0.016F; + bCol += (max - bCol) * 0.016F; + } + } + + @Override + public void extract(QuadParticleRenderState state, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + alpha = 0.9F * Math.min(1.0F, (1.0F - t) * 4.0F); + super.extract(state, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return size * (0.4F + 0.6F * Math.min(1.0F, t * 3.0F)); + } + + @Override + protected Layer getLayer() { + return Glow.SMOKE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public Particle createParticle(SmokeParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd, RandomSource random) { + return new SmokeParticle(level, x, y, z, xd, yd, zd, options, sprites, random); + } + } +} From 9e429cdf3e6a94e5a2be04235bb68411d6a17693 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:26 +0800 Subject: [PATCH 0447/1461] Add client/render/A10Renderer.java to the 26.1 shared sources --- .../mcheli/client/render/A10Renderer.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java new file mode 100644 index 00000000..938be6d9 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +/** The close air support A-10, {@code MCH_RenderA10}. */ +public class A10Renderer extends EntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/a10.png"); + + public A10Renderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 10.5F; + } + + public static class State extends EntityRenderState { + float yaw; + float pitch; + boolean visible; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(A10Entity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.visible = entity.isVisible(); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = state.visible ? MeshCache.INSTANCE.get("", "a-10") : null; + if (mesh != null) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(-state.pitch)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutCull(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From ee9b882b8f15db31c2c12ea06c0bbd0cf4534836 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:26 +0800 Subject: [PATCH 0448/1461] Add client/render/AircraftRenderState.java to the 26.1 shared sources --- .../client/render/AircraftRenderState.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java new file mode 100644 index 00000000..d2dd770d --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java @@ -0,0 +1,77 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import org.jspecify.annotations.Nullable; + +/** + * Snapshot of a vehicle for one frame, filled in by {@link AircraftRenderer#extractRenderState}. + * Every angle is already interpolated for the frame's partial tick. + */ +public class AircraftRenderState extends EntityRenderState { + /** Definitions are immutable after loading, so the snapshot can share them. */ + public @Nullable AircraftDefinition definition; + public String texture = ""; + public float yaw; + public float pitch; + public float roll; + /** Helicopter main rotor or plane propeller angle. */ + public float rotorAngle; + /** Each helicopter blade's own angle, per rotor, so folding blades can leave the rotor's spin. */ + public float[][] bladeAngles = new float[0][]; + public float throttle; + + // Part states, 0 to 90 degrees + public float canopy; + public float landingGear; + public float hatch; + /** Rope length below the hooks while troops go down, 0 with the ropes in. */ + public float ropeLength; + /** The beams of the search lights, or false with them off. */ + public boolean searchLight; + /** Where the crew aim a turning search light, in world degrees. */ + public float searchLightYaw; + public float searchLightPitch; + public float nozzle; + public float wing; + + public float wheelRoll; + public float steer; + public final float[] roller = new float[2]; + /** Track run per side from 0 to 1, in link spacings. */ + public final float[] track = new float[2]; + public float[] rotatingParts = new float[0]; + + /** Per seat: occupant view relative to the hull, and whether anyone sits there. */ + public float[] seatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public boolean[] seatOccupied = new boolean[0]; + /** Seat of the player at this client, or -1, and whether they look through their own eyes. */ + public int localSeat = -1; + public boolean localFirstPerson; + /** Shade for the unlit glass, from the light at the vehicle. */ + public int glassColor = 0xFFFFFFFF; + /** Drawn inside a screen, as the workshop does, rather than in the world. */ + public boolean inGui; + + /** Per {@code AddPartWeapon} line, the state of the weapon it belongs to; see {@link WeaponPartState}. */ + public WeaponPartState[] weaponParts = new WeaponPartState[0]; + /** Per weapon bay, 0 to 90 degrees. */ + public float[] bays = new float[0]; + + /** How one weapon part is drawn this frame. */ + public static final class WeaponPartState { + /** Whether the part's weapon exists; without one the part follows its operator's view. */ + public boolean hasWeapon; + /** Aim relative to the weapon's default direction. */ + public float yaw; + public float pitch; + public float defaultYaw; + /** How far a turret weapon lags behind the pilot's view. */ + public float turretLag; + public float recoil; + public float barrel; + /** A missile part whose round is on its way and not reloaded yet. */ + public boolean hidden; + } +} From 769bebe412ea3b3cf597bcf973377b0d8daf1ef9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:27 +0800 Subject: [PATCH 0449/1461] Port AircraftRenderer to 26.1, with its older gizmo collector and culling signature --- .../client/render/AircraftRenderer.java | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java new file mode 100644 index 00000000..2445f269 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java @@ -0,0 +1,342 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.SearchLight; +import com.jnhprojects.mcheli.content.aircraft.RepellingHook; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.AnimationState; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PartState; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.debug.DebugScreenEntries; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.gizmos.GizmoStyle; +import net.minecraft.gizmos.Gizmos; +import net.minecraft.resources.Identifier; +import net.minecraft.util.LightCoordsUtil; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * Draws any MCHeli vehicle from its MQO/OBJ model, following {@code MCH_RenderAircraft}: the hull, + * the kind's own moving parts (rotors, wings, gun mounts) and then the parts every kind can have. + */ +public class AircraftRenderer extends EntityRenderer { + private static final int HULL_COLOR = 0xFF00E0FF; + private static final int GENERATED_COLOR = 0xFFFFC000; + /** Ropes take the colour of brown wool. */ + private static final Identifier ROPE = Identifier.withDefaultNamespace("textures/block/brown_wool.png"); + private static final float ROPE_WIDTH = 0.03F; + + public AircraftRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 2.0F; + } + + /** + * With F3+B on, outlines the hull boxes next to vanilla's main box: what projectiles hit and + * what other vehicles collide with. The definition's own boxes are cyan, the ones generated + * from the model amber. + */ + private static void showHull(AircraftEntity entity, AircraftDefinition def, float partialTicks) { + List hull = entity.hullBoxes(); + if (hull.size() < 2) { + return; + } + Vec3 offset = entity.getPosition(partialTicks).subtract(entity.position()); + int listed = def.boundingBoxes.size(); + try (Gizmos.TemporaryCollection ignored = Minecraft.getInstance().levelRenderer.collectPerFrameGizmos()) { + // The first is the main box, which vanilla draws itself; then the listed boxes, then the generated ones. + for (int i = 1; i < hull.size(); i++) { + Gizmos.cuboid(hull.get(i).move(offset), GizmoStyle.stroke(i <= listed ? HULL_COLOR : GENERATED_COLOR)); + } + } + } + + @Override + public AircraftRenderState createRenderState() { + return new AircraftRenderState(); + } + + @Override + public void extractRenderState(T entity, AircraftRenderState state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + AircraftDefinition def = entity.definition(); + state.definition = def; + state.texture = entity.texture(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.roll = entity.roll(partialTicks); + if (def == null) { + return; + } + if (Minecraft.getInstance().debugEntries.isCurrentlyEnabled(DebugScreenEntries.ENTITY_HITBOXES)) { + showHull(entity, def, partialTicks); + } + + state.rotorAngle = switch (entity) { + case HelicopterEntity heli -> heli.rotorAngle(partialTicks); + case PlaneEntity plane -> plane.rotorAngle(partialTicks); + default -> 0.0F; + }; + state.canopy = angle(entity.canopy(), partialTicks); + state.landingGear = angle(entity.landingGear(), partialTicks); + state.hatch = angle(entity.hatch(), partialTicks); + if (entity instanceof HelicopterEntity heli && def instanceof HelicopterDefinition heliDef) { + bladeAngles(heli, heliDef, state, partialTicks); + } + state.ropeLength = entity.isRepelling() ? entity.ropeLength() : 0.0F; + state.searchLight = entity.isSearchLightOn(); + if (state.searchLight) { + Entity aimer = entity.searchLightAimer(); + state.searchLightYaw = aimer != null ? aimer.getYRot(partialTicks) : state.yaw; + state.searchLightPitch = aimer != null ? aimer.getXRot(partialTicks) : state.pitch; + } + if (entity instanceof PlaneEntity plane) { + state.nozzle = angle(plane.nozzle(), partialTicks); + state.wing = angle(plane.wing(), partialTicks); + } else { + state.nozzle = 0.0F; + state.wing = 0.0F; + } + + AnimationState anim = entity.animation; + state.throttle = Mth.lerp(partialTicks, anim.prevThrottle, anim.throttle); + state.wheelRoll = Mth.lerp(partialTicks, anim.prevWheelRoll, anim.wheelRoll); + state.steer = Mth.lerp(partialTicks, anim.prevSteer, anim.steer); + for (int i = 0; i < 2; i++) { + state.roller[i] = Mth.lerp(partialTicks, anim.prevRoller[i], anim.roller[i]); + state.track[i] = lerpWrapped(anim.prevTrack[i], anim.track[i], partialTicks); + } + if (state.rotatingParts.length != anim.rotatingParts.length) { + state.rotatingParts = new float[anim.rotatingParts.length]; + } + for (int i = 0; i < anim.rotatingParts.length; i++) { + state.rotatingParts[i] = Mth.lerp(partialTicks, anim.prevRotatingParts[i], anim.rotatingParts[i]); + } + + int seats = def.seatAndRackCount(); + if (state.seatYaw.length != seats) { + state.seatYaw = new float[seats]; + state.seatPitch = new float[seats]; + state.seatOccupied = new boolean[seats]; + } + for (int i = 0; i < seats; i++) { + state.seatYaw[i] = anim.seatYaw(i, partialTicks); + state.seatPitch[i] = anim.seatPitch(i, partialTicks); + state.seatOccupied[i] = entity.occupant(i) != null; + } + Minecraft mc = Minecraft.getInstance(); + state.localSeat = mc.player != null ? entity.seatOf(mc.player) : -1; + state.localFirstPerson = mc.options.getCameraType().isFirstPerson(); + state.glassColor = glassColor(entity, state.lightCoords); + extractWeapons(entity, def, state, partialTicks); + } + + /** + * Glass is drawn without the light map, so shade it by hand: as bright as the brighter of block + * light and the sky light left after night or rain darkens it. + */ + private static int glassColor(AircraftEntity entity, int lightCoords) { + int sky = Math.max(LightCoordsUtil.sky(lightCoords) - entity.level().getSkyDarken(), 0); + int light = Math.max(LightCoordsUtil.block(lightCoords), sky); + int shade = (int) ((0.3F + 0.7F * light / 15.0F) * 255.0F); + return 0xFF000000 | shade << 16 | shade << 8 | shade; + } + + /** {@code renderWeapon}'s inputs: each part's weapon aim, recoil, barrel spin and reload state. */ + private static void extractWeapons(AircraftEntity entity, AircraftDefinition def, AircraftRenderState state, float partialTicks) { + VehicleWeapons weapons = entity.weapons(); + int parts = def.weaponParts.size(); + if (state.weaponParts.length != parts) { + state.weaponParts = new AircraftRenderState.WeaponPartState[parts]; + for (int i = 0; i < parts; i++) { + state.weaponParts[i] = new AircraftRenderState.WeaponPartState(); + } + } + WeaponSet previous = null; + int mount = 0; + for (int i = 0; i < parts; i++) { + WeaponPart part = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState out = state.weaponParts[i]; + WeaponSet set = weapons != null && !part.weapons().isEmpty() ? weapons.byName(part.weapons().getFirst()) : null; + if (set != previous) { + mount = 0; + previous = set; + } + out.hasWeapon = set != null; + if (set == null) { + continue; + } + float yaw = set.rotationYaw; + float prevYaw = set.prevRotationYaw; + if (yaw - prevYaw > 180.0F) { + prevYaw += 360.0F; + } else if (yaw - prevYaw < -180.0F) { + prevYaw -= 360.0F; + } + out.yaw = Mth.lerp(partialTicks, prevYaw, yaw) - set.defaultRotationYaw; + out.pitch = Mth.lerp(partialTicks, set.prevRotationPitch, set.rotationPitch); + out.defaultYaw = set.defaultRotationYaw; + out.turretLag = set.rotationTurretYaw; + out.barrel = Mth.lerp(partialTicks, set.prevBarrel, set.barrel); + // A part shared by several weapons kicks back with whichever fired hardest. + float recoil = set.recoil(0, partialTicks); + for (String name : part.weapons()) { + WeaponSet other = weapons.byName(name); + if (other != null) { + recoil = Math.max(recoil, other.recoil(0, partialTicks)); + } + } + out.recoil = recoil; + out.hidden = part.missile() && weapons.isMountUsed(weapons.indexOf(set.name()), mount); + mount++; + } + if (state.bays.length != def.weaponBays.size()) { + state.bays = new float[def.weaponBays.size()]; + } + for (int i = 0; i < state.bays.length; i++) { + state.bays[i] = weapons != null ? weapons.bayAngle(i, partialTicks) : 0.0F; + } + } + + private static float angle(@Nullable PartState part, float partialTicks) { + return part != null ? part.angle(partialTicks) : 0.0F; + } + + /** Interpolates a value that wraps from 1 back to 0. */ + private static float lerpWrapped(float prev, float current, float partialTicks) { + float d = current - prev; + if (d > 0.5F) { + d -= 1.0F; + } else if (d < -0.5F) { + d += 1.0F; + } + float v = prev + d * partialTicks; + return v - Mth.floor(v); + } + + @Override + protected AABB getBoundingBoxForCulling(T entity) { + // The hit box is far smaller than most models, so cull on a generous box instead. + AircraftDefinition def = entity.definition(); + double reach = def != null ? Math.max(8.0, def.markerWidth * 4.0) : 8.0; + return entity.getBoundingBox().inflate(reach, reach / 2.0, reach); + } + + @Override + public void submit(AircraftRenderState state, PoseStack poseStack, SubmitNodeCollector collector, CameraRenderState camera) { + AircraftDefinition def = state.definition; + Mesh mesh = def != null ? MeshCache.INSTANCE.get(def.folder(), def.name()) : null; + if (mesh != null) { + // Canopies and windshields are semi-transparent texels; PartRenderer draws those + // triangles blended, as the original did, and everything else as cutout. + poseStack.pushPose(); + poseStack.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + poseStack.mulPose(Axis.XP.rotationDegrees(state.pitch)); + poseStack.mulPose(Axis.ZP.rotationDegrees(state.roll)); + new PartRenderer(def, mesh, state, poseStack, collector, texture(def.folder(), state.texture), state.lightCoords, + OverlayTexture.NO_OVERLAY, state.glassColor).submitAll(); + if (state.ropeLength < 0.0F) { + ropes(def, state, poseStack, collector); + } + if (state.searchLight) { + searchLights(def, state, poseStack, collector); + } + poseStack.popPose(); + } + super.submit(state, poseStack, collector, camera); + } + + /** + * {@code renderLight}: each lamp's cone. A turning light follows the crew's aim; a fixed one + * points along the vehicle, and a steering one also turns with the front wheels. + */ + private static void searchLights(AircraftDefinition def, AircraftRenderState state, PoseStack pose, SubmitNodeCollector collector) { + for (SearchLight light : def.searchLights) { + pose.pushPose(); + pose.translate(light.position().x(), light.position().y(), light.position().z()); + if (!light.fixed()) { + pose.mulPose(Axis.YN.rotationDegrees(state.searchLightYaw - state.yaw + light.yaw())); + pose.mulPose(Axis.XP.rotationDegrees(state.searchLightPitch + 90.0F - state.pitch + light.pitch())); + } else { + float steer = light.steering() ? -state.steer * light.steeringRotation() : 0.0F; + pose.mulPose(Axis.YN.rotationDegrees(light.yaw() + steer)); + pose.mulPose(Axis.XP.rotationDegrees(90.0F + light.pitch())); + } + Glow.cone(pose, collector, light.height(), light.width() / 2.0F, light.colorStart(), light.colorEnd()); + pose.popPose(); + } + } + + private static void bladeAngles(HelicopterEntity heli, HelicopterDefinition def, AircraftRenderState state, float partialTicks) { + if (state.bladeAngles.length != def.rotors.size()) { + state.bladeAngles = new float[def.rotors.size()][]; + } + for (int r = 0; r < def.rotors.size(); r++) { + RotorDefinition rotor = def.rotors.get(r); + int blades = Math.max(rotor.blades(), 1); + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + if (state.bladeAngles[r] == null || state.bladeAngles[r].length != blades) { + state.bladeAngles[r] = new float[blades]; + } + for (int b = 0; b < blades; b++) { + state.bladeAngles[r][b] = heli.bladeAngle(r, b, step, partialTicks); + } + } + } + + /** {@code renderRope}: a dark line straight down from each hook, in the vehicle's own frame. */ + private static void ropes(AircraftDefinition def, AircraftRenderState state, PoseStack pose, SubmitNodeCollector collector) { + int light = state.lightCoords; + float length = state.ropeLength; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(ROPE), (p, buffer) -> { + for (RepellingHook hook : def.repellingHooks) { + float x = (float) hook.position().x(); + float y = (float) hook.position().y(); + float z = (float) hook.position().z(); + // Two thin crossed strips, so the rope shows from every side. + ropeStrip(p, buffer, x - ROPE_WIDTH, z, x + ROPE_WIDTH, z, y, length, light); + ropeStrip(p, buffer, x, z - ROPE_WIDTH, x, z + ROPE_WIDTH, y, length, light); + } + }); + } + + private static void ropeStrip(PoseStack.Pose p, VertexConsumer buffer, float x0, float z0, float x1, float z1, float y, float length, int light) { + ropeVertex(p, buffer, x0, y, z0, 0.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y, z1, 1.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y + length, z1, 1.0F, 1.0F, light); + ropeVertex(p, buffer, x0, y + length, z0, 0.0F, 1.0F, light); + } + + private static void ropeVertex(PoseStack.Pose p, VertexConsumer buffer, float x, float y, float z, float u, float v, int light) { + buffer.addVertex(p, x, y, z).setColor(0xFFFFFFFF).setUv(u, v).setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(p, 0.0F, 1.0F, 0.0F); + } + + private static Identifier texture(String folder, String name) { + return RuntimeAssets.texture(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + folder + "/" + name + ".png")); + } +} From 96ef90dff5d0be60a462a84351d632f5a28decfb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:27 +0800 Subject: [PATCH 0450/1461] Add client/render/ChainRenderer.java to the 26.1 shared sources --- .../mcheli/client/render/ChainRenderer.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java new file mode 100644 index 00000000..5d7f43b7 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java @@ -0,0 +1,93 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +/** + * {@code MCH_RenderChain}: a line of links from the towed end to the end that pulls, one link + * every 0.95 blocks, each turned along the chain. + */ +public class ChainRenderer extends EntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/chain.png"); + private static final double LINK = 0.95; + private static final int GREY = 0xFF808080; + + public ChainRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + /** Towed end, relative to the chain entity. */ + Vec3 from = Vec3.ZERO; + /** From the towed end to the end that pulls. */ + Vec3 span = Vec3.ZERO; + } + + /** The chain hangs over the end that pulls but reaches to the other end; keep both in view. */ + @Override + protected AABB getBoundingBoxForCulling(ChainEntity chain) { + AABB box = super.getBoundingBoxForCulling(chain); + Entity towed = chain.towed(); + return towed != null ? box.minmax(towed.getBoundingBox()) : box; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ChainEntity chain, State state, float partialTicks) { + super.extractRenderState(chain, state, partialTicks); + Entity tow = chain.tow(); + Entity towed = chain.towed(); + if (tow == null || towed == null) { + state.span = Vec3.ZERO; + return; + } + Vec3 here = chain.getPosition(partialTicks); + Vec3 start = towed.getPosition(partialTicks); + state.from = start.subtract(here); + state.span = tow.getPosition(partialTicks).subtract(start); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("", "chain"); + double length = state.span.length(); + if (mesh != null && length > LINK) { + Vec3 step = state.span.scale(LINK / length); + float yaw = (float) Math.toDegrees(Math.atan2(step.z, step.x)); + float pitch = (float) Math.toDegrees(Math.atan2(step.y, Math.sqrt(step.x * step.x + step.z * step.z))); + int light = state.lightCoords; + pose.pushPose(); + pose.translate(state.from); + for (double left = length; left > LINK; left -= LINK) { + pose.translate(step); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-yaw)); + pose.mulPose(Axis.ZP.rotationDegrees(pitch)); + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, GREY)); + pose.popPose(); + } + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From 8cb4f5e65658d6c7b5e5622370316d37aa0da222 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:28 +0800 Subject: [PATCH 0451/1461] Add client/render/DraftingTableRenderer.java to the 26.1 shared sources --- .../client/render/DraftingTableRenderer.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java new file mode 100644 index 00000000..665893c1 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** {@code MCH_DraftingTableRenderer}: the original model, turned to the block's eighth of a circle. */ +public class DraftingTableRenderer implements BlockEntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/block/drafting_table.png"); + /** The original greyed the table a little. */ + private static final int TINT = 0xFFBFBFBF; + + public DraftingTableRenderer(BlockEntityRendererProvider.Context context) { + } + + public static class State extends BlockEntityRenderState { + int rotation; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(DraftingTableBlockEntity table, State state, float partialTicks, Vec3 cameraPosition, + ModelFeatureRenderer.@Nullable CrumblingOverlay breakProgress) { + BlockEntityRenderer.super.extractRenderState(table, state, partialTicks, cameraPosition, breakProgress); + state.rotation = table.getBlockState().getValue(DraftingTableBlock.ROTATION); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("blocks", "drafting_table"); + if (mesh == null) { + return; + } + pose.pushPose(); + pose.translate(0.5F, 0.0F, 0.5F); + pose.mulPose(Axis.YP.rotationDegrees(-state.rotation * 45.0F + 180.0F)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, TINT)); + pose.popPose(); + } +} From 6ae37b751e8bae567c7f360f95948358611245b5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:28 +0800 Subject: [PATCH 0452/1461] Add client/render/EquipmentRenderer.java to the 26.1 shared sources --- .../client/render/EquipmentRenderer.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java new file mode 100644 index 00000000..80ca1e3b --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; + +/** + * Equipment drawn as one model from the models root with the texture of the same name, turned to + * the entity's heading: the container, {@code MCH_RenderContainer}. + */ +public class EquipmentRenderer extends EntityRenderer { + private final String model; + private final Identifier texture; + private final float yOffset; + private final int tint; + + /** + * @param model the file name under {@code models/}, without extension + * @param yOffset how far the model sits below the entity's position + * @param tint ARGB the model is multiplied by; the original greyed the container a little + */ + public EquipmentRenderer(EntityRendererProvider.Context context, String model, float yOffset, int tint, float shadow) { + super(context); + this.model = model; + this.texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + model + ".png"); + this.yOffset = yOffset; + this.tint = tint; + this.shadowRadius = shadow; + } + + public static class State extends EntityRenderState { + float yaw; + float pitch; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(T entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("", model); + if (mesh != null) { + pose.pushPose(); + pose.translate(0.0F, -yOffset, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, tint)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From bb63f6cfecf8278087980a34d6f49d0b55aa8ab6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:29 +0800 Subject: [PATCH 0453/1461] Add client/render/FlareRenderer.java to the 26.1 shared sources --- .../mcheli/client/render/FlareRenderer.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java new file mode 100644 index 00000000..66cffb45 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and the burning debris of a crash were only their particle trails. Not in the original: + * a burning flare now glows, which is what makes a flare salvo light up the night sky. Smoke + * grenades stay invisible. + */ +public class FlareRenderer extends EntityRenderer { + private static final float RADIUS = 1.4F; + + public FlareRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + float glow; + float flicker = 1.0F; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(FlareEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.glow = entity.isSmoke() || entity.isInWater() ? 0.0F : Glow.strength(entity.level(), entity.blockPosition()); + // A cheap waver keyed to the entity, so neighbouring flares do not pulse in step. + state.flicker = 0.8F + 0.2F * (float) Math.sin((entity.tickCount + partialTicks) * 1.7F + entity.getId()); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Glow.halo(pose, collector, camera, Vec3.ZERO, RADIUS * state.flicker, GlowParticleOptions.FIRE, state.glow); + Glow.halo(pose, collector, camera, Vec3.ZERO, RADIUS * 0.35F, GlowParticleOptions.FLASH, state.glow); + super.submit(state, pose, collector, camera); + } +} From 9d555d81d5c2502c48ea4d5be81f62e9b6f0975e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:29 +0800 Subject: [PATCH 0454/1461] Add client/render/GlassSplit.java to the 26.1 shared sources --- .../mcheli/client/render/GlassSplit.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java new file mode 100644 index 00000000..d2a46857 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.resources.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Sorts a model's triangles into opaque ones and see-through glass, by the alpha of the texture + * under each triangle. + * + *

Drawing a whole vehicle translucent made its glass write depth before the game drew water, + * so water behind a canopy vanished. Opaque triangles now draw as cutout, like any entity, and + * only the glass draws translucent, through a type that leaves the depth buffer alone. + */ +final class GlassSplit { + static final GlassSplit INSTANCE = new GlassSplit(); + private static final Logger LOGGER = LoggerFactory.getLogger(GlassSplit.class); + /** Texels between these alphas are glass; fully clear ones are cut out, solid ones opaque. */ + private static final int MIN_GLASS_ALPHA = 10; + private static final int MAX_GLASS_ALPHA = 245; + + record Split(List opaque, List glass) { + static final Split EMPTY = new Split(List.of(), List.of()); + } + + private record Alpha(int width, int height, byte[] values) { + int at(float u, float v) { + int x = Math.floorMod((int) Math.floor(u * width), width); + int y = Math.floorMod((int) Math.floor(v * height), height); + return values[y * width + x] & 0xFF; + } + } + + private final Map> textures = new HashMap<>(); + private final Map, Map> splits = new IdentityHashMap<>(); + private final Map, Map, List>> buckets = new IdentityHashMap<>(); + + private GlassSplit() { + } + + /** Called from the render thread only. */ + Split split(List groups, Identifier texture) { + if (groups.isEmpty()) { + return Split.EMPTY; + } + return splits.computeIfAbsent(groups, g -> new HashMap<>()).computeIfAbsent(texture, t -> compute(groups, t)); + } + + /** + * A part's objects by the texture each draws with, for a vehicle with {@code PartTexture} lines. + * The lists stay the same objects from frame to frame, so {@link #split} can remember them too. + * Called from the render thread only. + */ + List buckets(List groups, Map overrides) { + return buckets.computeIfAbsent(groups, g -> new HashMap<>()) + .computeIfAbsent(Map.copyOf(overrides), o -> PartTextures.assign(groups, o)); + } + + void clear() { + textures.clear(); + splits.clear(); + buckets.clear(); + } + + private Split compute(List groups, Identifier texture) { + Alpha alpha = textures.computeIfAbsent(texture, GlassSplit::load).orElse(null); + if (alpha == null) { + return new Split(groups, List.of()); + } + List opaque = new ArrayList<>(); + List glass = new ArrayList<>(); + for (MeshGroup group : groups) { + split(group, alpha, opaque, glass); + } + return new Split(List.copyOf(opaque), List.copyOf(glass)); + } + + private static void split(MeshGroup group, Alpha alpha, List opaque, List glass) { + float[] uv = group.uvs(); + int triangles = group.vertexCount() / 3; + boolean[] isGlass = new boolean[triangles]; + int glassCount = 0; + for (int t = 0; t < triangles; t++) { + int i = t * 6; + float u = (uv[i] + uv[i + 2] + uv[i + 4]) / 3.0F; + float v = (uv[i + 1] + uv[i + 3] + uv[i + 5]) / 3.0F; + int a = alpha.at(u, v); + if (a > MIN_GLASS_ALPHA && a < MAX_GLASS_ALPHA) { + isGlass[t] = true; + glassCount++; + } + } + if (glassCount == 0) { + opaque.add(group); + } else if (glassCount == triangles) { + glass.add(group); + } else { + opaque.add(pick(group, isGlass, false, triangles - glassCount)); + glass.add(pick(group, isGlass, true, glassCount)); + } + } + + private static MeshGroup pick(MeshGroup group, boolean[] isGlass, boolean wanted, int count) { + float[] p = new float[count * 9]; + float[] n = new float[count * 9]; + float[] uv = new float[count * 6]; + int out = 0; + for (int t = 0; t < isGlass.length; t++) { + if (isGlass[t] != wanted) { + continue; + } + System.arraycopy(group.positions(), t * 9, p, out * 9, 9); + System.arraycopy(group.normals(), t * 9, n, out * 9, 9); + System.arraycopy(group.uvs(), t * 6, uv, out * 6, 6); + out++; + } + return new MeshGroup(group.name(), p, n, uv); + } + + private static Optional load(Identifier texture) { + try (InputStream in = RuntimeAssets.open(texture)) { + if (in == null) { + return Optional.empty(); + } + return Optional.of(read(in)); + } catch (IOException | RuntimeException e) { + LOGGER.warn("Could not read {} to find its glass; drawing it opaque", texture, e); + return Optional.empty(); + } + } + + private static Alpha read(InputStream in) throws IOException { + try (NativeImage image = NativeImage.read(in)) { + int w = image.getWidth(); + int h = image.getHeight(); + byte[] values = new byte[w * h]; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + values[y * w + x] = (byte) (image.getPixel(x, y) >>> 24); + } + } + return new Alpha(w, h, values); + } + } +} From d54e8ac23ca25bf0e7a4ebec8d9e6ef7643a9dd5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:30 +0800 Subject: [PATCH 0455/1461] Build the glow pipelines with the normal depth test and sampler names of 26.1 --- .../mcheli/client/render/Glow.java | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java new file mode 100644 index 00000000..230d0f30 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java @@ -0,0 +1,211 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.effect.NightGlow; +import com.mojang.blaze3d.pipeline.BlendFunction; +import com.mojang.blaze3d.pipeline.ColorTargetState; +import com.mojang.blaze3d.pipeline.DepthStencilState; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.platform.CompareOp; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.rendertype.RenderSetup; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.util.ARGB; +import net.minecraft.util.LightCoordsUtil; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.phys.Vec3; + +/** + * Not in the original: light that adds to the scene instead of covering it, for tracers, rocket + * motors, flares and fireballs. Everything here blends additively and leaves the depth buffer + * alone, so overlapping glows brighten each other and never cut holes in the smoke behind them. + * With order independent transparency on, the vanilla translucent pipelines stand in. + */ +public final class Glow { + /** A white disc fading to nothing at the rim; also the {@code mcheli:glow} particle sprite. */ + public static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/particle/glow.png"); + + private static final DepthStencilState TEST_ONLY = new DepthStencilState(CompareOp.LESS_THAN_OR_EQUAL, false); + + private static final RenderPipeline PARTICLE_PIPELINE = RenderPipeline.builder(RenderPipelines.PARTICLE_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/glow_particle")) + .withColorTargetState(new ColorTargetState(BlendFunction.LIGHTNING)) + .withDepthStencilState(TEST_ONLY) + .build(); + + /** + * Vanilla's translucent particles with depth writes off. A puff's clear edge would otherwise + * hide the puffs drawn after it, and a smoke cloud is mostly overlapping puffs. + */ + private static final RenderPipeline SMOKE_PIPELINE = RenderPipeline.builder(RenderPipelines.PARTICLE_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/smoke_particle")) + .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) + .withDepthStencilState(TEST_ONLY) + .build(); + + private static final RenderPipeline ENTITY_PIPELINE = RenderPipeline.builder(RenderPipelines.MATRICES_FOG_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/glow")) + .withVertexShader("core/entity") + .withFragmentShader("core/entity") + .withShaderDefine("EMISSIVE") + .withShaderDefine("NO_OVERLAY") + .withShaderDefine("NO_CARDINAL_LIGHTING") + .withSampler("Sampler0") + .withVertexFormat(DefaultVertexFormat.ENTITY, VertexFormat.Mode.QUADS) + .withColorTargetState(new ColorTargetState(BlendFunction.LIGHTNING)) + .withDepthStencilState(TEST_ONLY) + .build(); + + /** The particle layer for {@code mcheli:glow}. */ + @SuppressWarnings("deprecation") // Vanilla's own particle layers still name the atlas this way. + public static final SingleQuadParticle.Layer PARTICLE_LAYER = new SingleQuadParticle.Layer( + true, TextureAtlas.LOCATION_PARTICLES, PARTICLE_PIPELINE); + + /** The particle layer for {@code mcheli:smoke}. */ + @SuppressWarnings("deprecation") + public static final SingleQuadParticle.Layer SMOKE_LAYER = new SingleQuadParticle.Layer( + true, TextureAtlas.LOCATION_PARTICLES, SMOKE_PIPELINE); + + private static final RenderType HALO = RenderType.create(MCHeli.MOD_ID + "_glow", RenderSetup.builder(ENTITY_PIPELINE) + .withTexture("Sampler0", TEXTURE) + .sortOnUpload() + .createRenderSetup()); + + private static final int FULL_BRIGHT = LightCoordsUtil.FULL_BRIGHT; + + private Glow() { + } + + /** + * Hands our pipelines to Iris, so a shader pack draws them with a matching program: the halos + * and tracers as emissive entities, the glow and smoke particles as translucent particles. + * Called once the mods are loaded; without Iris it does nothing. + */ + public static void assignShaderPrograms() { + IrisCompat.assign(ENTITY_PIPELINE, "EMISSIVE_ENTITIES"); + IrisCompat.assign(PARTICLE_PIPELINE, "PARTICLES_TRANSLUCENT"); + IrisCompat.assign(SMOKE_PIPELINE, "PARTICLES_TRANSLUCENT"); + } + + /** + * How strongly something at {@code pos} glows: faint by day, full at night, in caves and + * under water. 0 when the player turned the glow off. + */ + public static float strength(Level level, BlockPos pos) { + if (!ClientConfig.nightGlow()) { + return 0.0F; + } + return NightGlow.strength(level.getBrightness(LightLayer.BLOCK, pos), level.getBrightness(LightLayer.SKY, pos), level.getSkyDarken()); + } + + /** + * A disc of light facing the camera, centred {@code offset} away from the current pose. + * + * @param color RGB; {@code strength} becomes the alpha, which scales the light added + */ + public static void halo(PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera, Vec3 offset, float radius, int color, + float strength) { + if (strength <= 0.0F || radius <= 0.0F) { + return; + } + int argb = ARGB.color(alpha(strength), color); + pose.pushPose(); + pose.translate(offset); + pose.mulPose(camera.orientation); + collector.submitCustomGeometry(pose, HALO, (p, buffer) -> { + // Both windings, so culling never hides it whichever way the quad ends up facing. + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + }); + pose.popPose(); + } + + /** + * A tracer: a ribbon of light from the current pose back along {@code tail}, turned to face + * the camera, bright at the head and fading out towards the end. + * + * @param toCamera from the camera to the head, so the ribbon can be turned side on to the view + */ + public static void streak(PoseStack pose, SubmitNodeCollector collector, Vec3 toCamera, Vec3 tail, float width, int color, float strength) { + if (strength <= 0.0F || tail.lengthSqr() < 1.0E-4) { + return; + } + Vec3 side = tail.cross(toCamera); + if (side.lengthSqr() < 1.0E-8) { + // Seen exactly end on; the halo at the head covers this. + return; + } + side = side.normalize().scale(width); + int head = ARGB.color(alpha(strength), color); + int end = ARGB.color(0, color); + float sx = (float) side.x; + float sy = (float) side.y; + float sz = (float) side.z; + float tx = (float) tail.x; + float ty = (float) tail.y; + float tz = (float) tail.z; + collector.submitCustomGeometry(pose, HALO, (p, buffer) -> { + // The middle row of the disc texture gives the ribbon soft edges. + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + }); + } + + /** + * A search light's beam, {@code renderLight}: a cone {@code height} long along +Y to a disc + * {@code radius} across, fading from {@code start} at the lamp to {@code end} at the rim (ARGB). + */ + public static void cone(PoseStack pose, SubmitNodeCollector collector, float height, float radius, int start, int end) { + collector.submitCustomGeometry(pose, HALO, (p, buffer) -> { + int segments = 24; + for (int i = 0; i < segments; i++) { + float a0 = i * Mth.TWO_PI / segments; + float a1 = (i + 1) * Mth.TWO_PI / segments; + // A quad with the apex twice is the fan's triangle; the texture's bright middle colours it. + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, Mth.sin(a0) * radius, height, Mth.cos(a0) * radius, 0.5F, 0.5F, end); + vertex(buffer, p, Mth.sin(a1) * radius, height, Mth.cos(a1) * radius, 0.5F, 0.5F, end); + } + }); + } + + private static int alpha(float strength) { + return Math.clamp(Math.round(strength * 255.0F), 0, 255); + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float x, float y, float z, float u, float v, int argb) { + buffer.addVertex(pose, x, y, z) + .setColor(argb) + .setUv(u, v) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(FULL_BRIGHT) + .setNormal(pose, 0.0F, 1.0F, 0.0F); + } +} From df53774f9a88d9f8f49ce517c2fe89d6bcabcd56 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:30 +0800 Subject: [PATCH 0456/1461] Add client/render/GltdRenderer.java to the 26.1 shared sources --- .../mcheli/client/render/GltdRenderer.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java new file mode 100644 index 00000000..02cc82ba --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * {@code MCH_RenderGLTD}: the tripod, its head turned to where the viewer aims, and a faint red + * laser out of the head while someone uses it. The viewer's own head part is left out in first + * person, where it would fill the view. + */ +public class GltdRenderer extends EntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gltd.png"); + private static final float HEAD_PIVOT = 0.45F; + private static final double LASER_LENGTH = 100.0; + private static final int LASER = 0xFF2020; + + public GltdRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + public static class State extends EntityRenderState { + float yaw; + float headYaw; + float headPitch; + boolean drawHead = true; + boolean laser; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(GltdEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + Entity viewer = entity.getFirstPassenger(); + Minecraft mc = Minecraft.getInstance(); + state.laser = viewer != null; + state.drawHead = viewer == null || viewer != mc.player || !mc.options.getCameraType().isFirstPerson(); + state.headYaw = viewer != null ? viewer.getYRot(partialTicks) : state.yaw; + state.headPitch = viewer != null ? viewer.getXRot(partialTicks) : 0.0F; + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("", "gltd"); + if (mesh != null) { + int light = state.lightCoords; + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + draw(mesh.part("$body"), pose, collector, light); + pose.popPose(); + if (state.drawHead) { + pose.pushPose(); + pose.translate(0.0F, HEAD_PIVOT, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-state.headYaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.headPitch)); + pose.translate(0.0F, -HEAD_PIVOT, 0.0F); + draw(mesh.part("$head"), pose, collector, light); + pose.popPose(); + } + } + if (state.laser) { + float yaw = state.headYaw * Mth.DEG_TO_RAD; + float pitch = state.headPitch * Mth.DEG_TO_RAD; + Vec3 look = new Vec3(-Mth.sin(yaw) * Mth.cos(pitch), -Mth.sin(pitch), Mth.cos(yaw) * Mth.cos(pitch)); + Vec3 head = new Vec3(0.0, HEAD_PIVOT + 0.2, 0.0); + Vec3 toCamera = new Vec3(state.x, state.y, state.z).add(head).subtract(camera.pos); + pose.pushPose(); + pose.translate(head); + // Brightest at the tripod, fading out along the beam. + Glow.streak(pose, collector, toCamera, look.scale(LASER_LENGTH), 0.02F, LASER, 0.25F); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } + + private static void draw(List groups, PoseStack pose, SubmitNodeCollector collector, int light) { + if (!groups.isEmpty()) { + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(TEXTURE), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY)); + } + } +} From ca6e32e3c77dc2e94d78f891c05c78721ee9e837 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:30 +0800 Subject: [PATCH 0457/1461] Add client/render/IrisCompat.java to the 26.1 shared sources --- .../mcheli/client/render/IrisCompat.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java new file mode 100644 index 00000000..3803102e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.pipeline.RenderPipeline; + +import java.lang.reflect.Method; + +/** + * Tells Iris which shader program draws our own render pipelines. Iris only knows the vanilla + * pipelines and draws any other with a program whose vertex layout does not match, which throws + * the vertices far off: the glows turn into long coloured lines running to the horizon. Iris is + * optional, so its API is looked up by reflection instead of compiled against. + */ +final class IrisCompat { + private static final String API = "net.irisshaders.iris.api.v0.IrisApi"; + private static final String PROGRAM = "net.irisshaders.iris.api.v0.IrisProgram"; + + private IrisCompat() { + } + + /** + * Assigns {@code pipeline} to the Iris program named {@code program}, such as + * {@code EMISSIVE_ENTITIES}. Does nothing without Iris. + * + * @return whether Iris took it + */ + static boolean assign(RenderPipeline pipeline, String program) { + Class api; + try { + api = Class.forName(API); + } catch (ClassNotFoundException absent) { + MCHeli.LOGGER.debug("Iris is not installed, {} keeps its own shader", pipeline.getLocation()); + return false; + } + try { + Class programs = Class.forName(PROGRAM); + Object instance = api.getMethod("getInstance").invoke(null); + Object target = programs.getField(program).get(null); + Method assign = api.getMethod("assignPipeline", RenderPipeline.class, programs); + assign.invoke(instance, pipeline, target); + MCHeli.LOGGER.debug("Iris draws {} with its {} program", pipeline.getLocation(), program); + return true; + } catch (ReflectiveOperationException | LinkageError e) { + // An Iris without this API: glows may look wrong under a shader pack, nothing worse. + MCHeli.LOGGER.warn("Could not hand {} to Iris, glows may look wrong with shaders: {}", pipeline.getLocation(), e.toString()); + return false; + } + } +} From 9c13664906c143a2f3986d5e07897334f66cc4d6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:31 +0800 Subject: [PATCH 0458/1461] Add client/render/MeshCache.java to the 26.1 shared sources --- .../mcheli/client/render/MeshCache.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java new file mode 100644 index 00000000..57dad7e5 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshLoader; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Lazily loads vehicle models, from the resource packs (so they can replace them) or else from the + * runtime roots of {@link RuntimeAssets}. Models are parsed the first time a vehicle of that kind + * is drawn, and dropped on resource reload and on content reload ({@link #clear}). + */ +public final class MeshCache implements ResourceManagerReloadListener { + public static final MeshCache INSTANCE = new MeshCache(); + private static final Logger LOGGER = LoggerFactory.getLogger(MeshCache.class); + private static final String[] EXTENSIONS = {".mqo", ".obj"}; + + private final Map> meshes = new ConcurrentHashMap<>(); + + private MeshCache() { + } + + /** + * @param folder content folder, for example {@code helicopters}, or empty for the models root + * @param name definition name + * @return the mesh, or {@code null} if no model exists or it failed to load + */ + public @Nullable Mesh get(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, true)).orElse(null); + } + + /** Like {@link #get}, for optional models such as separate part files; a missing one is not logged. */ + public @Nullable Mesh find(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, false)).orElse(null); + } + + private static Optional load(String folder, String name, boolean required) { + for (String extension : EXTENSIONS) { + Identifier id = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, + (folder.isEmpty() ? "models/" : "models/" + folder + "/") + name + extension); + try (InputStream in = RuntimeAssets.open(id)) { + if (in == null) { + continue; + } + Mesh mesh = MeshLoader.load(id.toString(), in); + LOGGER.debug("Loaded {}", mesh); + return Optional.of(mesh); + } catch (IOException | RuntimeException e) { + LOGGER.error("Failed to load model {}", id, e); + return Optional.empty(); + } + } + if (required) { + LOGGER.warn("No model found for {}/{}", folder, name); + } + return Optional.empty(); + } + + /** Forgets every model and glass split, so the next frame reads them again from disk. */ + public void clear() { + meshes.clear(); + GlassSplit.INSTANCE.clear(); + } + + @Override + public void onResourceManagerReload(ResourceManager resourceManager) { + clear(); + RuntimeAssets.clearTextures(); + } +} From a92eb1731e1157c4a620bed9bdc563e5553f9d39 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:31 +0800 Subject: [PATCH 0459/1461] Add client/render/MeshRenderer.java to the 26.1 shared sources --- .../mcheli/client/render/MeshRenderer.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java new file mode 100644 index 00000000..47cc66e3 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java @@ -0,0 +1,88 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.List; + +/** Streams {@link MeshGroup} triangles into an entity render type. */ +final class MeshRenderer { + private static final int WHITE = 0xFFFFFFFF; + + private MeshRenderer() { + } + + /** + * Entity render types draw quads, so each triangle is sent as a quad whose last corner repeats + * the third one. The GPU discards the degenerate half for free. + */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay) { + emit(groups, pose, buffer, light, overlay, WHITE); + } + + /** As above, tinted with an ARGB colour, for bullets drawn in their {@code BulletColor}. */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + /** + * As above, but only the triangles that face the camera. No vanilla type is both culled and + * free of depth writes, so glass is culled here. Back to back glass faces would otherwise sort + * in an arbitrary order and swap places as the view moves. + * + *

Entities are submitted in world-aligned space with the camera at the origin, so a front + * face (counter-clockwise, as in the original's GL setup) is one whose normal points back + * towards the origin. + */ + static void emitFrontFaces(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + Matrix4f m = pose.pose(); + Vector3f a = new Vector3f(); + Vector3f b = new Vector3f(); + Vector3f c = new Vector3f(); + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + m.transformPosition(p[v * 3], p[v * 3 + 1], p[v * 3 + 2], a); + m.transformPosition(p[v * 3 + 3], p[v * 3 + 4], p[v * 3 + 5], b); + m.transformPosition(p[v * 3 + 6], p[v * 3 + 7], p[v * 3 + 8], c); + b.sub(a); + c.sub(a); + if (b.cross(c).dot(a) >= 0.0F) { + continue; + } + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float[] p, float[] n, float[] uv, int i, int light, int overlay, int color) { + int a = i * 3; + int t = i * 2; + buffer.addVertex(pose, p[a], p[a + 1], p[a + 2]) + .setColor(color) + .setUv(uv[t], uv[t + 1]) + .setOverlay(overlay) + .setLight(light) + .setNormal(pose, n[a], n[a + 1], n[a + 2]); + } +} From e0382a224962177f17dc216110548d3cc497d7d0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:32 +0800 Subject: [PATCH 0460/1461] Add client/render/ParachuteRenderer.java to the 26.1 shared sources --- .../client/render/ParachuteRenderer.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java new file mode 100644 index 00000000..15ff27a2 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +import java.util.List; + +/** + * {@code MCH_RenderParachute}: the item's round canopy, or the ejection seat with its canopy, + * from the models the import brought in. + */ +public class ParachuteRenderer extends EntityRenderer { + private static final Identifier ROUND = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute1.png"); + private static final Identifier SEAT = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute2.png"); + + public ParachuteRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + public static class State extends EntityRenderState { + float yaw; + ParachuteEntity.Kind kind = ParachuteEntity.Kind.ITEM; + boolean open; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ParachuteEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.kind = entity.kind(); + state.open = entity.isOpen(); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + List groups = groups(state); + if (!groups.isEmpty()) { + Identifier texture = state.kind == ParachuteEntity.Kind.ITEM ? ROUND : SEAT; + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } + + private static List groups(State state) { + if (state.kind == ParachuteEntity.Kind.ITEM) { + Mesh mesh = MeshCache.INSTANCE.get("", "parachute1"); + return mesh != null ? mesh.groups() : List.of(); + } + Mesh mesh = MeshCache.INSTANCE.get("", "parachute2"); + boolean canopy = state.kind == ParachuteEntity.Kind.DROP || state.open; + return mesh != null ? mesh.part(canopy ? "$parachute" : "$seat") : List.of(); + } +} From 8dba4d86c5b38442c41556eb4a27958f0ba9a8cf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:32 +0800 Subject: [PATCH 0461/1461] Add client/render/PartRenderer.java to the 26.1 shared sources --- .../mcheli/client/render/PartRenderer.java | 554 ++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java new file mode 100644 index 00000000..d6eefbe0 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java @@ -0,0 +1,554 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.CameraPart; +import com.jnhprojects.mcheli.content.aircraft.part.CrawlerTrack; +import com.jnhprojects.mcheli.content.aircraft.part.LandingGearPart; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.content.aircraft.part.PropellerPart; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import com.jnhprojects.mcheli.content.aircraft.part.ThrottlePart; +import com.jnhprojects.mcheli.content.aircraft.part.TrackRoller; +import com.jnhprojects.mcheli.content.aircraft.part.VehiclePart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.content.aircraft.part.WheelPart; +import com.jnhprojects.mcheli.content.aircraft.part.WingPart; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import org.joml.Vector3f; + +import java.util.List; + +/** + * Submits a vehicle's hull and moving parts for one frame. A port of the part routines in + * {@code MCH_RenderAircraft} and the per kind renderers; every transform is applied in hull space + * on top of the pose the caller already set up. + * + *

A part is looked up as {@code $name} inside the vehicle model, or as a separate + * {@code _} model file, the two places the original accepted. + */ +final class PartRenderer { + private static final Vector3 X_AXIS = new Vector3(1, 0, 0); + private static final Vector3 DOWN_AXIS = new Vector3(0, -1, 0); + + private final AircraftDefinition def; + private final Mesh mesh; + private final AircraftRenderState state; + private final PoseStack pose; + private final SubmitNodeCollector collector; + private final Identifier texture; + private final RenderType opaqueType; + private final RenderType glassType; + private final int light; + private final int overlay; + /** Glass draws unlit, so its colour carries the brightness of where the vehicle stands. */ + private final int glassColor; + + PartRenderer(AircraftDefinition def, Mesh mesh, AircraftRenderState state, PoseStack pose, SubmitNodeCollector collector, + Identifier texture, int light, int overlay, int glassColor) { + this.def = def; + this.mesh = mesh; + this.state = state; + this.pose = pose; + this.collector = collector; + this.texture = texture; + // Culled, as the original drew it. The models are full of back to back faces for thin + // panels, and drawing both sides of those makes them fight for the same depth. + this.opaqueType = RenderTypes.entityCutoutCull(texture); + // The one entity translucent type that does not write depth, so water behind glass still + // draws. It does not cull, so MeshRenderer drops the back faces itself. + this.glassType = RenderTypes.entityTranslucentEmissive(texture); + this.light = light; + this.overlay = overlay; + this.glassColor = glassColor; + } + + void submitAll() { + switch (def) { + case PlaneDefinition plane -> { + nozzles(plane); + wings(plane); + propellers(plane); + hull(); + } + case HelicopterDefinition heli -> { + hull(); + rotors(heli); + } + case VehicleDefinition vehicle -> { + hull(); + for (VehiclePart part : vehicle.parts) { + vehiclePart(vehicle, part); + } + } + default -> hull(); + } + weapons(); + rotatingParts(); + movingParts(def.hatches, state.hatch); + trackRollers(); + crawlerTracks(); + steeringWheels(); + wheels(); + throttles(); + cameras(); + landingGear(); + weaponBays(); + movingParts(def.canopies, state.canopy); + } + + /** + * The body and, as a {@code $} object of its own so it can have its own {@code PartTexture}, + * the cockpit. A model without {@code $body} draws as one piece, cockpit included, and no original + * model has a {@code $cockpit}. + */ + private void hull() { + submit(mesh.body()); + if (mesh.contains("$body")) { + submit(mesh.part("$cockpit")); + } + } + + // Kind specific parts + + private void rotors(HelicopterDefinition heli) { + for (int r = 0; r < heli.rotors.size(); r++) { + RotorDefinition rotor = heli.rotors.get(r); + List blade = part(rotor.part()); + if (blade.isEmpty() || isZero(rotor.axis())) { + continue; + } + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + int blades = Math.max(rotor.blades(), 1); + for (int i = 0; i < blades; i++) { + pose.pushPose(); + Vector3 p = rotor.position(); + translate(p, 1.0F); + float[] angles = r < state.bladeAngles.length ? state.bladeAngles[r] : null; + rotate(angles != null && i < angles.length ? angles[i] : state.rotorAngle + i * step, rotor.axis()); + if (!rotor.legacyPivot()) { + translate(p, -1.0F); + } + submit(blade); + pose.popPose(); + } + } + } + + private void nozzles(PlaneDefinition plane) { + for (MovingPart nozzle : plane.nozzles) { + pose.pushPose(); + rotateAround(nozzle.position(), state.nozzle, nozzle.axis()); + submit(part(nozzle.part())); + pose.popPose(); + } + } + + private void wings(PlaneDefinition plane) { + for (WingPart wing : plane.wings) { + pose.pushPose(); + rotateAround(wing.position(), state.wing * wing.factor(), wing.axis()); + submit(part(wing.part())); + for (MovingPart pylon : wing.pylons()) { + pose.pushPose(); + rotateAround(pylon.position(), state.wing * pylon.factor(), pylon.axis()); + submit(part(pylon.part())); + pose.popPose(); + } + pose.popPose(); + } + } + + /** Tilt rotors follow the nozzle state, the blades spin with the engine. */ + private void propellers(PlaneDefinition plane) { + if (!plane.hasNozzleState()) { + return; + } + for (PropellerPart rotor : plane.propellers) { + pose.pushPose(); + rotateAround(rotor.position(), state.nozzle * rotor.factor(), rotor.axis()); + submit(part(rotor.part())); + for (PropellerPart.Blade blade : rotor.blades()) { + List groups = part(blade.part()); + pose.pushPose(); + rotateAround(blade.position(), state.rotorAngle, blade.axis()); + for (int i = 0; i < blade.count(); i++) { + rotateAround(blade.position(), blade.step(), blade.axis()); + submit(groups); + } + pose.popPose(); + } + pose.popPose(); + } + } + + /** {@code MCH_RenderVehicle.drawPart}: mount parts turn with the gunner's view. */ + private void vehiclePart(VehicleDefinition vehicle, VehiclePart part) { + pose.pushPose(); + if (part.yaw() || part.pitch() || part.type() == VehiclePart.TYPE_ROTATING_BARREL) { + translate(part.position(), 1.0F); + if (part.yaw()) { + rotate(seatYaw(0), DOWN_AXIS); + } + if (part.pitch()) { + rotate(Mth.clamp(seatPitch(0), vehicle.minRotationPitch, vehicle.maxRotationPitch), X_AXIS); + } + translate(part.position(), -1.0F); + } + for (VehiclePart child : part.children()) { + vehiclePart(vehicle, child); + } + boolean gunnerLooking = state.localSeat == 0 && state.localFirstPerson; + if (part.drawFirstPerson() || !gunnerLooking) { + submit(part(part.part())); + } + pose.popPose(); + } + + // Parts every kind can have + + /** + * Gun mounts, turrets and missiles, {@code renderWeapon}. Each follows its weapon's aim, kicks + * back as it fires, spins its barrels and, for missiles, disappears until reloaded. A part + * whose weapon is missing follows the view of the seat that would fire it. + */ + private void weapons() { + float turretYaw = seatYaw(0); + for (int i = 0; i < def.weaponParts.size(); i++) { + WeaponPart w = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState ws = i < state.weaponParts.length ? state.weaponParts[i] : null; + boolean live = ws != null && ws.hasWeapon; + WeaponMount mount = mount(w); + int seat = mount != null ? mount.seatIndex() : 0; + if (w.hideInFirstPerson() && state.localFirstPerson && state.localSeat == seat) { + continue; + } + float defaultYaw = live ? ws.defaultYaw : mount != null ? mount.defaultYaw() : 0.0F; + float yaw; + float pitch; + if (live) { + yaw = ws.yaw; + pitch = ws.pitch; + } else { + yaw = Mth.wrapDegrees(seatYaw(seat) - defaultYaw); + pitch = seatPitch(seat); + } + + pose.pushPose(); + if (w.turret()) { + rotateAround(def.turretPosition, turretYaw, DOWN_AXIS); + } + translate(w.position(), 1.0F); + if (w.yaw()) { + rotate(w.turret() && !live ? yaw - turretYaw : yaw, DOWN_AXIS); + } + if (w.turret() && live) { + rotate(-(turretYaw - ws.turretLag), DOWN_AXIS); + } + // Side facing guns pitch around their own axis, which the original did by turning + // into the gun's frame and flipping the sign for guns pointing left or right. + boolean sideways = false; + if ((int) defaultYaw != 0) { + float t = Mth.wrapDegrees(defaultYaw); + sideways = (t >= 45.0F && t <= 135.0F) || (t <= -45.0F && t >= -135.0F); + rotate(-defaultYaw, DOWN_AXIS); + } + if (w.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && w.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, w.recoil() * ws.recoil); + } + if (mount != null || live) { + rotate(defaultYaw, DOWN_AXIS); + } + if (live && w.rotatingBarrel()) { + rotate(ws.barrel, w.axis()); + } + translate(w.position(), -1.0F); + if (!w.missile() || !live || !ws.hidden) { + submit(part(w.part())); + for (WeaponPart.Child child : w.children()) { + pose.pushPose(); + translate(child.position(), 1.0F); + if (child.yaw()) { + rotate(yaw, DOWN_AXIS); + } + if (live && (int) defaultYaw != 0) { + rotate(-defaultYaw, DOWN_AXIS); + } + if (child.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && child.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, -child.recoil() * ws.recoil); + } + if (live && (int) defaultYaw != 0) { + rotate(defaultYaw, DOWN_AXIS); + } + translate(child.position(), -1.0F); + submit(part(child.part())); + pose.popPose(); + } + } + pose.popPose(); + } + } + + private WeaponMount mount(WeaponPart part) { + if (part.weapons().isEmpty()) { + return null; + } + int group = def.weaponGroup(part.weapons().getFirst()); + return group >= 0 ? def.weaponGroups.get(group).mounts().getFirst() : null; + } + + private void rotatingParts() { + for (int i = 0; i < def.rotatingParts.size() && i < state.rotatingParts.length; i++) { + RotatingPart part = def.rotatingParts.get(i); + pose.pushPose(); + rotateAround(part.position(), state.rotatingParts[i], part.axis()); + submit(part(part.part())); + pose.popPose(); + } + } + + /** Hatches and canopies: swing by {@code angle * factor}, or slide along their offset. */ + private void movingParts(List parts, float angle) { + for (MovingPart part : parts) { + pose.pushPose(); + moveOrSwing(part, angle); + submit(part(part.part())); + pose.popPose(); + } + } + + private void moveOrSwing(MovingPart part, float angle) { + if (part.slide()) { + translate(part.position(), angle / 90.0F); + } else { + rotateAround(part.position(), angle * part.factor(), part.axis()); + } + } + + private void trackRollers() { + for (TrackRoller roller : def.trackRollers) { + pose.pushPose(); + rotateAround(roller.position(), state.roller[roller.side()], X_AXIS); + submit(part(roller.part())); + pose.popPose(); + } + } + + /** + * Each link slides towards the next one by the track's run, so the loop appears to move. + * The link part is modelled at the origin and placed at each point of the outline. + */ + private void crawlerTracks() { + for (CrawlerTrack track : def.crawlerTracks) { + List link = part(track.part()); + List links = track.links(); + int count = links.size() - 1; + if (link.isEmpty() || count < 1) { + continue; + } + float run = state.track[track.side()]; + for (int i = 0; i < count; i++) { + CrawlerTrack.Link cur = links.get(i); + CrawlerTrack.Link next = links.get((i + 1) % count); + float angleNext = next.angle(); + if (angleNext - cur.angle() < -180.0F) { + angleNext += 360.0F; + } else if (angleNext - cur.angle() > 180.0F) { + angleNext -= 360.0F; + } + pose.pushPose(); + pose.translate(0.0F, cur.y() + (next.y() - cur.y()) * run, cur.z() + (next.z() - cur.z()) * run); + pose.mulPose(Axis.XN.rotationDegrees(cur.angle() + (angleNext - cur.angle()) * run)); + submit(link); + pose.popPose(); + } + } + } + + private void steeringWheels() { + for (WheelPart wheel : def.steeringWheels) { + pose.pushPose(); + rotateAround(wheel.position(), state.steer * wheel.steerAngle(), wheel.axis()); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void wheels() { + for (WheelPart wheel : def.wheels) { + pose.pushPose(); + rotateAround(wheel.pivot(), state.steer * wheel.steerAngle(), wheel.axis()); + rotateAround(wheel.position(), state.wheelRoll, X_AXIS); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void throttles() { + for (ThrottlePart lever : def.throttleParts) { + pose.pushPose(); + rotateAround(lever.position(), state.throttle * lever.angle(), lever.axis()); + translate(lever.slide(), state.throttle); + submit(part(lever.part())); + pose.popPose(); + } + } + + /** Sensor turrets look where the gunner looks, or the pilot when the gunner seat is empty. */ + private void cameras() { + int seat = state.seatOccupied.length > 1 && state.seatOccupied[1] ? 1 : 0; + for (CameraPart camera : def.cameraParts) { + pose.pushPose(); + translate(camera.position(), 1.0F); + if (camera.yawSync()) { + rotate(seatYaw(seat), DOWN_AXIS); + } + if (camera.pitchSync()) { + rotate(seatPitch(seat), X_AXIS); + } + translate(camera.position(), -1.0F); + submit(part(camera.part())); + pose.popPose(); + } + } + + /** + * Gear legs swing up with the gear state; reversed parts close as the gear rises, and gear bay + * doors open fully halfway through and shut again. + */ + private void landingGear() { + float angle = state.landingGear; + float reverse = 90.0F - angle; + float door = Math.min(90.0F, 90.0F * Mth.sin(angle * 2.0F * Mth.DEG_TO_RAD) * 3.0F); + for (LandingGearPart gear : def.landingGear) { + pose.pushPose(); + translate(gear.position(), 1.0F); + if (gear.reverse()) { + rotate(reverse * gear.factor(), gear.axis()); + } else { + rotate((gear.hatch() ? door : angle) * gear.factor(), gear.axis()); + } + if (gear.secondAxis() != null) { + rotate((gear.reverse() ? reverse : angle) * gear.secondFactor(), gear.secondAxis()); + } + translate(gear.position(), -1.0F); + if (gear.slide() != null) { + float f = angle / 90.0F; + translate(gear.slide(), gear.reverse() ? 1.0F - f : f); + } + submit(part(gear.part())); + pose.popPose(); + } + } + + /** Bays swing open while a crew member has one of their weapons selected. */ + private void weaponBays() { + for (int i = 0; i < def.weaponBays.size(); i++) { + WeaponBayPart bay = def.weaponBays.get(i); + pose.pushPose(); + moveOrSwing(bay.door(), i < state.bays.length ? state.bays[i] : 0.0F); + submit(part(bay.door().part())); + pose.popPose(); + } + } + + // Helpers + + private float seatYaw(int seat) { + return seat >= 0 && seat < state.seatYaw.length ? state.seatYaw[seat] : 0.0F; + } + + private float seatPitch(int seat) { + return seat >= 0 && seat < state.seatPitch.length ? state.seatPitch[seat] : 0.0F; + } + + private List part(String name) { + List groups = mesh.part("$" + name); + if (!groups.isEmpty()) { + return groups; + } + Mesh separate = MeshCache.INSTANCE.find(def.folder(), def.name() + "_" + name); + return separate != null ? separate.groups() : List.of(); + } + + private void submit(List groups) { + if (groups.isEmpty()) { + return; + } + if (def.partTextures.isEmpty()) { + submit(groups, texture, opaqueType, glassType); + return; + } + for (PartTextures.Bucket bucket : GlassSplit.INSTANCE.buckets(groups, def.partTextures)) { + if (bucket.texture() == null) { + submit(bucket.groups(), texture, opaqueType, glassType); + } else { + Identifier own = RuntimeAssets.texture(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, + "textures/" + def.folder() + "/" + bucket.texture() + ".png")); + submit(bucket.groups(), own, RenderTypes.entityCutoutCull(own), RenderTypes.entityTranslucentEmissive(own)); + } + } + } + + private void submit(List groups, Identifier texture, RenderType opaqueType, RenderType glassType) { + GlassSplit.Split split = GlassSplit.INSTANCE.split(groups, texture); + if (!split.opaque().isEmpty()) { + collector.submitCustomGeometry(pose, opaqueType, (p, buffer) -> MeshRenderer.emit(split.opaque(), p, buffer, light, overlay)); + } + if (!split.glass().isEmpty()) { + // Culling glass relies on the camera sitting at the origin, which is not so in a GUI. + collector.submitCustomGeometry(pose, glassType, state.inGui + ? (p, buffer) -> MeshRenderer.emit(split.glass(), p, buffer, light, overlay, glassColor) + : (p, buffer) -> MeshRenderer.emitFrontFaces(split.glass(), p, buffer, light, overlay, glassColor)); + } + } + + private void translate(Vector3 v, float scale) { + pose.translate((float) v.x() * scale, (float) v.y() * scale, (float) v.z() * scale); + } + + private void rotateAround(Vector3 pivot, float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + translate(pivot, 1.0F); + rotate(degrees, axis); + translate(pivot, -1.0F); + } + + /** Like {@code glRotatef}: any axis length, a zero axis does nothing. */ + private void rotate(float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + Vector3f a = new Vector3f((float) axis.x(), (float) axis.y(), (float) axis.z()).normalize(); + pose.mulPose(Axis.of(a).rotationDegrees(degrees)); + } + + private static boolean isZero(Vector3 v) { + return v.x() * v.x() + v.y() * v.y() + v.z() * v.z() < 1.0E-12; + } +} From d041bed0aa46a0463da6ddba1d27f0a7f8850142 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:33 +0800 Subject: [PATCH 0462/1461] Make the translucent culled render type 26.1 only offers for the item target --- .../client/render/ProjectileRenderer.java | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java new file mode 100644 index 00000000..a780333e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java @@ -0,0 +1,181 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.weapon.WeaponColor; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderSetup; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Util; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +import java.util.function.Function; + +/** + * Draws rounds, rockets, missiles and bombs from {@code models/bullets}, tinted with the weapon's + * {@code BulletColor} (its in-water colour below the surface) and always fully lit, as the + * original's {@code MCH_RenderBulletBase} did. + * + *

Not in the original: rounds trail a tracer and rocket motors burn with a glow, faint by day + * and bright at night, so a firefight in the dark can be followed by eye. Nothing glows under + * water. + */ +public class ProjectileRenderer extends EntityRenderer { + /** Vanilla on 26.1 only offers this one drawn into the item target. */ + private static final Function TRANSLUCENT_CULL = Util.memoize(texture -> RenderType.create( + MCHeli.MOD_ID + "_translucent_cull", RenderSetup.builder(RenderPipelines.ENTITY_TRANSLUCENT_CULL) + .withTexture("Sampler0", texture) + .useLightmap() + .useOverlay() + .affectsCrumbling() + .sortOnUpload() + .setOutline(RenderSetup.OutlineProperty.AFFECTS_OUTLINE) + .createRenderSetup())); + private static final int FULL_BRIGHT = 0xF000F0; + /** Warm tracer colour for rounds whose weapon keeps the default white tint. */ + private static final int TRACER = 0xFFC46A; + /** The longest tracer ribbon, in blocks; fast rounds cover far more than this per tick. */ + private static final double MAX_TRACER = 6.0; + /** How far behind the projectile's centre the motor glow sits, in blocks. */ + private static final double MOTOR_OFFSET = 0.6; + + public ProjectileRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + enum GlowKind { + NONE, + TRACER, + MOTOR + } + + public static class State extends EntityRenderState { + String model = ""; + float yaw; + float pitch; + int color = 0xFFFFFFFF; + boolean visible = true; + GlowKind glowKind = GlowKind.NONE; + float glow; + int glowColor = TRACER; + /** Distance covered over the last tick, which points the tracer and the motor glow. */ + Vec3 travel = Vec3.ZERO; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ProjectileEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.model = entity.model(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.visible = !entity.isSpent() && (!(entity instanceof BombEntity bomb) || bomb.isVisible()); + WeaponDefinition w = entity.weapon(); + boolean inWater = inWater(entity.level(), entity); + WeaponColor c = w != null ? (inWater ? w.colorInWater : w.color) : WeaponColor.WHITE; + state.color = c.argb(); + + state.glowKind = inWater || entity.isSpent() ? GlowKind.NONE : glowKind(entity); + if (state.glowKind != GlowKind.NONE) { + state.glow = Glow.strength(entity.level(), entity.blockPosition()); + state.travel = new Vec3(entity.getX() - entity.xo, entity.getY() - entity.yo, entity.getZ() - entity.zo); + if (state.glowKind == GlowKind.MOTOR) { + state.glowColor = GlowParticleOptions.FIRE; + } else { + // A weapon with its own tint keeps it for the tracer; the default white burns warm. + int tint = state.color & 0xFFFFFF; + state.glowColor = tint == 0xFFFFFF ? TRACER : tint; + } + } + } + + private static GlowKind glowKind(ProjectileEntity entity) { + if (entity instanceof BulletEntity) { + return GlowKind.TRACER; + } + if (entity instanceof RocketEntity || entity instanceof MarkerRocketEntity || entity instanceof AAMissileEntity + || entity instanceof ASMissileEntity || entity instanceof ATMissileEntity || entity instanceof TvMissileEntity) { + return GlowKind.MOTOR; + } + return GlowKind.NONE; + } + + /** The original looked for water at the round and up to two blocks above it. */ + private static boolean inWater(Level level, ProjectileEntity e) { + for (int y = 0; y < 3; y++) { + if (level.getFluidState(BlockPos.containing(e.getX() + 0.5, e.getY() + 1.5 - y, e.getZ() + 0.5)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = state.visible ? MeshCache.INSTANCE.find("bullets", state.model) : null; + if (mesh != null) { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/" + state.model + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + int color = state.color; + collector.submitCustomGeometry(pose, TRANSLUCENT_CULL.apply(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, FULL_BRIGHT, OverlayTexture.NO_OVERLAY, color)); + pose.popPose(); + } + if (state.visible) { + submitGlow(state, pose, collector, camera); + } + super.submit(state, pose, collector, camera); + } + + private static void submitGlow(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + if (state.glowKind == GlowKind.NONE || state.glow <= 0.0F) { + return; + } + double speed = state.travel.length(); + Vec3 back = speed > 1.0E-3 ? state.travel.scale(-1.0 / speed) : Vec3.ZERO; + switch (state.glowKind) { + case TRACER -> { + Vec3 toCamera = new Vec3(state.x - camera.pos.x, state.y - camera.pos.y, state.z - camera.pos.z); + Glow.streak(pose, collector, toCamera, back.scale(Math.min(speed, MAX_TRACER)), 0.08F, state.glowColor, state.glow); + Glow.halo(pose, collector, camera, Vec3.ZERO, 0.35F, state.glowColor, state.glow); + } + case MOTOR -> { + Vec3 nozzle = back.scale(MOTOR_OFFSET); + Glow.halo(pose, collector, camera, nozzle, 1.1F, state.glowColor, state.glow); + Glow.halo(pose, collector, camera, nozzle, 0.4F, GlowParticleOptions.FLASH, state.glow); + } + default -> { + } + } + } +} From 9d1b77b98769250db24dbc0e2993a111be8c783a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:33 +0800 Subject: [PATCH 0463/1461] Add client/render/RiderPose.java to the 26.1 shared sources --- .../mcheli/client/render/RiderPose.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java new file mode 100644 index 00000000..9ddb1690 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Tilts players riding a vehicle with its hull, as the original did from its render hook. + * + *

Vanilla keeps riders upright, so a banked plane left the pilot standing straight up through + * the canopy. The tilt is the hull's pitch and roll expressed in the world frame, applied about + * the rider's vehicle attachment point (the hip), which is the point that sits on the seat. The + * rider's own body yaw is applied by the renderer afterwards, so turning in the seat still works. + * + *

The entity render dispatcher pushes a pose around every entity, so nothing needs popping here. + */ +public final class RiderPose { + private RiderPose() { + } + + /** Called before a living entity is drawn, with the pose the renderer is about to use. */ + public static void beforeRender(LivingEntityRenderState renderState, PoseStack pose, float partialTicks) { + // Render states carry no entity reference; players are the ones that keep their id. + if (!(renderState instanceof AvatarRenderState state)) { + return; + } + ClientLevel level = Minecraft.getInstance().level; + Entity rider = level != null ? level.getEntity(state.id) : null; + if (rider == null || !(rider.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + float yaw = vehicle.getYRot(partialTicks); + float pitch = vehicle.getXRot(partialTicks); + float roll = vehicle.roll(partialTicks); + if (pitch == 0.0F && roll == 0.0F) { + return; + } + Quaternionf tilt = AircraftRotation.orientation(yaw, pitch, roll).rotateY((float) Math.toRadians(yaw)); + Vec3 hip = rider.getVehicleAttachmentPoint(vehicle); + pose.rotateAround(tilt, (float) hip.x, (float) hip.y, (float) hip.z); + } +} From 3a9cf1526c1ccd4ca356305e94045f6f5f4a7dcc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:34 +0800 Subject: [PATCH 0464/1461] Add client/render/ThrowableRenderer.java to the 26.1 shared sources --- .../client/render/ThrowableRenderer.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java new file mode 100644 index 00000000..a693052b --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +/** {@code MCH_RenderThrowable}: the grenade's own model from {@code models/throwable}. */ +public class ThrowableRenderer extends EntityRenderer { + public ThrowableRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + String name = ""; + float yaw; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ThrowableEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.name = entity.definitionName(); + state.yaw = entity.getYRot(partialTicks); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = state.name.isEmpty() ? null : MeshCache.INSTANCE.get("throwable", state.name); + if (mesh != null) { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/throwable/" + state.name + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From 8fe8f096360ce8b65865c5db44ad954926b584ae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:34 +0800 Subject: [PATCH 0465/1461] Add client/render/UavStationRenderer.java to the 26.1 shared sources --- .../client/render/UavStationRenderer.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java new file mode 100644 index 00000000..e3f7f9c7 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +import java.util.List; + +/** + * The UAV station and the portable controller, {@code MCH_RenderUavStation}. The station's screen + * lights up while a drone is flown from it; the controller's lid, keyboard half and display fold + * open as it is opened, and light up once fully open. + */ +public class UavStationRenderer extends EntityRenderer { + private static final String[] MODEL = {"uav_station", "uav_portable_controller"}; + /** The original greyed both a little. */ + private static final int TINT = 0xFFBFBFBF; + + public UavStationRenderer(EntityRendererProvider.Context context) { + super(context); + shadowRadius = 1.0F; + } + + public static class State extends EntityRenderState { + float yaw; + int kind; + boolean lit; + float cover; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(UavStationEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.kind = entity.kind(); + state.cover = entity.cover(partialTicks); + state.lit = state.kind == UavStationEntity.PORTABLE ? state.cover > 0.95F : entity.isLinked(); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + int index = state.kind == UavStationEntity.PORTABLE ? 1 : 0; + Mesh mesh = MeshCache.INSTANCE.get("", MODEL[index]); + if (mesh != null) { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + MODEL[index] + (state.lit ? "_on" : "") + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = state.lightCoords; + if (index == 0) { + draw(pose, collector, texture, mesh.groups(), light); + } else { + draw(pose, collector, texture, mesh.part("$body"), light); + folding(pose, collector, texture, mesh.part("$cover"), state.cover * 60.0F, -0.1812F, -0.3186F, light); + folding(pose, collector, texture, mesh.part("$laptop_cover"), state.cover * 95.0F, -0.1808F, -0.0422F, light); + folding(pose, collector, texture, mesh.part("$display"), state.cover * -85.0F, -0.1807F, 0.2294F, light); + } + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } + + /** {@code renderRotPart}: turns a part about the x axis through its hinge. */ + private static void folding(PoseStack pose, SubmitNodeCollector collector, Identifier texture, List part, + float angle, float y, float z, int light) { + pose.pushPose(); + pose.translate(0.0F, y, z); + pose.mulPose(Axis.XN.rotationDegrees(angle)); + pose.translate(0.0F, -y, -z); + draw(pose, collector, texture, part, light); + pose.popPose(); + } + + private static void draw(PoseStack pose, SubmitNodeCollector collector, Identifier texture, List groups, int light) { + if (!groups.isEmpty()) { + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY, TINT)); + } + } +} From 0b95f0d18e69181a835a98771e80359ba3fae92e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:34 +0800 Subject: [PATCH 0466/1461] Add client/screen/DraftingTableScreen.java to the 26.1 shared sources --- .../client/screen/DraftingTableScreen.java | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java new file mode 100644 index 00000000..e70d6bd0 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java @@ -0,0 +1,335 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.workshop.WorkshopHomeScreen; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.network.DraftingTablePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeCollection; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; +import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * The drafting table's recipe browser, {@code MCH_DraftingTableGui}. Pick a list (helicopters, + * planes, tanks, ground vehicles or equipment), step through its recipes, and Create makes the one + * shown into the output slot. The right page shows the original's illustrated description for + * equipment and a vehicle's numbers for vehicles; the original turned a 3D model there instead. + * + *

The recipes come from the player's recipe book, which holds every MCHeli recipe from login. + */ +public class DraftingTableScreen extends AbstractContainerScreen { + private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/drafting_table.png"); + private static final int TEXTURE_SIZE = 512; + /** The description pages take this part of their 512 square texture. */ + private static final int PAGE_WIDTH = 340; + private static final int PAGE_HEIGHT = 380; + private static final int PAGE_X = 210; + private static final int PAGE_Y = 20; + private static final int GRID_X = 121; + private static final int GRID_Y = 34; + private static final int TEXT = 0xFF6A3A00; + private static final int WHITE = 0xFFFFFFFF; + private static final String[] LISTS = {"helicopter", "plane", "vehicle", "tank", "item"}; + + private final List> lists = new ArrayList<>(); + private int list; + private int index; + private int page; + private int pageCount; + private @Nullable Button create; + private @Nullable Button previous; + private @Nullable Button next; + private @Nullable Button previousPage; + private @Nullable Button nextPage; + + public DraftingTableScreen(DraftingTableMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title, 400, 240); + inventoryLabelX = 30; + inventoryLabelY = 130; + } + + @Override + protected void init() { + super.init(); + collectRecipes(); + for (int i = 0; i < LISTS.length; i++) { + int which = i; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.list." + LISTS[i]), b -> selectList(which)) + .bounds(leftPos + 20, topPos + 20 + i * 20, 90, 20).build()); + } + create = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.create"), b -> create()) + .bounds(leftPos + 120, topPos + 89, 50, 20).build()); + previous = addRenderableWidget(Button.builder(Component.literal("<<"), b -> step(-1)).bounds(leftPos + 120, topPos + 111, 36, 20).build()); + next = addRenderableWidget(Button.builder(Component.literal(">>"), b -> step(1)).bounds(leftPos + 155, topPos + 111, 35, 20).build()); + previousPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.previous_page"), b -> turnPage(-1)) + .bounds(leftPos + 210, topPos + 210, 60, 20).build()); + nextPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.next_page"), b -> turnPage(1)) + .bounds(leftPos + 270, topPos + 210, 60, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.workshop"), b -> openWorkshop()) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.drafting_table.workshop_hint"))) + .bounds(leftPos + 334, topPos + 210, 56, 20).build()); + selectList(list); + } + + /** The workshop is a screen of its own and needs no block, so the table's menu closes first. */ + private void openWorkshop() { + minecraft.player.closeContainer(); + minecraft.setScreen(new WorkshopHomeScreen()); + } + + /** Every MCHeli recipe the recipe book holds, sorted into the five lists by what it makes. */ + private void collectRecipes() { + lists.clear(); + for (int i = 0; i < LISTS.length; i++) { + lists.add(new ArrayList<>()); + } + ContextMap context = SlotDisplayContext.fromLevel(minecraft.level); + for (RecipeCollection collection : minecraft.player.getRecipeBook().getCollections()) { + for (RecipeDisplayEntry entry : collection.getRecipes()) { + ItemStack result = entry.display().result().resolveForFirstStack(context); + if (!result.isEmpty() && BuiltInRegistries.ITEM.getKey(result.getItem()).getNamespace().equals(MCHeli.MOD_ID)) { + lists.get(listOf(result.getItem())).add(entry); + } + } + } + for (List entries : lists) { + entries.sort(Comparator.comparing(e -> e.display().result().resolveForFirstStack(context).getHoverName().getString())); + } + } + + private static int listOf(Item item) { + if (item == ModItems.HELICOPTER.get()) { + return 0; + } + if (item == ModItems.PLANE.get()) { + return 1; + } + if (item == ModItems.VEHICLE.get()) { + return 2; + } + if (item == ModItems.TANK.get()) { + return 3; + } + return 4; + } + + private void selectList(int which) { + list = which; + index = 0; + showRecipe(); + } + + private void step(int direction) { + int size = lists.get(list).size(); + if (size > 0) { + index = Math.floorMod(index + direction, size); + showRecipe(); + } + } + + private void turnPage(int direction) { + page = Math.clamp(page + direction, 0, Math.max(pageCount - 1, 0)); + updateButtons(); + } + + private void showRecipe() { + page = 0; + ItemStack result = result(); + pageCount = descriptionPages(result); + updateButtons(); + } + + private @Nullable RecipeDisplayEntry current() { + List entries = lists.get(list); + return entries.isEmpty() ? null : entries.get(Math.min(index, entries.size() - 1)); + } + + private ItemStack result() { + RecipeDisplayEntry entry = current(); + return entry != null ? entry.display().result().resolveForFirstStack(SlotDisplayContext.fromLevel(minecraft.level)) : ItemStack.EMPTY; + } + + /** The original's description pages for equipment: {@code drafting_table_desc/_.png}. */ + private int descriptionPages(ItemStack result) { + if (result.isEmpty() || AircraftItem.vehicleOf(result) != null) { + return 0; + } + String name = descriptionName(result); + int pages = 0; + while (pages < 10 && minecraft.getResourceManager().getResource(pageTexture(name, pages)).isPresent()) { + pages++; + } + return pages; + } + + private static String descriptionName(ItemStack result) { + if (result.getItem() == ModItems.THROWABLE.get()) { + String throwable = result.get(ModDataComponents.THROWABLE.get()); + return throwable != null ? throwable : "throwable"; + } + return BuiltInRegistries.ITEM.getKey(result.getItem()).getPath(); + } + + private static Identifier pageTexture(String name, int page) { + return Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/drafting_table_desc/" + name + "_" + page + ".png"); + } + + private boolean canCreate() { + RecipeDisplayEntry entry = current(); + if (entry == null || menu.getSlot(menu.slots.size() - 1).hasItem() && !minecraft.player.getAbilities().instabuild) { + return false; + } + if (minecraft.player.getAbilities().instabuild) { + return true; + } + StackedItemContents contents = new StackedItemContents(); + minecraft.player.getInventory().fillStackedContents(contents); + return entry.canCraft(contents); + } + + private void create() { + RecipeDisplayEntry entry = current(); + if (entry != null) { + ModNetwork.sendToServer(new DraftingTablePacket(entry.id())); + } + } + + private void updateButtons() { + if (create == null || previous == null || next == null || previousPage == null || nextPage == null) { + return; + } + boolean any = current() != null; + create.active = any && canCreate(); + previous.active = lists.get(list).size() > 1; + next.active = lists.get(list).size() > 1; + previousPage.visible = pageCount > 1; + nextPage.visible = pageCount > 1; + previousPage.active = page > 0; + nextPage.active = page < pageCount - 1; + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractBackground(graphics, mouseX, mouseY, a); + graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, TEXTURE_SIZE, TEXTURE_SIZE); + if (pageCount > 0) { + String name = descriptionName(result()); + graphics.blit(RenderPipelines.GUI_TEXTURED, pageTexture(name, page), leftPos + PAGE_X, topPos + PAGE_Y, 0.0F, 0.0F, + PAGE_WIDTH / 2, PAGE_HEIGHT / 2, PAGE_WIDTH, PAGE_HEIGHT, TEXTURE_SIZE, TEXTURE_SIZE); + } + } + + @Override + protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, TEXT, false); + RecipeDisplayEntry entry = current(); + if (entry == null) { + graphics.text(font, Component.translatable("gui.mcheli.drafting_table.empty"), 120, 20, TEXT, false); + return; + } + ItemStack result = result(); + graphics.text(font, font.plainSubstrByWidth(result.getHoverName().getString(), 85), 120, 20, WHITE, true); + graphics.text(font, (index + 1) + " / " + lists.get(list).size(), 120, 134, TEXT, false); + ingredients(graphics, entry.display(), mouseX, mouseY); + graphics.item(result, 178, 52); + if (pageCount == 0) { + details(graphics, result); + } + } + + /** {@code drawItemRecipe}: the 3x3 grid, each slot cycling through what it accepts once a second. */ + private void ingredients(GuiGraphicsExtractor graphics, RecipeDisplay display, int mouseX, int mouseY) { + ContextMap context = SlotDisplayContext.fromLevel(minecraft.level); + List slots; + int width; + switch (display) { + case ShapedCraftingRecipeDisplay shaped -> { + slots = shaped.ingredients(); + width = shaped.width(); + } + case ShapelessCraftingRecipeDisplay shapeless -> { + slots = shapeless.ingredients(); + width = 3; + } + default -> { + return; + } + } + int tick = (int) (minecraft.level.getGameTime() / 20); + for (int i = 0; i < slots.size() && i < 9; i++) { + List options = slots.get(i).resolveForStacks(context); + if (options.isEmpty()) { + continue; + } + ItemStack stack = options.get(tick % options.size()); + int x = GRID_X + (i % width) * 18; + int y = GRID_Y + (i / width) * 18; + graphics.item(stack, x, y); + if (mouseX - leftPos >= x && mouseX - leftPos < x + 16 && mouseY - topPos >= y && mouseY - topPos < y + 16) { + graphics.setTooltipForNextFrame(font, stack, mouseX - leftPos, mouseY - topPos); + } + } + } + + /** The vehicle's numbers, where the original turned its model. */ + private void details(GuiGraphicsExtractor graphics, ItemStack result) { + String name = AircraftItem.vehicleOf(result); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + return; + } + List lines = new ArrayList<>(); + lines.add(Component.translatable("gui.mcheli.drafting_table.hp", def.maxHp)); + lines.add(Component.translatable("gui.mcheli.drafting_table.speed", "%.2f".formatted(def.speed))); + lines.add(Component.translatable("gui.mcheli.drafting_table.seats", def.seatCount())); + lines.add(Component.translatable("gui.mcheli.drafting_table.fuel", def.maxFuel)); + lines.add(Component.translatable("gui.mcheli.drafting_table.weapons")); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + String weaponName = weapon != null && weapon.displayName != null ? weapon.displayName : group.weaponType(); + lines.add(Component.literal(" " + weaponName + (group.mounts().size() > 1 ? " x" + group.mounts().size() : ""))); + } + int y = PAGE_Y + 20; + for (Component line : lines) { + if (y > PAGE_Y + 180) { + break; + } + graphics.text(font, font.split(line, 170).getFirst(), PAGE_X, y, TEXT, false); + y += 10; + } + } +} From 093ba976ebb32f76083a521f7954d659a806ca39 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:35 +0800 Subject: [PATCH 0467/1461] Pick team colours from ChatFormatting on 26.1, which has no TeamColor --- .../client/screen/TeamManagerScreen.java | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java new file mode 100644 index 00000000..f5ad2f9a --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java @@ -0,0 +1,136 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.TeamPacket; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.scores.PlayerTeam; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * The team manager, {@code MCH_GuiScoreboard_Main} and {@code MCH_GuiScoreboard_CreateTeam}, for + * operators setting up a match: switch PvP, shuffle everyone into the teams, make a team, send + * everyone to their spawn point and clear away every vehicle. The teams and their players are + * listed on the right. + */ +public class TeamManagerScreen extends Screen { + private static final int WIDTH = 150; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + + private @Nullable EditBox name; + private int color = TeamManager.COLORS.indexOf(ChatFormatting.RED); + private boolean friendlyFire; + private @Nullable Button colorButton; + private @Nullable Button friendlyFireButton; + private @Nullable Button createButton; + + public TeamManagerScreen() { + super(Component.translatable("gui.mcheli.team.title")); + } + + @Override + protected void init() { + int left = width / 2 - WIDTH - 10; + int y = 40; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.pvp"), b -> send(TeamPacket.Action.PVP)) + .bounds(left, y, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.shuffle"), b -> send(TeamPacket.Action.SHUFFLE)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.shuffle_hint"))) + .bounds(left, y + 22, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.jump_spawn"), b -> send(TeamPacket.Action.JUMP_TO_SPAWN)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.jump_spawn_hint"))) + .bounds(left, y + 44, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.destroy_all"), b -> send(TeamPacket.Action.DESTROY_ALL)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.destroy_all_hint"))) + .bounds(left, y + 66, WIDTH, 20).build()); + + int create = y + 104; + name = addRenderableWidget(new EditBox(font, left, create, WIDTH, 20, Component.translatable("gui.mcheli.team.name"))); + name.setMaxLength(16); + name.setHint(Component.translatable("gui.mcheli.team.name")); + name.setResponder(text -> updateButtons()); + colorButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + color = (color + 1) % TeamManager.COLORS.size(); + updateButtons(); + }).bounds(left, create + 22, WIDTH, 20).build()); + friendlyFireButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + friendlyFire = !friendlyFire; + updateButtons(); + }).bounds(left, create + 44, WIDTH, 20).build()); + createButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.create"), b -> { + ModNetwork.sendToServer(new TeamPacket(TeamPacket.Action.CREATE_TEAM, name.getValue(), color, friendlyFire)); + name.setValue(""); + }).bounds(left, create + 66, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 - 50, height - 28, 100, 20).build()); + updateButtons(); + } + + private void send(TeamPacket.Action action) { + ModNetwork.sendToServer(new TeamPacket(action)); + } + + private void updateButtons() { + if (colorButton == null || friendlyFireButton == null || createButton == null || name == null) { + return; + } + ChatFormatting teamColor = TeamManager.COLORS.get(color); + colorButton.setMessage(Component.translatable("gui.mcheli.team.color", + Component.literal(teamColor.getName()).withStyle(teamColor))); + friendlyFireButton.setMessage(Component.translatable(friendlyFire ? "gui.mcheli.team.friendly_fire_on" : "gui.mcheli.team.friendly_fire_off")); + String value = name.getValue().trim(); + createButton.active = !value.isEmpty() && !value.contains(" "); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractRenderState(graphics, mouseX, mouseY, a); + graphics.centeredText(font, title, width / 2, 15, TEXT); + int left = width / 2 - WIDTH - 10; + graphics.text(font, Component.translatable("gui.mcheli.team.new_team"), left, 132, DIM); + teams(graphics, width / 2 + 10, 40); + } + + /** The scoreboard's teams and who is in them, as this client sees the scoreboard. */ + private void teams(GuiGraphicsExtractor graphics, int x, int y) { + if (minecraft.level == null) { + return; + } + List teams = new ArrayList<>(minecraft.level.getScoreboard().getPlayerTeams()); + if (teams.isEmpty()) { + graphics.text(font, Component.translatable("gui.mcheli.team.none"), x, y, DIM); + return; + } + int bottom = height - 40; + for (PlayerTeam team : teams) { + if (y > bottom) { + break; + } + graphics.text(font, team.getFormattedDisplayName(), x, y, TEXT); + y += 11; + for (String player : team.getPlayers()) { + if (y > bottom) { + break; + } + graphics.text(font, " " + player, x, y, DIM); + y += 10; + } + y += 4; + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From 4121fac78fa29b03d51c2183bbb3240319e81329 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:35 +0800 Subject: [PATCH 0468/1461] Add client/screen/UavStationScreen.java to the 26.1 shared sources --- .../client/screen/UavStationScreen.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java new file mode 100644 index 00000000..0a9c3da6 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_GuiUavStation}: the slot for the drone, the launch offset + * per axis with its buttons, and Continue to take the last drone flown from here again. + */ +public class UavStationScreen extends AbstractContainerScreen { + private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/uav_station.png"); + private static final String[] AXES = {"X", "Y", "Z"}; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF0000; + + private @Nullable Button continueButton; + + public UavStationScreen(UavStationMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title, 176, 166); + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + int x = width / 2 - 5; + int y = height / 2 - 76; + for (int axis = 0; axis < 3; axis++) { + for (int step = 0; step < UavStationMenu.STEPS.length; step++) { + int s = UavStationMenu.STEPS[step]; + int id = axis * 4 + step; + addRenderableWidget(Button.builder(Component.literal((s > 0 ? "+" : "") + s), b -> press(id)) + .bounds(x + step * 20, y + axis * 22, 20, 20).build()); + } + } + continueButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.uav_station.continue"), b -> press(UavStationMenu.BUTTON_CONTINUE)) + .bounds(x - 77, y + 44, 50, 20).build()); + continueButton.active = menu.canContinue(); + } + + @Override + protected void containerTick() { + super.containerTick(); + if (continueButton != null) { + continueButton.active = menu.canContinue(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractBackground(graphics, mouseX, mouseY, a); + graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, 256, 256); + } + + @Override + protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + UavStationEntity station = menu.station(); + ItemStack stack = menu.getSlot(0).getItem(); + AircraftDefinition def = stack.isEmpty() ? null : UavStationEntity.definitionOf(stack); + Component label; + int color = WHITE; + if (!stack.isEmpty() && (def == null || !def.uav)) { + label = Component.translatable("gui.mcheli.uav_station.not_uav"); + color = RED; + } else if (station != null && station.kind() == UavStationEntity.PORTABLE && def != null && !def.smallUav) { + label = Component.translatable("gui.mcheli.uav_station.small_only"); + color = RED; + } else { + label = title; + } + graphics.text(font, label, 8, 6, color, false); + graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, WHITE, false); + for (int axis = 0; axis < 3; axis++) { + graphics.text(font, String.format("%s.%+d", AXES[axis], menu.offset(axis)), 58, 15 + axis * 22, WHITE, false); + } + } +} From 3889565c76a04c49b7dde20b3b00d1e3dd6984da Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:36 +0800 Subject: [PATCH 0469/1461] Add client/screen/VehicleScreen.java to the 26.1 shared sources --- .../mcheli/client/screen/VehicleScreen.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java new file mode 100644 index 00000000..a320b923 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java @@ -0,0 +1,180 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGui}: fuel and parachute slots on the left, the weapon + * panel on the right with its reload cost and button, and a button into the cargo chest for + * vehicles that have one. The panel is drawn from the chest texture so it looks like vanilla. + */ +public class VehicleScreen extends AbstractContainerScreen { + private static final Identifier CHEST = Identifier.withDefaultNamespace("textures/gui/container/generic_54.png"); + private static final Identifier SLOT = Identifier.withDefaultNamespace("container/slot"); + /** Height of the part above the player inventory. */ + private static final int TOP = 88; + private static final int PANEL_X = 72; + private static final int PANEL_WIDTH = 96; + private static final int LABEL = 0xFF404040; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF5555; + private static final int GREEN = 0xFF55FF55; + + private int weapon; + private boolean cargoButton; + private @Nullable Button reload; + private @Nullable Button previous; + private @Nullable Button next; + + public VehicleScreen(VehicleMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title, 176, TOP + 96); + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + previous = addRenderableWidget(Button.builder(Component.literal("<"), b -> step(-1)) + .bounds(leftPos + PANEL_X, topPos + 68, 16, 16).build()); + reload = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.reload"), b -> press(weapon)) + .bounds(leftPos + PANEL_X + 18, topPos + 68, PANEL_WIDTH - 36, 16).build()); + next = addRenderableWidget(Button.builder(Component.literal(">"), b -> step(1)) + .bounds(leftPos + PANEL_X + PANEL_WIDTH - 16, topPos + 68, 16, 16).build()); + AircraftEntity vehicle = menu.vehicle(); + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + cargoButton = def != null && def.cargoRows() > 0; + if (cargoButton) { + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.cargo"), b -> press(VehicleMenu.BUTTON_CARGO)) + .bounds(leftPos + imageWidth - 52, topPos + 3, 46, 13).build()); + } + updateButtons(); + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + private int weaponCount() { + AircraftEntity vehicle = menu.vehicle(); + VehicleWeapons weapons = vehicle != null ? vehicle.weapons() : null; + return weapons != null ? Math.min(weapons.sets().size(), VehicleMenu.MAX_WEAPONS) : 0; + } + + private void step(int direction) { + int count = weaponCount(); + if (count > 0) { + weapon = Math.floorMod(weapon + direction, count); + updateButtons(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + private void updateButtons() { + if (reload == null || previous == null || next == null) { + return; + } + int count = weaponCount(); + reload.visible = count > 0; + previous.visible = count > 1; + next.visible = count > 1; + reload.active = menu.canReload(weapon); + AircraftEntity vehicle = menu.vehicle(); + boolean grounded = vehicle != null && vehicle.canSupply(); + reload.setTooltip(grounded ? null : Tooltip.create(Component.translatable("gui.mcheli.vehicle.reload_hint"))); + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractBackground(graphics, mouseX, mouseY, a); + int x = leftPos; + int y = topPos; + // The chest's title bar, its plain middle stretched down to the inventory, then the inventory. + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y, 0.0F, 0.0F, imageWidth, 17, 256, 256); + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y + 17, 0.0F, 12.0F, imageWidth, TOP - 17, imageWidth, 1, 256, 256); + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y + TOP, 0.0F, 126.0F, imageWidth, 96, 256, 256); + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SLOT, x + VehicleMenu.FUEL_X + i * 18 - 1, y + VehicleMenu.FUEL_Y - 1, 18, 18); + } + for (int i = 0; i < menu.parachuteSlots(); i++) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SLOT, x + VehicleMenu.PARACHUTE_X + i * 18 - 1, y + VehicleMenu.PARACHUTE_Y - 1, 18, 18); + } + graphics.fill(x + PANEL_X - 2, y + 17, x + PANEL_X + PANEL_WIDTH + 2, y + 66, 0xFF373737); + } + + @Override + protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + AircraftEntity vehicle = menu.vehicle(); + // Long names only give way to the Cargo button, where there is one. + int titleWidth = cargoButton ? imageWidth - 64 : imageWidth - 16; + graphics.text(font, font.plainSubstrByWidth(title.getString(), titleWidth), titleLabelX, titleLabelY, LABEL, false); + graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, LABEL, false); + if (vehicle == null) { + return; + } + if (vehicle.maxFuel() > 0) { + // A percentage and a gauge: the exact units would run into the weapon panel. + int percent = Math.round(vehicle.fuelRatio() * 100.0F); + graphics.text(font, Component.translatable("gui.mcheli.vehicle.fuel", percent), VehicleMenu.FUEL_X, VehicleMenu.FUEL_Y - 11, LABEL, false); + int gaugeY = VehicleMenu.FUEL_Y + 18; + int width = VehicleInventory.FUEL_SLOTS * 18 - 2; + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width, gaugeY + 3, 0xFF373737); + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width * percent / 100, gaugeY + 3, percent <= 10 ? RED : GREEN); + } + if (menu.parachuteSlots() > 0) { + graphics.text(font, Component.translatable("gui.mcheli.vehicle.parachute"), VehicleMenu.PARACHUTE_X, VehicleMenu.PARACHUTE_Y - 11, LABEL, false); + } + weaponPanel(graphics, vehicle); + } + + /** Name, rounds carried against the maximum, and what one reload costs. */ + private void weaponPanel(GuiGraphicsExtractor graphics, AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null && weaponCount() > 0 ? weapons.set(Math.min(weapon, weaponCount() - 1)) : null; + if (set == null) { + graphics.text(font, Component.translatable("gui.mcheli.vehicle.no_weapons"), PANEL_X, 20, WHITE, false); + return; + } + graphics.text(font, font.plainSubstrByWidth(set.displayName(), PANEL_WIDTH), PANEL_X, 20, WHITE, false); + int rounds = menu.rounds(weapon); + int max = set.maxAmmo(); + int color = rounds == 0 && max > 0 ? RED : rounds >= max ? GREEN : WHITE; + graphics.text(font, rounds + " / " + max, PANEL_X, 31, color, false); + int itemX = PANEL_X; + for (RoundItem cost : set.definition().roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null) { + continue; + } + graphics.item(new ItemStack(item), itemX, 42); + graphics.text(font, String.valueOf(cost.count()), itemX + 17, 47, WHITE, false); + itemX += 32; + if (itemX > PANEL_X + PANEL_WIDTH - 16) { + break; + } + } + } +} From fcd08f8171f013d806cc908085179b616a2998a9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:36 +0800 Subject: [PATCH 0470/1461] Add client/sound/EngineSound.java to the 26.1 shared sources --- .../mcheli/client/sound/EngineSound.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java new file mode 100644 index 00000000..b32040f5 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** + * A looping sound of one vehicle: its engine, or the rotor loop some vehicles have alongside it. + * Ported from {@code MCH_SoundUpdater}. + * + *

Volume and pitch follow the vehicle's throttle every tick, the pitch along the definition's + * curve if it has one and otherwise the way the kind of vehicle always has. For listeners outside + * the vehicle the sound is placed between them and the vehicle, so that the 16 block range of a + * normal sound stretches to the definition's {@code SoundRange}, and the pitch bends slightly as the + * vehicle approaches or leaves, which is what the original did. + */ +final class EngineSound extends RuntimeSoundInstance { + private static final float MAX_DOPPLER = 0.2F; + + private final AircraftEntity vehicle; + private final EngineSoundSource source; + private final boolean rotor; + private float doppler; + private double lastDistance = -1.0; + + EngineSound(AircraftEntity vehicle, EngineSoundSource source, String sound, boolean rotor) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.vehicle = vehicle; + this.source = source; + this.rotor = rotor; + this.looping = true; + this.delay = 0; + update(); + } + + @Override + public void tick() { + if (vehicle.isRemoved() || vehicle.definition() == null) { + stop(); + return; + } + update(); + if (volume <= 0.0F) { + stop(); + } + } + + private void update() { + AircraftDefinition def = vehicle.definition(); + LocalPlayer player = Minecraft.getInstance().player; + if (def == null || player == null) { + volume = 0.0F; + return; + } + volume = (rotor ? def.rotorSoundVolume : def.soundVolume) * source.engineVolume(); + PitchCurve curve = rotor ? def.rotorSoundPitchCurve : def.soundPitchCurve; + float pitch = def.soundPitch * (curve != null ? curve.at(vehicle.visibleThrottle()) : source.enginePitch()); + boolean aboard = player.getRootVehicle() == vehicle.getRootVehicle(); + if (aboard) { + x = vehicle.getX(); + y = vehicle.getY(); + z = vehicle.getZ(); + } else { + double dx = vehicle.getX() - player.getX(); + double dz = vehicle.getZ() - player.getZ(); + double distance = Math.sqrt(dx * dx + dz * dz); + if (lastDistance >= 0.0) { + double closing = lastDistance - distance; + if (Math.abs(closing) > 0.3) { + doppler = Math.clamp(doppler + (float) (closing / 40.0), -MAX_DOPPLER, MAX_DOPPLER); + } + } + doppler *= 0.9F; + pitch += doppler; + lastDistance = distance; + + double scale = def.soundRange / 16.0; + x = player.getX() + dx / scale; + y = player.getY() + (vehicle.getY() - player.getY()) / scale; + z = player.getZ() + dz / scale; + } + this.pitch = Math.max(pitch, 0.0F); + } + + @Override + public boolean canStartSilent() { + return true; + } +} From acfdad6b5c573f7bda247c3d7b681731ff88c269 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:37 +0800 Subject: [PATCH 0471/1461] Add client/sound/EngineSoundPlayer.java to the 26.1 shared sources --- .../client/sound/EngineSoundPlayer.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java new file mode 100644 index 00000000..62e52284 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.Entity; + +import java.util.HashMap; +import java.util.Map; + +/** + * Starts engine and rotor loops for vehicles near the player and restarts them after they fall + * silent. The sounds themselves track their vehicle until it stops or disappears. + */ +public final class EngineSoundPlayer { + /** Ticks to wait before restarting a loop, as the original did, so a stuttering engine does not spam. */ + private static final int RESTART_DELAY = 20; + + /** By {@link #key}, since a vehicle can have two loops. */ + private static final Map PLAYING = new HashMap<>(); + private static final Map COOLDOWN = new HashMap<>(); + + private EngineSoundPlayer() { + } + + public static void tick() { + Minecraft mc = Minecraft.getInstance(); + ClientLevel level = mc.level; + LocalPlayer player = mc.player; + if (level == null || player == null || mc.isPaused()) { + if (level == null) { + PLAYING.clear(); + COOLDOWN.clear(); + } + return; + } + PLAYING.values().removeIf(EngineSound::isStopped); + COOLDOWN.replaceAll((id, ticks) -> ticks - 1); + COOLDOWN.values().removeIf(ticks -> ticks <= 0); + + for (Entity entity : level.entitiesForRendering()) { + if (!(entity instanceof AircraftEntity vehicle) || !(entity instanceof EngineSoundSource source)) { + continue; + } + AircraftDefinition def = vehicle.definition(); + if (def == null) { + continue; + } + double range = def.soundRange * 1.2; + if (vehicle.distanceToSqr(player) > range * range) { + continue; + } + start(mc, vehicle, source, def.soundOrDefault(), def.soundVolume, false); + start(mc, vehicle, source, def.rotorSound, def.rotorSoundVolume, true); + } + } + + private static void start(Minecraft mc, AircraftEntity vehicle, EngineSoundSource source, String sound, float volume, boolean rotor) { + long key = key(vehicle, rotor); + if (sound.isEmpty() || PLAYING.containsKey(key) || COOLDOWN.containsKey(key) || volume * source.engineVolume() <= 0.01F) { + return; + } + EngineSound loop = new EngineSound(vehicle, source, sound, rotor); + mc.getSoundManager().play(loop); + PLAYING.put(key, loop); + COOLDOWN.put(key, RESTART_DELAY); + } + + private static long key(AircraftEntity vehicle, boolean rotor) { + return (long) vehicle.getId() << 1 | (rotor ? 1L : 0L); + } +} From 6151bfbde69d0e6ab00190c8abe6044d4f62341c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:37 +0800 Subject: [PATCH 0472/1461] Add client/sound/OneShotSound.java to the 26.1 shared sources --- .../mcheli/client/sound/OneShotSound.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java new file mode 100644 index 00000000..31825a4f --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java @@ -0,0 +1,22 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** A sound from a content pack played once at a point, such as a gun firing. */ +public final class OneShotSound extends RuntimeSoundInstance { + public OneShotSound(String sound, double x, double y, double z, float volume, float pitch) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.x = x; + this.y = y; + this.z = z; + this.volume = volume; + this.pitch = pitch; + } + + @Override + public void tick() { + // It stays where it was started and ends when the file does. + } +} From 14d41e845cfa2fb72248ad64a4e135227ff00133 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:38 +0800 Subject: [PATCH 0473/1461] Add client/sound/RuntimeSoundInstance.java to the 26.1 shared sources --- .../client/sound/RuntimeSoundInstance.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java new file mode 100644 index 00000000..fb3ee2e3 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -0,0 +1,79 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.JOrbisAudioStream; +import net.minecraft.client.sounds.LoopingAudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.client.sounds.WeighedSoundEvents; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.util.Util; +import net.minecraft.util.valueproviders.ConstantFloat; +import org.jspecify.annotations.Nullable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * A sound that plays through the sound manager when a resource pack defines its event, and + * otherwise streams the OGG file a runtime root holds for it ({@link RuntimeAssets#soundFile}). + * Content added while the game runs can bring sounds this way without touching + * {@code sounds.json} or reloading resources. + */ +public abstract class RuntimeSoundInstance extends AbstractTickableSoundInstance { + private @Nullable Path file; + + protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSource random) { + super(event, source, random); + } + + @Override + public @Nullable WeighedSoundEvents resolve(SoundManager manager) { + WeighedSoundEvents events = super.resolve(manager); + if (events != null) { + return events; + } + file = RuntimeAssets.soundFile(identifier.getPath()); + if (file == null) { + return null; + } + // Streamed, so the file is read as it plays and never cached as a buffer. + sound = new Sound(identifier, new ConstantFloat(1.0F), new ConstantFloat(1.0F), 1, Sound.Type.FILE, true, false, 16); + return new WeighedSoundEvents(identifier, null); + } + + /** + * Forge and NeoForge call this through their sound instance extension, which it overrides + * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + */ + public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { + return openStream(buffers, sound.getPath(), looping); + } + + /** Streams the file found on disk, or the resource {@code path} when there is none. */ + public CompletableFuture openStream(SoundBufferLibrary buffers, Identifier path, boolean looping) { + Path source = file; + if (source == null) { + return buffers.getStream(path, looping); + } + return CompletableFuture.supplyAsync(() -> { + try { + InputStream in = new BufferedInputStream(Files.newInputStream(source)); + return looping ? new LoopingAudioStream(JOrbisAudioStream::new, in) : new JOrbisAudioStream(in); + } catch (IOException e) { + throw new CompletionException(e); + } + }, Util.nonCriticalIoPool()); + } +} From f464029fcfc9d1d6f610896e4fafda7933dd79a7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:38 +0800 Subject: [PATCH 0474/1461] Add client/workshop/WorkshopClient.java to the 26.1 shared sources --- .../client/workshop/WorkshopClient.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java new file mode 100644 index 00000000..26401727 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSubmitPacket; +import com.jnhprojects.mcheli.network.TestFlightPacket; +import net.minecraft.client.Minecraft; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; + +/** + * What the workshop does beyond its screens: takes the player to a test flight and back, and sends + * a pack to the server. The screen stays alive during a flight, so the undo history and the view are + * as they were when the pilot gets out. + */ +public final class WorkshopClient { + /** If no vehicle has come after this long, the server turned the flight down and said why. */ + private static final int SPAWN_TIMEOUT_TICKS = 100; + /** About 1.2 MB a second, so a download-sized pack does not crowd out the game's own packets. */ + private static final int SUBMIT_CHUNKS_PER_TICK = 2; + + private static @Nullable WorkshopScreen waiting; + private static boolean airborne; + private static int ticks; + private static byte @Nullable [] submitting; + private static int submitted; + + private WorkshopClient() { + } + + static void startTestFlight(WorkshopScreen screen, String vehicle) { + waiting = screen; + airborne = false; + ticks = 0; + Minecraft.getInstance().setScreen(null); + ModNetwork.sendToServer(new TestFlightPacket(vehicle)); + } + + /** Starts sending a pack's zip to the server, a few chunks a tick. */ + static void submit(byte[] zip) { + submitting = zip; + submitted = 0; + } + + /** How far along the zip being sent is, from 0 to 100, or -1 when none is. */ + static int submitProgress() { + byte[] zip = submitting; + return zip != null ? (int) (submitted * 100L / zip.length) : -1; + } + + public static void tick() { + tickSubmit(); + WorkshopScreen screen = waiting; + Minecraft mc = Minecraft.getInstance(); + if (screen == null || mc.player == null) { + return; + } + boolean flying = mc.player.getVehicle() instanceof AircraftEntity; + airborne |= flying; + ticks++; + if (airborne ? !flying : ticks > SPAWN_TIMEOUT_TICKS) { + waiting = null; + // Not over another screen the player has opened in flight, such as the pause menu. + if (mc.screen == null) { + mc.setScreen(screen); + } + } + } + + private static void tickSubmit() { + byte[] zip = submitting; + if (zip == null) { + return; + } + for (int i = 0; i < SUBMIT_CHUNKS_PER_TICK && submitted < zip.length; i++) { + int end = Math.min(submitted + PackSubmitPacket.CHUNK_SIZE, zip.length); + ModNetwork.sendToServer(new PackSubmitPacket(submitted, zip.length, Arrays.copyOfRange(zip, submitted, end))); + submitted = end; + } + if (submitted >= zip.length) { + submitting = null; + } + } + + public static void loggedOut() { + submitting = null; + WorkshopScreen screen = waiting; + waiting = null; + if (screen != null) { + screen.releaseAssets(); + } + } +} From de3701009707418d4371865b8196aff5a1094786 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:38 +0800 Subject: [PATCH 0475/1461] Add client/workshop/WorkshopHomeScreen.java to the 26.1 shared sources --- .../client/workshop/WorkshopHomeScreen.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java new file mode 100644 index 00000000..7fa4ba6c --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java @@ -0,0 +1,201 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.workshop.ModelImport; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopKind; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Where the workshop starts: the vehicles in the content packs on the left, ready to open, and on + * the right a form to start a new one from an OBJ file, which can be dropped on the window. + */ +public class WorkshopHomeScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int FORM_WIDTH = 190; + private static final int MAX_LIST_WIDTH = 300; + private static final int LINE_HEIGHT = 10; + + private final List entries; + private int scroll; + private WorkshopKind kind = WorkshopKind.HELICOPTER; + private String vehicleText = ""; + private String packText = ""; + private String modelText = ""; + private @Nullable Component error; + private @Nullable EditBox vehicleBox; + private @Nullable EditBox packBox; + private @Nullable EditBox modelBox; + + public WorkshopHomeScreen() { + super(Component.translatable("gui.mcheli.workshop.title")); + entries = WorkshopProject.list(MCHeli.packs().folder()); + } + + @Override + protected void init() { + listRows(); + form(); + } + + private int listWidth() { + return Math.clamp(width - FORM_WIDTH - 30, 100, MAX_LIST_WIDTH); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + private void listRows() { + int rows = visibleRows(); + for (int i = scroll; i < entries.size() && i < scroll + rows; i++) { + WorkshopProject.Entry entry = entries.get(i); + String suffix = entry.zipped() ? " (zip)" : ""; + addRenderableWidget(Button.builder(Component.literal(entry.pack() + " / " + entry.vehicle() + suffix), b -> open(entry)) + .bounds(10, TOP + (i - scroll) * ROW, listWidth(), 20).build()); + } + } + + private void form() { + int x = width - FORM_WIDTH - 10; + int y = TOP; + vehicleBox = box(x, y + 10, Component.translatable("gui.mcheli.workshop.new.vehicle"), vehicleText, 64); + vehicleBox.setResponder(text -> { + // The pack follows the vehicle's name until the player gives it its own. + if (packBox != null && packBox.getValue().equals(vehicleText)) { + packBox.setValue(text); + } + vehicleText = text; + }); + packBox = box(x, y + 44, Component.translatable("gui.mcheli.workshop.new.pack"), packText, 64); + packBox.setResponder(text -> packText = text); + addRenderableWidget(Button.builder(kindName(), b -> { + kind = WorkshopKind.values()[(kind.ordinal() + 1) % WorkshopKind.values().length]; + b.setMessage(kindName()); + }).bounds(x, y + 68, FORM_WIDTH, 20).build()); + modelBox = box(x, y + 102, Component.translatable("gui.mcheli.workshop.new.model"), modelText, 1024); + modelBox.setResponder(text -> modelText = text); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.new.create"), b -> create()) + .bounds(x, y + 126, FORM_WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.home.packs"), b -> minecraft.setScreen(new WorkshopPacksScreen())) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private EditBox box(int x, int y, Component name, String value, int maxLength) { + EditBox box = new EditBox(font, x, y, FORM_WIDTH, 18, name); + box.setMaxLength(maxLength); + box.setValue(value); + return addRenderableWidget(box); + } + + private Component kindName() { + return Component.translatable("gui.mcheli.workshop.new.kind", Component.translatable("gui.mcheli.workshop.kind." + kind.id())); + } + + private void open(WorkshopProject.Entry entry) { + try { + minecraft.setScreen(new WorkshopScreen(WorkshopProject.open(MCHeli.packs().folder(), entry))); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + private void create() { + String vehicle = vehicleText.trim(); + AircraftDefinition existing = MCHeli.content().any(vehicle); + if (existing != null && existing.pack().equals(ContentSource.ORIGINAL)) { + // The import's models come from a resource pack, which would win over this vehicle's. + error = Component.translatable("gui.mcheli.workshop.new.name_taken", vehicle); + return; + } + try { + Path obj = Path.of(modelText.trim().replace("\"", "")); + String author = minecraft.player != null ? minecraft.player.getGameProfile().name() : ""; + WorkshopProject project = WorkshopProject.create(MCHeli.packs().folder(), packText.trim(), kind, vehicle, author, + obj, ModelImport.findTexture(obj)); + minecraft.setScreen(new WorkshopScreen(project)); + } catch (InvalidPathException e) { + error = Component.translatable("gui.mcheli.workshop.new.bad_path", modelText); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + /** A file dropped on the window fills in the form, its name standing in for the vehicle's. */ + @Override + public void onFilesDrop(List files) { + if (files.isEmpty() || modelBox == null || vehicleBox == null) { + return; + } + Path file = files.getFirst(); + modelBox.setValue(file.toString()); + if (vehicleText.isEmpty()) { + String name = file.getFileName().toString(); + int dot = name.lastIndexOf('.'); + vehicleBox.setValue((dot > 0 ? name.substring(0, dot) : name).toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "-")); + } + error = null; + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int max = Math.max(entries.size() - visibleRows(), 0); + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, max); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractRenderState(graphics, mouseX, mouseY, a); + graphics.centeredText(font, title, width / 2, 10, TEXT); + graphics.text(font, Component.translatable("gui.mcheli.workshop.home.open"), 10, TOP - 12, DIM); + if (entries.isEmpty()) { + graphics.text(font, Component.translatable("gui.mcheli.workshop.home.none"), 10, TOP, DIM); + } + int x = width - FORM_WIDTH - 10; + graphics.text(font, Component.translatable("gui.mcheli.workshop.home.new"), x, TOP - 12, DIM); + graphics.text(font, Component.translatable("gui.mcheli.workshop.new.vehicle"), x, TOP, DIM); + graphics.text(font, Component.translatable("gui.mcheli.workshop.new.pack"), x, TOP + 34, DIM); + graphics.text(font, Component.translatable("gui.mcheli.workshop.new.model"), x, TOP + 92, DIM); + int y = TOP + 150; + for (var line : font.split(Component.translatable("gui.mcheli.workshop.home.drop"), FORM_WIDTH)) { + graphics.text(font, line, x, y, DIM); + y += LINE_HEIGHT; + } + if (error != null) { + y += 2; + for (var line : font.split(error, FORM_WIDTH)) { + graphics.text(font, line, x, y, ERROR); + y += LINE_HEIGHT; + } + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From 45db7323dbdb090f274bec70bf75c942bd5f58de Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:39 +0800 Subject: [PATCH 0476/1461] Add client/workshop/WorkshopPacksScreen.java to the 26.1 shared sources --- .../client/workshop/WorkshopPacksScreen.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java new file mode 100644 index 00000000..1b3213b9 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java @@ -0,0 +1,218 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.ClientPacks; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.workshop.PackCatalog; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Util; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * The package browser: the packs in {@code mcheli/packs} and those the server in use sent, who made + * them and what is in them. A zip someone gave the player can be dropped on it to install. + * + *

This only ever reads the packs and adds to the folder. Removing one is left to the file manager + * the Open folder button opens, since a pack can be the only copy of someone's work. + */ +public class WorkshopPacksScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int GOOD = 0xFF80E080; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int LIST_WIDTH = 170; + private static final int LINE_HEIGHT = 10; + private static final int VEHICLES_SHOWN = 8; + + /** A pack in the list, and where it came from. */ + private record Row(PackCatalog.Info info, boolean fromServer) { + } + + private final List rows = new ArrayList<>(); + private final List problems = new ArrayList<>(); + private int scroll; + private int selected; + private @Nullable Component message; + private int messageColor = DIM; + + public WorkshopPacksScreen() { + super(Component.translatable("gui.mcheli.workshop.packs.title")); + read(); + } + + private void read() { + rows.clear(); + problems.clear(); + for (ContentPack pack : ClientPacks.INSTANCE.serverPacks()) { + try { + rows.add(new Row(PackCatalog.info(pack), true)); + } catch (IOException e) { + problems.add(new PackProblem(pack.id(), "could not be read: " + e)); + } + } + PackCatalog.Listing own = PackCatalog.read(MCHeli.packs().folder()); + for (PackCatalog.Info info : own.packs()) { + // A pack the player has that the server also uses is listed once, as the server's. + if (rows.stream().noneMatch(row -> row.info().hash().equals(info.hash()))) { + rows.add(new Row(info, false)); + } + } + problems.addAll(own.problems()); + selected = Math.clamp(selected, 0, Math.max(rows.size() - 1, 0)); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + @Override + protected void init() { + scroll = Math.clamp(scroll, 0, Math.max(rows.size() - visibleRows(), 0)); + for (int i = scroll; i < rows.size() && i < scroll + visibleRows(); i++) { + int index = i; + Button row = addRenderableWidget(Button.builder(Component.literal(rows.get(i).info().manifest().name()), b -> { + selected = index; + rebuildWidgets(); + }).bounds(10, TOP + (i - scroll) * ROW, LIST_WIDTH, 20).build()); + row.active = i != selected; + } + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.packs.open_folder"), b -> openFolder()) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private void openFolder() { + Path folder = MCHeli.packs().folder(); + try { + Files.createDirectories(folder); + Util.getPlatform().openPath(folder); + } catch (IOException e) { + say(Component.literal(e.toString()), ERROR); + } + } + + private void say(Component text, int color) { + message = text; + messageColor = color; + } + + @Override + public void onFilesDrop(List files) { + for (Path file : files) { + if (!file.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) { + say(Component.translatable("gui.mcheli.workshop.packs.not_zip", file.getFileName().toString()), ERROR); + continue; + } + try { + PackManifest installed = PackCatalog.install(file, MCHeli.packs().folder()); + say(Component.translatable("gui.mcheli.workshop.packs.installed", installed.name()), GOOD); + read(); + rebuildWidgets(); + } catch (WorkshopException e) { + say(Component.literal(e.getMessage()), ERROR); + } + } + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, Math.max(rows.size() - visibleRows(), 0)); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void onClose() { + minecraft.setScreen(new WorkshopHomeScreen()); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractRenderState(graphics, mouseX, mouseY, a); + graphics.centeredText(font, title, width / 2, 10, TEXT); + if (rows.isEmpty()) { + graphics.text(font, Component.translatable("gui.mcheli.workshop.packs.none"), 10, TOP, DIM); + } + int x = 10 + LIST_WIDTH + 10; + int w = width - x - 10; + int y = TOP; + if (selected < rows.size()) { + y = details(graphics, rows.get(selected), x, y, w) + LINE_HEIGHT; + } + for (PackProblem problem : problems) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.problem", problem.pack(), problem.message()), x, y, w, ERROR); + } + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.drop"), x, y + 4, w, DIM); + if (message != null) { + wrapped(graphics, message, x, y + 4, w, messageColor); + } + } + + private int details(GuiGraphicsExtractor graphics, Row row, int x, int y, int w) { + PackCatalog.Info info = row.info(); + PackManifest m = info.manifest(); + y = wrapped(graphics, Component.literal(m.name() + " " + m.version()), x, y, w, TEXT); + if (!m.author().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.author", m.author()), x, y, w, TEXT); + } + if (!m.description().isEmpty()) { + y = wrapped(graphics, Component.literal(m.description()), x, y + 2, w, DIM); + } + y += 4; + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.id", m.id()), x, y, w, DIM); + // A small pack is still something, and 0.0 MB reads as an empty one. + String megabytes = "%.1f".formatted(Math.max(info.size() / 1048576.0, 0.1)); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.size", megabytes, ContentPack.shortHash(info.hash())), x, y, w, DIM); + Component source = row.fromServer() ? Component.translatable("gui.mcheli.workshop.packs.from_server") + : Component.literal(info.source().getFileName().toString() + (info.zipped() ? "" : "/")); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.source", source), x, y, w, DIM); + y += 4; + if (!info.vehicles().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.vehicles", info.vehicles().size(), shortList(info.vehicles())), x, y, w, TEXT); + } + if (!info.weapons().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.weapons", info.weapons().size(), shortList(info.weapons())), x, y, w, TEXT); + } + return y; + } + + private static String shortList(List names) { + if (names.size() <= VEHICLES_SHOWN) { + return String.join(", ", names); + } + return String.join(", ", names.subList(0, VEHICLES_SHOWN)) + ", ..."; + } + + private int wrapped(GuiGraphicsExtractor graphics, Component text, int x, int y, int w, int color) { + for (FormattedCharSequence line : font.split(text, w)) { + graphics.text(font, line, x, y, color); + y += LINE_HEIGHT; + } + return y; + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From 6341de87b35dd1d022913a2ffcc01b0d96b15d41 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:39 +0800 Subject: [PATCH 0477/1461] Add client/workshop/WorkshopScreen.java to the 26.1 shared sources --- .../client/workshop/WorkshopScreen.java | 1646 +++++++++++++++++ 1 file changed, 1646 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java new file mode 100644 index 00000000..6017c3cd --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java @@ -0,0 +1,1646 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.render.AircraftRenderState; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.workshop.AssetImport; +import com.jnhprojects.mcheli.workshop.EditHistory; +import com.jnhprojects.mcheli.workshop.PartField; +import com.jnhprojects.mcheli.workshop.PartType; +import com.jnhprojects.mcheli.workshop.TuningKey; +import com.jnhprojects.mcheli.workshop.WorkshopCamera; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopPart; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.FormattedCharSequence; +import org.joml.Matrix3x2fStack; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.TreeSet; +import java.util.function.Consumer; + +/** + * The workshop's editor. The vehicle turns in the middle, drawn by the game's own vehicle renderer + * through a GUI entity, so what shows is what will fly. The parts list is on the left, the values + * of what is selected on the right, and gizmos for every part lie over the model. + * + *

Gizmos are plain GUI drawing at the points {@link WorkshopCamera#project} gives. They always + * show, even when the part is inside the hull, which is where most seats and pivots are. + * + *

Left drag on empty space turns the view, right drag moves it, the wheel zooms. Dragging a + * handle moves the part in the plane of the screen and an arrow along its axis, snapping to a + * sixteenth of a block unless Shift is held. + */ +public class WorkshopScreen extends Screen { + private static final int LEFT = 112; + private static final int RIGHT = 144; + private static final int TOP = 26; + private static final int BOTTOM = 26; + private static final int LIST_ROW = 16; + private static final int FIELD_ROW = 20; + private static final int LABEL_ROW = 10; + + private static final int PANEL = 0xFF181818; + private static final int VIEW = 0xFF2A2F36; + private static final int GRID = 0xFF3A414A; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int WARNING = 0xFFFFC040; + private static final int INVALID = 0xFFFF6060; + private static final int X_COLOR = 0xFFFF5050; + private static final int Y_COLOR = 0xFF60E060; + private static final int Z_COLOR = 0xFF5090FF; + private static final int SELECTED = 0xFFFFFFFF; + private static final int HIT_BOX = 0xFF00E0FF; + private static final int AXIS = 0xFFFF60FF; + + private static final int HANDLE_RADIUS = 3; + private static final double PICK_DISTANCE = 6.0; + private static final double ARROW_LENGTH = 34.0; + private static final double SNAP = 16.0; + private static final int GRID_HALF_SIZE = 16; + private static final int GRID_STEP = 2; + private static final double ZOOM_STEP = 1.15; + + private static final int PREVIEW_SIZE = 64; + private static final int PLOT_HEIGHT = 56; + private static final float PITCH_SNAP = 20.0F; + private static final String DEFAULT_CURVE = "0:0.7, 0.25:0.85, 0.5:1, 0.75:1.15, 1:1.3"; + + private enum Tab { + PARTS, PAINT, SOUND, TUNING, PACK + } + + private enum Drag { + NONE, ORBIT, PAN, PLANE, AXIS_X, AXIS_Y, AXIS_Z, CURVE + } + + private record Label(Component text, int x, int y, int color) { + } + + private record WrappedLine(FormattedCharSequence text, int x, int y) { + } + + private record MergeKey(int line, String field, int component) { + } + + private final WorkshopProject project; + private final WorkshopCamera camera = new WorkshopCamera(); + private final EditHistory> history = new EditHistory<>(); + private final List

Movement is simulated wherever {@link #isLocalInstanceAuthoritative()} is true. With a player + * at the controls that is the pilot's client, exactly like vanilla boats, and the server checks + * the result through the regular vehicle move packet. Without a pilot the server simulates the + * vehicle on its own. Roll, throttle and key state are not part of the vanilla packet, so the + * pilot sends them separately and the server republishes them through synched data. + * + *

Seats are explicit. Vanilla only knows an ordered passenger list, so the seat each passenger + * occupies is kept in a synched seat map of entity ids. Seat zero is the pilot and decides who + * controls the vehicle; crew seats follow, then cargo racks. + */ +public abstract class AircraftEntity extends VehicleEntity { + private static final EntityDataAccessor DATA_TYPE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TEXTURE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_HP = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_THROTTLE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_REVERSE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_ROLL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_HOVERING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_GUNNER_MODE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_INPUT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_PARTS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SEATS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_SELECTION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_USE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TV_MISSILE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_FLARE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SINKING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_FUEL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + /** The UAV station a drone is flown from, by entity id, or 0. */ + private static final EntityDataAccessor DATA_STATION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_REPELLING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SEARCH_LIGHT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + + /** Marks a player flying a drone from a station, so a stale hide can be undone at login. */ + public static final String REMOTE_PILOT_TAG = "mcheli_remote_pilot"; + + // Bits of DATA_PARTS, the same assignment the original used for its part status word. + protected static final int PART_CANOPY = 0; + protected static final int PART_NOZZLE = 1; + protected static final int PART_LANDING_GEAR = 2; + protected static final int PART_WING = 3; + protected static final int PART_HATCH = 4; + + private static final int EMPTY = -1; + /** How far above its hit box the original drew a vehicle's model; see {@link #waterDepth}. */ + private static final float LEGACY_MODEL_LIFT = 0.35F; + /** How fast a cooked-off boat goes under, in blocks per tick (about 0.4 blocks a second). */ + private static final double SINK_SPEED = 0.02; + /** The bow-up pitch and the list a sinking boat settles into, in degrees. */ + private static final float SINK_PITCH = 14.0F; + private static final float SINK_LIST = 12.0F; + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + protected final ControlInput input = new ControlInput(); + public final AnimationState animation = new AnimationState(); + + /** Throttle in [0, 1] as seen by the simulating side. Other sides read {@link #DATA_THROTTLE}. */ + protected double throttle; + /** Reverse throttle in [0, 0.6] for vehicles with {@code EnableBack}. */ + protected float reverse; + protected float roll; + public float rollO; + protected int modeSwitchCooldown; + private int ticksAlive; + + private @Nullable AircraftDefinition cachedDefinition; + private String cachedName = ""; + private int cachedGeneration; + + protected @Nullable PartState canopy; + protected @Nullable PartState landingGear; + protected @Nullable PartState hatch; + private final List parts = new ArrayList<>(); + + private int[] seatIds = new int[0]; + private int pendingSeat = EMPTY; + private final Map savedSeats = new HashMap<>(); + private final Map lastSeats = new HashMap<>(); + /** The station a drone is flown from, and where its pilot goes back to. Server only. */ + private @Nullable UUID station; + private @Nullable Vec3 stationReturn; + /** Remote pilots leaving this tick, by entity id, with where they come back down. */ + private final Map remoteReturns = new HashMap<>(); + /** Where a workshop test flight started, or {@code null} for an ordinary vehicle. Server only. */ + private @Nullable Vec3 testFlightStart; + private boolean testFlightOver; + /** How far the ropes hang below their hooks, as a negative number; both sides let them out. */ + private float ropeLength; + private int repellingTicks; + private int lastRope; + /** Passengers are leaving one by one under parachutes. Server only. */ + private boolean droppingTroops; + + private @Nullable VehicleWeapons weapons; + private int @Nullable [] savedAmmo; + private @Nullable TvMissileEntity tvMissile; + + // Damage model + /** Damage factor of the extra hit box the last projectile ray struck, {@code lastBBDamageFactor}. */ + private float hitBoxFactor = 1.0F; + private int timeSinceHit; + /** The fuel load from which an aircraft counts as heavy: the H8K flying boat and up. */ + private static final int HEAVY_FUEL = 3000; + /** How long a wreck stays, burning, before it disappears: 30 seconds. */ + private static final int WRECK_TICKS = 600; + /** The wreck fire dies down over the last five seconds. */ + private static final int WRECK_FADE_TICKS = 100; + /** Ticks until a destroyed wreck disappears; above zero means destroyed. Server only. */ + private int despawnCount; + /** Ticks the client has seen this vehicle as a wreck, for the fire to die down on time. */ + private int wreckTicks; + /** Ticks since this aircraft was shot down, for the crash spin. Counted on both sides. */ + private int spinTicks; + private boolean exploded; + /** Blasts still to come while a wrecked boat cooks off, and ticks until the next one. */ + private int cookOffBlasts; + private int cookOffDelay; + private @Nullable Entity cookOffCause; + private int alertCount; + + /** Fuel burnt but not yet taken from the tank, which only holds whole units. Server only. */ + private double fuelBurnt; + /** Ticks a fuel truck nearby keeps this vehicle from burning fuel. Server only. */ + private int fuelSuppliedCount; + /** One bit per seat whose occupant has night vision on. Server only, not saved. */ + private int nightVisionSeats; + /** Fuel and parachute slots and the cargo chest. Only the server's copy holds anything. */ + private final VehicleInventory inventory = new VehicleInventory(this); + + // Crashes, measured from position so they work whichever side simulates the vehicle. + /** Blocks per tick into terrain, sideways, that count as a crash: about 25 m/s. */ + private static final double TERRAIN_CRASH_SPEED = 1.25; + /** + * How far a hull reaches past its main box, at most, when looking for neighbours: other + * vehicles are found by their main box, and the longest hulls stretch about this far from it. + */ + private static final double HULL_REACH = 24.0; + /** Closing speed between two vehicles that counts as a crash: about 10 m/s. */ + private static final double VEHICLE_CRASH_SPEED = 0.5; + /** Highest point since a ground vehicle left the ground, or NaN while it is on it. */ + private double airborneFromY = Double.NaN; + private @Nullable Vec3 lastTickPos; + private Vec3 lastVelocity = Vec3.ZERO; + private int collisionCooldown; + + protected AircraftEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_TYPE, ""); + builder.define(DATA_TEXTURE, ""); + builder.define(DATA_HP, 0); + builder.define(DATA_THROTTLE, 0.0F); + builder.define(DATA_REVERSE, 0.0F); + builder.define(DATA_ROLL, 0.0F); + builder.define(DATA_HOVERING, false); + builder.define(DATA_GUNNER_MODE, false); + builder.define(DATA_INPUT, (byte) 0); + builder.define(DATA_PARTS, 0); + builder.define(DATA_SEATS, ""); + builder.define(DATA_WEAPON_SELECTION, ""); + builder.define(DATA_WEAPON_USE, 0); + builder.define(DATA_TV_MISSILE, 0); + builder.define(DATA_FLARE, false); + builder.define(DATA_SINKING, false); + builder.define(DATA_FUEL, 0); + builder.define(DATA_STATION, 0); + builder.define(DATA_REPELLING, false); + builder.define(DATA_SEARCH_LIGHT, false); + } + + // Definition + + /** + * Looks the definition up by name. Returns {@code null} for vehicles whose content was removed. + * After a content reload the next call picks up the new definition object. + */ + public @Nullable AircraftDefinition definition() { + String name = typeName(); + int generation = MCHeli.content().generation(); + if (!name.equals(cachedName) || generation != cachedGeneration) { + boolean reloaded = name.equals(cachedName); + cachedName = name; + cachedGeneration = generation; + AircraftDefinition previous = cachedDefinition; + cachedDefinition = name.isEmpty() ? null : lookup(name); + if (cachedDefinition != null && cachedDefinition != previous) { + if (reloaded && weapons != null) { + // The same vehicle with new numbers: keep the rounds it carries. + savedAmmo = weapons.saveAmmo(); + } + onDefinitionChanged(cachedDefinition); + } + } + return cachedDefinition; + } + + /** Finds a definition of this entity's kind. */ + protected abstract @Nullable AircraftDefinition lookup(String name); + + /** Builds the per vehicle state that depends on the definition: seats, parts, animation arrays. */ + protected void onDefinitionChanged(AircraftDefinition def) { + if (seatIds.length != def.seatAndRackCount()) { + int[] resized = new int[def.seatAndRackCount()]; + Arrays.fill(resized, EMPTY); + System.arraycopy(seatIds, 0, resized, 0, Math.min(seatIds.length, resized.length)); + seatIds = resized; + } + parts.clear(); + canopy = def.canopies.isEmpty() ? null : addPart(PART_CANOPY, 3.5F, PartState.Sounds.CANOPY); + landingGear = def.landingGear.isEmpty() ? null : addPart(PART_LANDING_GEAR, 2.5F, PartState.Sounds.LANDING_GEAR); + hatch = def.hatches.isEmpty() ? null : addPart(PART_HATCH, 1.5F, PartState.Sounds.HATCH); + animation.resize(def); + weapons = new VehicleWeapons(this, def); + if (savedAmmo != null) { + weapons.loadAmmo(savedAmmo); + savedAmmo = null; + } + } + + protected PartState addPart(int bit, float speed, PartState.Sounds sounds) { + PartState state = new PartState(this, bit, speed, sounds); + // A vehicle that comes into view with a part already switched should not animate it. + state.force(state.status()); + parts.add(state); + return state; + } + + /** Sets which vehicle this is. Call once, right after construction, before adding to the level. */ + public void setType(AircraftDefinition definition) { + entityData.set(DATA_TYPE, definition.name()); + entityData.set(DATA_TEXTURE, definition.textures().getFirst()); + entityData.set(DATA_HP, definition.maxHp); + entityData.set(DATA_FUEL, definition.maxFuel); + refreshDimensions(); + definition(); + } + + public String typeName() { + return entityData.get(DATA_TYPE); + } + + public String texture() { + String texture = entityData.get(DATA_TEXTURE); + return texture.isEmpty() ? typeName() : texture; + } + + public int hp() { + return entityData.get(DATA_HP); + } + + /** + * {@code repair}: adds hit points up to the maximum. A wreck stays a wreck. Server only. + * + * @return whether anything was repaired + */ + public boolean repair(int amount) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || hp() >= def.maxHp) { + return false; + } + entityData.set(DATA_HP, Math.min(hp() + amount, def.maxHp)); + return true; + } + + /** + * {@code switchNextTextureName}: the next paint scheme the definition lists. Server only. + * + * @return false when the vehicle has only one + */ + public boolean nextTexture() { + AircraftDefinition def = definition(); + if (def == null || def.textures().size() < 2) { + return false; + } + List textures = def.textures(); + int next = (textures.indexOf(texture()) + 1) % textures.size(); + entityData.set(DATA_TEXTURE, textures.get(next)); + return true; + } + + // Fuel + + public int fuel() { + return entityData.get(DATA_FUEL); + } + + public int maxFuel() { + AircraftDefinition def = definition(); + return def != null ? def.maxFuel : 0; + } + + /** {@code getFuelP}: how full the tank is, 0 to 1. Vehicles without a tank read as full. */ + public float fuelRatio() { + int max = maxFuel(); + return max <= 0 ? 1.0F : (float) fuel() / max; + } + + /** {@code canUseFuel}: a vehicle without a tank never runs dry. */ + public boolean hasFuel() { + return maxFuel() <= 0 || fuel() > 1; + } + + /** Sets the tank outright; a drone leaves its station with 5%. Server only. */ + public void setFuelLevel(int fuel) { + entityData.set(DATA_FUEL, Math.clamp(fuel, 0, Math.max(maxFuel(), 0))); + } + + /** + * Pours up to {@code amount} into the tank. Server only. + * + * @return how much went in + */ + public int refuel(int amount) { + int added = Math.clamp(maxFuel() - fuel(), 0, Math.max(amount, 0)); + if (added > 0) { + entityData.set(DATA_FUEL, fuel() + added); + } + return added; + } + + // Night vision + + /** A crew member switched night vision on or off. Server only. */ + public void setNightVision(Player player, boolean on) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (seat < 0 || seat >= 31) { + return; + } + if (on && def != null && def.nightVision && !isDestroyed()) { + nightVisionSeats |= 1 << seat; + } else if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(player); + } + } + + /** Keeps night vision on for everyone in the crew who switched it on, see {@link NightVision}. */ + private void tickNightVision() { + if (nightVisionSeats == 0) { + return; + } + if (isDestroyed()) { + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null) { + NightVision.end(crew); + } + } + nightVisionSeats = 0; + return; + } + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (hasNightVision(seat) && crew != null) { + NightVision.topUp(crew); + } + } + } + + /** Only the first 31 seats can switch it on; racks and the like come after them anyway. */ + private boolean hasNightVision(int seat) { + return seat >= 0 && seat < 31 && (nightVisionSeats & 1 << seat) != 0; + } + + /** + * {@code updateFuel}: once a second the engine burns its {@code FuelConsumption}, scaled by the + * throttle and topping out at 70% throttle. Nothing burns next to a fuel truck or with the + * server's infinite fuel on. + */ + private void burnFuel(AircraftDefinition def) { + if (def.maxFuel <= 0 || isDestroyed()) { + return; + } + if (fuelSuppliedCount > 0) { + fuelSuppliedCount--; + return; + } + if (ticksAlive % 20 != 0 || fuel() <= 1 || throttle <= 0.0 || ServerConfig.infiniteFuel()) { + return; + } + fuelBurnt += Math.min(throttle * 1.4, 1.0) * def.fuelConsumption; + if (fuelBurnt > 1.0) { + int whole = (int) fuelBurnt; + fuelBurnt -= whole; + entityData.set(DATA_FUEL, Math.max(fuel() - whole, 0)); + } + } + + /** + * {@code supplyFuel}: a fuel truck ({@code FuelSupplyRange}) tops up every vehicle within range, + * 30 units every half second. While it is on the ground it only reaches vehicles that are on + * the ground too. + */ + private void supplyFuel(AircraftDefinition def) { + float range = def.fuelSupplyRange; + if (range <= 0.0F || ticksAlive % 10 != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + if (other == this) { + continue; + } + if (!onGround() || other.onGround()) { + other.refuel(30); + } + other.fuelSuppliedCount = 40; + } + } + + // Inventory and supply + + public VehicleInventory inventory() { + return inventory; + } + + /** + * {@code canSupply}: fuel cans and resupply only work while the vehicle stands on something, + * within three blocks; a vehicle that cannot float must not be in the water either. + */ + public boolean canSupply() { + AircraftDefinition def = definition(); + return def != null && hasBlockBelow(1, 3, false) && (def.floats || !isInWater()); + } + + /** {@code openGui}: shows a crew member the vehicle screen. Server only. */ + public void openScreen(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || player.getVehicle() != this) { + return; + } + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new VehicleMenu(id, playerInventory, this), displayName(def))); + } + + /** + * {@code openInventory}: shows the cargo chest of a vehicle with an {@code InventorySize}. + * Server only. + * + * @return false when the vehicle has none + */ + public boolean openCargo(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || def.cargoRows() <= 0 || isDestroyed() || !inventory.stillValid(player)) { + return false; + } + int rows = Math.min(def.cargoRows(), 6); + MenuType type = switch (rows) { + case 1 -> MenuType.GENERIC_9x1; + case 2 -> MenuType.GENERIC_9x2; + case 3 -> MenuType.GENERIC_9x3; + case 4 -> MenuType.GENERIC_9x4; + case 5 -> MenuType.GENERIC_9x5; + default -> MenuType.GENERIC_9x6; + }; + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new ChestMenu(type, id, playerInventory, inventory.cargo(), rows), + displayName(def))); + return true; + } + + /** The vehicle's name as its item shows it. */ + private static Component displayName(AircraftDefinition def) { + return Component.translatableWithFallback("item.mcheli." + def.name(), def.displayName); + } + + /** + * {@code supplyAmmoToOtherAircraft}: an ammunition truck ({@code AmmoSupplyRange}) tops up the + * weapons of every vehicle within range that stands on something, a tenth of each weapon's + * maximum every two seconds. + */ + private void supplyAmmo(AircraftDefinition def) { + float range = def.ammoSupplyRange; + if (range <= 0.0F || ticksAlive % SupplyRules.AMMO_TRUCK_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + VehicleWeapons theirs = other.weapons(); + if (other == this || theirs == null || other.isDestroyed() || !other.canSupply()) { + continue; + } + for (WeaponSet set : theirs.sets()) { + int rounds = SupplyRules.roundsFromTruck(set.ammo() + set.restAmmo(), set.maxAmmo()); + if (rounds > 0) { + set.setRestAmmo(set.restAmmo() + rounds); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + } + } + } + } + + /** {@code repairOtherAircraft}: a repair vehicle ({@code RepairOtherVehicles}) mends those in range. */ + private void repairOthers(AircraftDefinition def) { + float range = def.repairOtherVehiclesRange; + if (range <= 0.0F || ticksAlive % SupplyRules.REPAIR_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + AircraftDefinition theirs = other.definition(); + if (other != this && theirs != null) { + other.repair(SupplyRules.repair(other.hp(), theirs.maxHp, def.repairOtherVehiclesValue)); + } + } + } + + /** Whether the flight model holds a hover: in hover mode, or while the pilot mans the guns. */ + public boolean isHovering() { + return isHoveringMode() || isGunnerMode(); + } + + /** The hover mode the helicopter pilot switches with jump. */ + public boolean isHoveringMode() { + return entityData.get(DATA_HOVERING); + } + + protected void setHovering(boolean hovering) { + entityData.set(DATA_HOVERING, hovering); + } + + // Pilot gunner mode + + /** + * Pilot gunner mode ({@code EnableGunnerMode}): the pilot leaves the controls to man the + * weapons from the camera position. Helicopters hold a hover; planes level off and circle. + */ + public boolean isGunnerMode() { + return entityData.get(DATA_GUNNER_MODE); + } + + /** {@code canSwitchGunnerMode}: the kinds add their own attitude and speed limits. */ + public boolean canSwitchGunnerMode() { + AircraftDefinition def = definition(); + if (def == null || !def.gunnerMode || !isCanopyClosed() || isDestroyed() || isHoveringMode()) { + return false; + } + // Without ConcurrentGunnerMode a player in the gunner seat has the guns already. + return def.concurrentGunnerMode || !(occupant(1) instanceof Player); + } + + /** Server side. Leaving gunner mode is always allowed; entering it only when {@link #canSwitchGunnerMode}. */ + public void setGunnerMode(boolean on) { + if (!on || canSwitchGunnerMode()) { + entityData.set(DATA_GUNNER_MODE, on); + } + } + + /** Whether {@code entity} is the pilot and currently in gunner mode. */ + public boolean isGunnerMode(Entity entity) { + return isGunnerMode() && seatOf(entity) == 0; + } + + /** Drops gunner mode once its conditions no longer hold. Server side, every tick. */ + private void checkGunnerMode(AircraftDefinition def) { + if (isGunnerMode() && (occupant(0) == null || isDestroyed() + || (!def.concurrentGunnerMode && occupant(1) instanceof Player))) { + entityData.set(DATA_GUNNER_MODE, false); + } + } + + // Throttle, roll and input + + /** Throttle for animation and sound, valid on every side. */ + public float visibleThrottle() { + return isSimulatingHere() ? (float) throttle : entityData.get(DATA_THROTTLE); + } + + public float visibleReverse() { + return isSimulatingHere() ? reverse : entityData.get(DATA_REVERSE); + } + + public float roll() { + return isSimulatingHere() ? roll : entityData.get(DATA_ROLL); + } + + public float roll(float partialTicks) { + return Mth.rotLerp(partialTicks, rollO, roll()); + } + + public void setRoll(float roll) { + this.roll = roll; + } + + protected boolean isSimulatingHere() { + return isLocalInstanceAuthoritative() && !isPassenger(); + } + + public ControlInput input() { + return input; + } + + /** + * Applies the pilot's view change for one frame. Only called on the pilot's client, and only + * for kinds that steer with the mouse. + * + * @param dYaw change of the player's yaw since the last frame, in degrees + * @param dPitch change of the player's pitch since the last frame, in degrees + */ + public void applyPilotLook(float dYaw, float dPitch) { + } + + /** Whether the pilot steers with the mouse, as in helicopters and planes. */ + public boolean steersWithMouse() { + return false; + } + + /** Server side entry point for the pilot's control packet. */ + public void acceptPilotState(float throttle, float reverse, float roll, boolean hovering, byte keys) { + this.throttle = Mth.clamp(throttle, 0.0F, 1.0F); + this.reverse = Mth.clamp(reverse, 0.0F, 1.0F); + this.roll = AircraftRotation.wrap(roll); + input.read(keys); + entityData.set(DATA_THROTTLE, (float) this.throttle); + entityData.set(DATA_REVERSE, this.reverse); + entityData.set(DATA_ROLL, this.roll); + entityData.set(DATA_HOVERING, hovering); + entityData.set(DATA_INPUT, keys); + } + + public float reverse() { + return reverse; + } + + // Tick + + /** + * Makes this a workshop test flight: it takes no damage, is not saved with the world, and goes + * away once its pilot gets out, putting them back where they started. + */ + public void startTestFlight(Vec3 start) { + testFlightStart = start; + } + + @Override + public boolean shouldBeSaved() { + return testFlightStart == null && super.shouldBeSaved(); + } + + @Override + public void tick() { + if (testFlightOver) { + discard(); + return; + } + rollO = roll(); + if (getHurtTime() > 0) { + setHurtTime(getHurtTime() - 1); + } + super.tick(); + interpolation.interpolate(); + ticksAlive++; + lastSeats.clear(); + remoteReturns.clear(); + if (modeSwitchCooldown > 0) { + modeSwitchCooldown--; + } + + AircraftDefinition def = definition(); + if (def == null) { + // Content for this vehicle no longer exists. Keep the entity inert rather than crash. + setDeltaMovement(Vec3.ZERO); + return; + } + if (timeSinceHit > 0) { + timeSinceHit--; + } + if (alertCount > 0) { + alertCount--; + } + boolean groundedBefore = onGround(); + double fallBefore = getDeltaMovement().y; + tickDestroyed(); + + if (!level().isClientSide()) { + supplyFuel(def); + supplyAmmo(def); + repairOthers(def); + burnFuel(def); + if (def.maxFuel > 0 && fuel() < def.maxFuel && !isDestroyed()) { + inventory.pourFuelCans(ticksAlive); + } + tickNightVision(); + tickStation(); + } + tickRopes(def); + + if (isSimulatingHere()) { + if (!(getControllingPassenger() instanceof Player) || isDestroyed()) { + input.clear(); + } + if (isRepelling()) { + // {@code MCH_AircraftClientTickHandler}: the pilot holds a hover while the ropes are out. + input.throttleUp = false; + input.throttleDown = false; + input.left = false; + input.right = false; + } + if (!hasFuel()) { + // An empty tank: the throttle runs down by itself, as with the pilot's hand off it. + input.throttleUp = false; + } + if (entityData.get(DATA_SINKING)) { + sink(); + } else { + simulate(); + } + float[] kick = weapons != null ? weapons.recoilKick() : null; + if (kick != null) { + setXRot(getXRot() + kick[0]); + roll += kick[1]; + } + if (!level().isClientSide()) { + entityData.set(DATA_THROTTLE, (float) throttle); + entityData.set(DATA_REVERSE, reverse); + entityData.set(DATA_ROLL, roll); + entityData.set(DATA_INPUT, input.write()); + } + } else { + if (!isLocalInstanceAuthoritative()) { + input.read(entityData.get(DATA_INPUT)); + } + setDeltaMovement(Vec3.ZERO); + } + for (PartState part : parts) { + part.tick(); + } + if (!level().isClientSide()) { + updatePartsServer(def); + checkGunnerMode(def); + } + animation.tick(this, def); + tickAnimation(); + if (weapons != null) { + weapons.tick(); + if (!level().isClientSide()) { + entityData.set(DATA_FLARE, weapons.flares.isActive()); + warnIncomingMissiles(); + } + } + if (level().isClientSide()) { + damageSmoke(def); + VehicleParticles.tick(this, def); + } else { + tickImpacts(def, groundedBefore, fallBefore); + } + } + + /** + * Server side crashes. A wreck explodes when it lands; an intact vehicle takes damage when it + * meets the ground steeply tilted ({@code onUpdate_CollisionGroundDamage}), sits under water + * without floats, flies into terrain, or rams another vehicle. + */ + private void tickImpacts(AircraftDefinition def, boolean groundedBefore, double fallBefore) { + Vec3 velocity = lastTickPos != null ? position().subtract(lastTickPos) : Vec3.ZERO; + Vec3 previous = lastVelocity; + lastTickPos = position(); + lastVelocity = velocity; + if (collisionCooldown > 0) { + collisionCooldown--; + } + double speed = Math.max(velocity.length(), previous.length()); + + if (isDestroyed()) { + boolean falling = fallBefore < -0.2 || previous.y < -0.2; + boolean landed = onGround() && !groundedBefore; + boolean smashed = horizontalCollision && previous.horizontalDistance() > 0.5; + if (!exploded && (landed && falling || smashed)) { + crashExplosion(); + } + return; + } + + if (isNearGround(3)) { + float roll = Math.abs(Mth.wrapDegrees(roll())); + float pitch = Math.abs(Mth.wrapDegrees(getXRot())); + if (roll > crashTilt() || pitch > crashTilt()) { + float damage = roll + pitch; + damage *= damage < 90.0F ? 0.4F * (float) velocity.length() : 0.4F; + if (damage > 1.0F && random.nextInt(4) == 0) { + hurtServer((ServerLevel) level(), damageSources().inWall(), damage); + } + } + } + if (ticksAlive % 30 == 0 && !def.floats && level().getFluidState( + BlockPos.containing(getX() + 0.5, getY() + 1.5 + def.submergedDamageHeight, getZ() + 0.5)).is(FluidTags.WATER)) { + hurtServer((ServerLevel) level(), damageSources().inWall(), Math.max(def.maxHp / 10, 1)); + } + + if (takesGroundImpacts()) { + groundImpacts(def, previous); + } + + // Not in the original: flying into terrain or another vehicle at speed is a crash. + if (horizontalCollision && previous.horizontalDistance() > TERRAIN_CRASH_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), crashDamage(previous.horizontalDistance())); + } + if (speed > VEHICLE_CRASH_SPEED && collisionCooldown == 0) { + // A blocked move can leave the hulls up to a tick of travel apart; when the vehicle + // stopped short this tick, look that far for what stopped it. + boolean stopped = velocity.length() < previous.length() * 0.5; + double reach = stopped ? 0.5 + previous.length() : 0.5; + for (AircraftEntity other : solidNeighbours(hullBounds().inflate(reach))) { + if (!hullsTouch(other, reach)) { + continue; + } + double closing = velocity.subtract(other.lastVelocity).length(); + closing = Math.max(closing, previous.subtract(other.lastVelocity).length()); + if (closing > VEHICLE_CRASH_SPEED && other.collisionCooldown == 0) { + collisionCooldown = 20; + other.collisionCooldown = 20; + float damage = crashDamage(closing); + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), damage); + other.hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), damage); + break; + } + } + } + } + + /** {@code getGiveDamageRot}: how far a vehicle may lean near the ground before it scrapes. */ + protected float crashTilt() { + return 40.0F; + } + + /** Crash damage grows with the square of the impact speed, like the energy involved. */ + private static float crashDamage(double speed) { + return (float) (speed * speed * 60.0); + } + + /** Whether hitting walls and falling hurt this vehicle. Tanks and cars, which live on the ground. */ + protected boolean takesGroundImpacts() { + return false; + } + + /** + * A ground vehicle that hits a wall or lands after a long drop takes a share of its hit points + * by the speed or the height, see {@link GroundImpact}. The same height costs a car and a tank + * the same share before armour. + */ + private void groundImpacts(AircraftDefinition def, Vec3 previous) { + if (horizontalCollision && previous.horizontalDistance() > GroundImpact.WALL_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), GroundImpact.wall(previous.horizontalDistance()) * def.maxHp); + } + if (isInWater() || isPassenger()) { + airborneFromY = Double.NaN; + } else if (!onGround()) { + airborneFromY = Double.isNaN(airborneFromY) ? getY() : Math.max(airborneFromY, getY()); + } else if (!Double.isNaN(airborneFromY)) { + double drop = airborneFromY - getY(); + airborneFromY = Double.NaN; + if (drop > GroundImpact.SAFE_DROP) { + hurtServer((ServerLevel) level(), damageSources().fall(), GroundImpact.drop(drop) * def.maxHp); + } + } + } + + /** {@code MCH_MissileDetector}: the crew hears a warning while a missile homes on them. */ + private void warnIncomingMissiles() { + if (alertCount > 0 || isDestroyed() || weapons == null || !weapons.flares.isFitted() || ticksAlive % 5 != 0) { + return; + } + boolean tracked = !level().getEntitiesOfClass(ProjectileEntity.class, getBoundingBox().inflate(60.0), + p -> p.target() != null && (p.target() == this || hasPassenger(p.target()))).isEmpty(); + if (tracked) { + alertCount = 20; + ModSounds.playAt(this, "alert", 1.0F, 1.0F); + } + } + + /** One tick of control and physics on the simulating side. */ + protected abstract void simulate(); + + /** Visual state that every side updates, for example rotor angles. */ + protected void tickAnimation() { + } + + /** Ticks since this entity instance was created, used to delay controls right after spawning. */ + protected int ticksAlive() { + return ticksAlive; + } + + // Moving parts + + int partBits() { + return entityData.get(DATA_PARTS); + } + + void setPartBit(int bit, boolean on) { + int bits = entityData.get(DATA_PARTS); + entityData.set(DATA_PARTS, on ? bits | (1 << bit) : bits & ~(1 << bit)); + } + + public @Nullable PartState canopy() { + return canopy; + } + + public @Nullable PartState landingGear() { + return landingGear; + } + + public @Nullable PartState hatch() { + return hatch; + } + + public boolean isCanopyClosed() { + return canopy == null || canopy.isOff(); + } + + protected void openCanopy() { + if (canopy != null && modeSwitchCooldown <= 0) { + canopy.set(true, true); + modeSwitchCooldown = 20; + } + } + + protected void closeCanopy() { + if (canopy != null && modeSwitchCooldown <= 0 && canopy.status()) { + canopy.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean isLandingGearUp() { + return landingGear != null && landingGear.status(); + } + + /** Gear can be raised once the vehicle is at least ten blocks above anything solid. */ + public boolean canRaiseLandingGear() { + return landingGear != null && landingGear.angle() < 1.0F && !landingGear.status() && !hasBlockBelow(3, 10, true); + } + + public boolean canLowerLandingGear() { + return landingGear != null && landingGear.angle() >= 89.0F && landingGear.status(); + } + + public void toggleLandingGear() { + if (landingGear == null || modeSwitchCooldown > 0) { + return; + } + if (canRaiseLandingGear()) { + landingGear.set(true, true); + modeSwitchCooldown = 20; + } else if (canLowerLandingGear()) { + landingGear.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean canFoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOff(); + } + + public boolean canUnfoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOn(); + } + + /** The fold key: hatches by default, planes fold or sweep their wings instead. */ + public void toggleFold() { + if (canFoldHatch()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldHatch()) { + hatch.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Throttle below which the landing gear comes down by itself near the ground. */ + protected float autoLowerGearThrottle() { + return 0.8F; + } + + /** {@code updateParts}: automatic landing gear, plus whatever a kind adds. Server only. */ + protected void updatePartsServer(AircraftDefinition def) { + if (landingGear == null) { + return; + } + if (!landingGear.status() && landingGear.factor() <= 0.1F) { + // Amphibians retract the gear when they settle on water. + boolean onWater = isInWater() || isWaterBelow(20); + if (def.floats && onWater && (throttle <= 0.8 || onGround() || hasBlockBelow(3, 20, true))) { + landingGear.set(true, true); + } + } else if (landingGear.status() && landingGear.factor() >= 0.9F) { + boolean ground = hasBlockBelow(3, 10, true); + if (throttle < autoLowerGearThrottle() && ground) { + if (!def.floats || !isWaterBelow(150)) { + landingGear.set(false, true); + } + } else if (vtolMode() == 2 && ground) { + landingGear.set(false, true); + } + } + } + + /** 0 off, 1 tilting, 2 fully in VTOL mode. Only planes have one. */ + public int vtolMode() { + return 0; + } + + // Seats + + /** Seat (or rack) index of {@code entity}, or -1. */ + public int seatOf(Entity entity) { + int id = entity.getId(); + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == id) { + return i; + } + } + return EMPTY; + } + + public @Nullable Entity occupant(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] == EMPTY) { + return null; + } + int id = seatIds[seat]; + for (Entity passenger : getPassengers()) { + if (passenger.getId() == id) { + return passenger; + } + } + return null; + } + + public @Nullable SeatDefinition seat(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && seat >= 0 && seat < def.seatCount() ? def.seats.get(seat) : null; + } + + private boolean isSeatFree(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] != EMPTY) { + return false; + } + AircraftDefinition def = definition(); + if (def != null) { + for (int[] group : def.exclusiveSeats) { + if (contains(group, seat)) { + for (int other : group) { + if (other != seat && other >= 0 && other < seatIds.length && seatIds[other] != EMPTY) { + return false; + } + } + } + } + } + return true; + } + + private static boolean contains(int[] values, int value) { + for (int v : values) { + if (v == value) { + return true; + } + } + return false; + } + + /** First crew seat a player boarding by hand would take. Drones keep their pilot seat for the remote. */ + private int firstFreeCrewSeat() { + AircraftDefinition def = definition(); + if (def == null) { + return EMPTY; + } + for (int i = def.uav ? 1 : 0; i < def.seatCount(); i++) { + if (isSeatFree(i)) { + return i; + } + } + return EMPTY; + } + + private void assignSeat(Entity entity, int seat) { + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == entity.getId()) { + seatIds[i] = EMPTY; + if (weapons != null) { + weapons.onLeft(i); + } + } + } + if (seat >= 0 && seat < seatIds.length) { + seatIds[seat] = entity.getId(); + } + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onSeated(entity, seat); + weapons.resendAmmo(); + } + } + + private String encodeSeats() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < seatIds.length; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(seatIds[i]); + } + return sb.toString(); + } + + private void decodeSeats(String encoded) { + if (encoded.isEmpty()) { + Arrays.fill(seatIds, EMPTY); + return; + } + String[] parts = encoded.split(","); + if (parts.length != seatIds.length) { + seatIds = new int[parts.length]; + } + for (int i = 0; i < parts.length; i++) { + try { + seatIds[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + seatIds[i] = EMPTY; + } + } + } + + /** Moves {@code entity} to the next free crew seat, wrapping around. Server only. */ + public void switchSeat(Entity entity) { + AircraftDefinition def = definition(); + int current = seatOf(entity); + if (def == null || current < 0 || current >= def.seatCount() || current == 0 && station != null) { + // A remote pilot stays at the controls; they are not really aboard. + return; + } + int count = def.seatCount(); + seatIds[current] = EMPTY; + for (int step = 1; step < count; step++) { + int seat = (current + step) % count; + if (def.uav && seat == 0) { + continue; + } + if (isSeatFree(seat)) { + assignSeat(entity, seat); + if (seat == 0) { + closeCanopy(); + } + return; + } + } + assignSeat(entity, current); + } + + // UAV stations + + /** Whether a player at a station could take this drone's controls now. */ + public boolean canTakeRemotePilot() { + AircraftDefinition def = definition(); + return def != null && def.uav && !def.targetDrone && !isDestroyed() && isSeatFree(0); + } + + /** + * Seats {@code player}, who sits at {@code from}, at this drone's controls. The pilot is hidden + * and takes no harm while flying, since in the original they never left the station. Server only. + */ + public boolean takeRemotePilot(ServerPlayer player, UavStationEntity from) { + if (!canTakeRemotePilot()) { + return false; + } + pendingSeat = 0; + boolean seated = player.startRiding(this, true, true); + pendingSeat = EMPTY; + if (!seated) { + return false; + } + station = from.getUUID(); + stationReturn = from.returnPosition(); + entityData.set(DATA_STATION, from.getId()); + player.addTag(REMOTE_PILOT_TAG); + player.setInvisible(true); + player.setInvulnerable(true); + return true; + } + + /** Sends the remote pilot back to the station, if one is flying. Server only. */ + public void releaseRemotePilot() { + Entity pilot = occupant(0); + if (station != null && pilot != null) { + pilot.stopRiding(); + } + } + + /** The station this drone is flown from, as the client sees it. */ + public @Nullable UavStationEntity stationEntity() { + return level().getEntity(entityData.get(DATA_STATION)) instanceof UavStationEntity s ? s : null; + } + + /** Flat distance to the station this drone is flown from, or -1 without one. */ + public double stationDistance() { + UavStationEntity from = stationEntity(); + if (from == null) { + return -1.0; + } + double dx = getX() - from.getX(); + double dz = getZ() - from.getZ(); + return Math.sqrt(dx * dx + dz * dz); + } + + /** Whether {@code entity} flies this drone from a station rather than sitting in it. */ + public boolean isRemotePilot(Entity entity) { + return (station != null || entityData.get(DATA_STATION) != 0) && occupant(0) == entity; + } + + private void endRemoteControl(Entity pilot) { + UavStationEntity from = level() instanceof ServerLevel server && station != null + && server.getEntity(station) instanceof UavStationEntity s ? s : null; + Vec3 home = from != null ? from.returnPosition() : stationReturn; + if (home != null) { + remoteReturns.put(pilot.getId(), home); + } + clearRemotePilot(pilot); + if (from != null) { + from.setLinked(false); + } + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + } + + /** Undoes what {@link #takeRemotePilot} did to the player. */ + public static void clearRemotePilot(Entity pilot) { + if (pilot.removeTag(REMOTE_PILOT_TAG)) { + pilot.setInvisible(false); + pilot.setInvulnerable(false); + } + } + + /** + * {@code updateUAV}: the link breaks when the station is gone, and flying out of its range + * loses the drone, as in the original. + */ + private void tickStation() { + if (station == null || !(level() instanceof ServerLevel server)) { + return; + } + Entity pilot = occupant(0); + if (pilot == null) { + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + return; + } + UavStationEntity from = server.getEntity(station) instanceof UavStationEntity s && s.isAlive() ? s : null; + if (from == null) { + pilot.stopRiding(); + return; + } + entityData.set(DATA_STATION, from.getId()); + if (!from.inRange(this) && !isDestroyed()) { + if (pilot instanceof Player player) { + player.sendOverlayMessage(Component.translatable("message.mcheli.uav.lost")); + } + pilot.stopRiding(); + hurtServer(server, damageSources().fellOutOfWorld(), hp() + 10.0F); + } + } + + @Override + public @Nullable LivingEntity getControllingPassenger() { + // A wreck is nobody's to fly; the server lets it fall, so its crash is seen the same way by everyone. + return occupant(0) instanceof Player player && !isDestroyed() ? player : null; + } + + @Override + protected boolean canAddPassenger(Entity passenger) { + AircraftDefinition def = definition(); + if (def == null || !def.canRide) { + return false; + } + if (pendingSeat >= 0) { + return isSeatFree(pendingSeat); + } + // Coming back aboard after a reload, a remote pilot included. + Integer saved = savedSeats.get(passenger.getUUID()); + if (saved != null && isSeatFree(saved)) { + return true; + } + // Cargo only boards through a rack, never into a crew seat. + return !(passenger instanceof AircraftEntity || passenger instanceof ContainerEntity) && firstFreeCrewSeat() >= 0; + } + + @Override + protected void addPassenger(Entity passenger) { + super.addPassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = pendingSeat; + pendingSeat = EMPTY; + if (seat < 0) { + Integer saved = savedSeats.remove(passenger.getUUID()); + seat = saved != null && isSeatFree(saved) ? saved : firstFreeCrewSeat(); + } + assignSeat(passenger, seat); + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = seatOf(passenger); + if (seat >= 0) { + if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(passenger); + } + lastSeats.put(passenger.getId(), seat); + seatIds[seat] = EMPTY; + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onLeft(seat); + } + } + if (seat == 0) { + input.clear(); + if (station != null) { + endRemoteControl(passenger); + } + if (testFlightStart != null) { + remoteReturns.put(passenger.getId(), testFlightStart); + testFlightOver = true; + } + } + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + AircraftDefinition def = definition(); + int seat = seatOf(passenger); + if (def == null || seat < 0 || seat >= def.seatAndRackCount()) { + return super.getPassengerAttachmentPoint(passenger, dimensions, scale); + } + Vector3f p = seatPosition(def, seat, 1.0F); + return new Vec3(p.x, p.y, p.z); + } + + /** World offset of a seat from the vehicle origin. Turret seats turn with the pilot's view. */ + public Vector3f seatPosition(AircraftDefinition def, int seat, float partialTicks) { + Vector3 pos; + boolean turret; + if (seat < def.seatCount()) { + SeatDefinition s = def.seats.get(seat); + pos = s.position(); + turret = s.rotatingSeat(); + } else { + RackDefinition rack = def.rack(seat); + pos = rack.position(); + turret = rack.rotatingSeat(); + } + return toWorldOffset(turret ? onTurret(def, pos, partialTicks) : pos); + } + + /** Rotates a hull-space point around {@code TurretPosition} by the pilot's view yaw. */ + public Vector3 onTurret(AircraftDefinition def, Vector3 pos, float partialTicks) { + Vector3 t = def.turretPosition; + Vector3f v = AircraftRotation.toWorld(animation.seatYaw(0, partialTicks), 0.0F, 0.0F, + pos.x() - t.x(), pos.y() - t.y(), pos.z() - t.z()); + return new Vector3(v.x + t.x(), v.y + t.y(), v.z + t.z()); + } + + /** Rotates a hull-space point into the world frame, relative to the vehicle origin. */ + public Vector3f toWorldOffset(Vector3 local) { + return AircraftRotation.toWorld(getYRot(), getXRot(), roll(), local.x(), local.y(), local.z()); + } + + public Vec3 toWorld(Vector3 local) { + Vector3f v = toWorldOffset(local); + return position().add(v.x, v.y, v.z); + } + + @Override + protected void positionRider(Entity passenger, MoveFunction moveFunction) { + super.positionRider(passenger, moveFunction); + AircraftDefinition def = definition(); + if (def != null && passenger instanceof AircraftEntity cargo) { + RackDefinition rack = def.rack(seatOf(passenger)); + float yaw = rack != null ? rack.yaw() : 0.0F; + float pitch = rack != null ? rack.pitch() : 0.0F; + cargo.setYRot(getYRot() + yaw); + cargo.setXRot(getXRot() + pitch); + cargo.roll = roll(); + cargo.setDeltaMovement(Vec3.ZERO); + } else if (def != null && passenger instanceof ContainerEntity container) { + RackDefinition rack = def.rack(seatOf(passenger)); + container.setYRot(getYRot() + (rack != null ? rack.yaw() : 0.0F)); + container.setDeltaMovement(Vec3.ZERO); + } + } + + @Override + public Vec3 getDismountLocationForPassenger(LivingEntity passenger) { + Vec3 home = remoteReturns.remove(passenger.getId()); + if (home != null) { + return home; + } + AircraftDefinition def = definition(); + Integer seat = lastSeats.get(passenger.getId()); + if (def != null) { + Vector3 local; + if (def.unmountPosition != null) { + local = def.unmountPosition; + } else { + Vector3 seatPos = seat != null && seat < def.seatCount() ? def.seats.get(seat).position() : def.seats.getFirst().position(); + double x = seatPos.x() >= 0.0 ? seatPos.x() + 3.0 : seatPos.x() - 3.0; + local = new Vector3(x, 2.0, seatPos.z()); + } + Vec3 at = toWorld(local); + AABB box = passenger.getDimensions(Pose.STANDING).makeBoundingBox(at); + if (level().noCollision(passenger, box)) { + return at; + } + } + return super.getDismountLocationForPassenger(passenger); + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand, Vec3 location) { + ItemStack held = player.getItemInHand(hand); + if (held.getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (definition() != null && (held.getItem() instanceof FuelItem || held.getItem() instanceof WrenchItem)) { + if (level().isClientSide()) { + return InteractionResult.SUCCESS; + } + return held.getItem() instanceof FuelItem ? FuelItem.pour(held, this, player) : WrenchItem.useOn(held, this, player, hand); + } + InteractionResult result = super.interact(player, hand, location); + AircraftDefinition def = definition(); + if (result == InteractionResult.PASS && player.isSecondaryUseActive() && def != null && def.cargoRows() > 0 + && !isDestroyed() && player.getVehicle() != this) { + // Sneaking opens the cargo chest from outside, as in the original. + if (player instanceof ServerPlayer server) { + openCargo(server); + } + return InteractionResult.SUCCESS; + } + if (result != InteractionResult.PASS || player.isSecondaryUseActive()) { + return result; + } + if (def == null || !def.canRide || player.getVehicle() == this) { + return InteractionResult.PASS; + } + boolean boardingPilot = !def.uav && isSeatFree(0); + if (boardingPilot && canopy != null && isCanopyClosed()) { + // The first click opens the canopy, the next one climbs in. + if (!level().isClientSide()) { + openCanopy(); + } + return InteractionResult.SUCCESS; + } + if (boardingPilot && modeSwitchCooldown > 0) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + if (!player.startRiding(this)) { + return InteractionResult.PASS; + } + if (seatOf(player) == 0) { + closeCanopy(); + boardNearbyMobs(); + } + } + return InteractionResult.SUCCESS; + } + + // Racks + + /** Loads {@code cargo} into the given rack seat. Server only. */ + private boolean loadCargo(Entity cargo, int seat) { + if (!isSeatFree(seat) || cargo.isPassenger()) { + return false; + } + pendingSeat = seat; + boolean loaded = cargo.startRiding(this, true, true); + pendingSeat = EMPTY; + if (loaded) { + level().playSound(null, getX(), getY(), getZ(), SoundEvents.UI_BUTTON_CLICK, SoundSource.NEUTRAL, 1.0F, 1.0F); + } + return loaded; + } + + /** Whether {@code cargo} may ride rack {@code seat} of this vehicle. */ + public boolean acceptsCargo(AircraftEntity cargo, int seat) { + AircraftDefinition def = definition(); + AircraftDefinition cargoDef = cargo.definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + if (rack == null || cargoDef == null || cargo == this || isPassenger() || cargo.isPassenger()) { + return false; + } + // A vehicle that carries cargo itself cannot be loaded. + for (Entity p : cargo.getPassengers()) { + if (p instanceof AircraftEntity) { + return false; + } + } + if (rack.accepts(cargoDef.name(), cargoDef.kind())) { + return true; + } + for (RideRack rr : cargoDef.rideRacks) { + if (rr.carrier().equalsIgnoreCase(def.name()) && def.seatCount() + rr.rack() - 1 == seat) { + return true; + } + } + return false; + } + + /** Whether rack {@code seat} carries containers, {@code AddRack} naming {@code container}. */ + public boolean acceptsContainer(ContainerEntity container, int seat) { + AircraftDefinition def = definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + return rack != null && !isPassenger() && !container.isPassenger() + && rack.accepts(ContainerEntity.CARGO_NAME, ContainerEntity.CARGO_NAME); + } + + private boolean isNearRackEntry(AircraftEntity cargo, RackDefinition rack) { + Vec3 entry = toWorld(rack.entry()); + float r = rack.range(); + return Math.abs(cargo.getX() - entry.x) <= r && Math.abs(cargo.getY() - entry.y) <= r && Math.abs(cargo.getZ() - entry.z) <= r; + } + + /** Carrier pilot loads every nearby vehicle or container that fits a free rack. Server only. */ + public void loadNearbyCargo() { + AircraftDefinition def = definition(); + if (def == null || def.racks.isEmpty() || throttle > 0.3 || !hasBlockBelow(1, 3, true)) { + return; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (!isSeatFree(seat)) { + continue; + } + RackDefinition rack = def.rack(seat); + Vec3 entry = toWorld(rack.entry()); + AABB area = new AABB(entry, entry).inflate(rack.range()); + boolean loaded = false; + for (AircraftEntity cargo : level().getEntitiesOfClass(AircraftEntity.class, area)) { + if (acceptsCargo(cargo, seat) && loadCargo(cargo, seat)) { + loaded = true; + break; + } + } + if (!loaded) { + for (ContainerEntity container : level().getEntitiesOfClass(ContainerEntity.class, area)) { + if (acceptsContainer(container, seat) && loadCargo(container, seat)) { + break; + } + } + } + } + } + + /** Drops the last loaded piece of cargo at its rack's entry point. Server only. */ + public void unloadCargo() { + AircraftDefinition def = definition(); + if (def == null) { + return; + } + for (int seat = def.seatAndRackCount() - 1; seat >= def.seatCount(); seat--) { + Entity cargo = occupant(seat); + if (cargo == null) { + continue; + } + RackDefinition rack = def.rack(seat); + Vector3 drop = rack.entry(); + if (cargo instanceof AircraftEntity) { + // Aircraft roll out well clear of the carrier's tail or nose. + drop = drop.add(0.0, 0.0, drop.z() >= centreZ(def) ? 12.0 : -12.0); + } + Vec3 at = toWorld(drop); + cargo.stopRiding(); + cargo.snapTo(at.x, at.y, at.z, getYRot() + rack.yaw(), 0.0F); + // As in the original, with no ground in the 20 blocks below it goes down under a canopy. + if (!(cargo instanceof AircraftEntity) && !hasBlockBelow(3, 20, false)) { + ParachuteEntity.open(level(), cargo, ParachuteEntity.Kind.DROP); + } + return; + } + } + + /** Leaves the rack this vehicle rides on, at the rack entry point. Server only. */ + public void leaveRack() { + if (!(getVehicle() instanceof AircraftEntity carrier)) { + return; + } + AircraftDefinition carrierDef = carrier.definition(); + RackDefinition rack = carrierDef != null ? carrierDef.rack(carrier.seatOf(this)) : null; + Vec3 at = rack != null ? carrier.toWorld(rack.entry()) : position(); + stopRiding(); + snapTo(at.x, at.y, at.z, getYRot(), 0.0F); + } + + /** + * Pilot of a vehicle near a free rack of a carrier boards that rack. Server only. + * + * @return whether a rack was found + */ + public boolean boardNearbyRack() { + if (isPassenger()) { + return false; + } + for (AircraftEntity carrier : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(60.0))) { + AircraftDefinition def = carrier.definition(); + if (carrier == this || def == null) { + continue; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (carrier.isSeatFree(seat) && carrier.acceptsCargo(this, seat) && carrier.isNearRackEntry(this, def.rack(seat)) + && carrier.loadCargo(this, seat)) { + return true; + } + } + } + return false; + } + + private static double centreZ(AircraftDefinition def) { + if (def.boundingBoxes.isEmpty()) { + return 0.0; + } + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + for (var box : def.boundingBoxes) { + min = Math.min(min, box.offset().z() - box.width() / 2.0); + max = Math.max(max, box.offset().z() + box.width() / 2.0); + } + return (min + max) / 2.0; + } + + // Ejection + + /** Seats zero and one of vehicles with {@code EnableEjectionSeat} can bail out. */ + public boolean canEject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && def.ejectionSeat && seat >= 0 && seat < 2 && !(seat == 0 && def.uav); + } + + /** + * {@code ejectSeat}: throws the occupant clear of the aircraft in an ejection seat, whose + * parachute opens once it starts to fall. Each ejection takes a parachute from the parachute + * slots of the vehicle screen; without one the seat stays put. Creative players need none. + * Server only. + */ + public void eject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + if (def == null || !canEject(entity)) { + return; + } + boolean creative = entity instanceof Player player && player.getAbilities().instabuild; + if (!creative && !inventory.takeParachute()) { + if (entity instanceof ServerPlayer player) { + ModSounds.playTo(player, "ng", 1.0F, 1.0F); + player.sendOverlayMessage(Component.translatable("message.mcheli.no_parachute")); + } + return; + } + Vector3 seatPos = def.seats.get(seat).position(); + Vec3 out = toWorld(seatPos.add(0.0, 2.0, 0.0)); + Vector3f push = toWorldOffset(new Vector3(0.0, 2.0, 0.0)); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement().add(push.x + (random.nextFloat() - 0.5) * 0.1, push.y, + push.z + (random.nextFloat() - 0.5) * 0.1)); + entity.needsSync = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.EJECTION_SEAT); + if (canopy != null && !canopy.status()) { + canopy.set(true, false); + modeSwitchCooldown = 40; + } + ModSounds.playAt(entity, "eject_seat", 5.0F, 1.0F); + } + + // Search lights + + public boolean isSearchLightOn() { + AircraftDefinition def = definition(); + return def != null && !def.searchLights.isEmpty() && entityData.get(DATA_SEARCH_LIGHT); + } + + /** {@code canSwitchSearchLight}: the pilot or the first crew seat, in vehicles that have one. */ + public boolean canSwitchSearchLight(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && !def.searchLights.isEmpty() && seat >= 0 && seat <= 1; + } + + /** Server only. */ + public void toggleSearchLight(Entity entity) { + if (canSwitchSearchLight(entity)) { + entityData.set(DATA_SEARCH_LIGHT, !entityData.get(DATA_SEARCH_LIGHT)); + } + } + + /** + * Where a turning search light points: the first crew seat's view, else the pilot's, as + * {@code renderLight} took them. Null with nobody aboard to aim it. + */ + public @Nullable Entity searchLightAimer() { + Entity crew = occupant(1); + return crew != null ? crew : occupant(0); + } + + // Troops: parachute drops and ropes + + public boolean isRepelling() { + return entityData.get(DATA_REPELLING); + } + + /** How far the ropes hang below their hooks, as a negative number. */ + public float ropeLength() { + return ropeLength; + } + + /** Where rope {@code index} hangs from, in the world. */ + public Vec3 ropePosition(int index) { + AircraftDefinition def = definition(); + return def != null && index >= 0 && index < def.repellingHooks.size() ? toWorld(def.repellingHooks.get(index).position()) : position(); + } + + /** + * {@code onUpdate_Repelling} and {@code onUpdate_UnmountCrew}: the ropes run out towards the + * ground, then the mobs aboard slide down them one per hook interval; a parachute drop lets one + * passenger out per {@code MobDropOption} interval while the hatch is open. + */ + private void tickRopes(AircraftDefinition def) { + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + int clear = airBelow(50); + if (ropeLength > -50.0F && ropeLength > -clear) { + ropeLength -= level().isClientSide() ? 0.3F : 0.25F; + } + } else { + ropeLength = 0.0F; + } + } + if (level().isClientSide()) { + return; + } + if (isRepelling() && isDestroyed()) { + entityData.set(DATA_REPELLING, false); + } + if (!isRepelling()) { + repellingTicks = 0; + } else if (repellingTicks < 60) { + repellingTicks++; + } else { + for (int rope = 0; rope < def.repellingHooks.size(); rope++) { + if (ticksAlive % def.repellingHooks.get(rope).interval() != 0) { + continue; + } + for (int seat = 1; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + rappel(mob, rope); + break; + } + } + } + } + if (droppingTroops) { + if (hasBlockBelow(3, 10, false) || isDestroyed()) { + droppingTroops = false; + } else if ((hatch == null || hatch.isOn()) && ticksAlive % def.dropInterval == 0 && !dropNextTroop(def)) { + droppingTroops = false; + } + } + } + + /** Air blocks straight below, up to {@code max}. */ + private int airBelow(int max) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + int i = 0; + while (i < max && pos.getY() - i > level().getMinY() && level().getBlockState(pos.setY(blockPosition().getY() - i - 1)).isAir()) { + i++; + } + return i; + } + + /** + * {@code canStartRepelling}: a helicopter in hover mode, level and holding still. The original + * compared against where it was a few ticks before. + */ + private boolean canStartRepelling() { + return this instanceof HelicopterEntity && isHoveringMode() && Math.abs(getXRot()) < 3.0F && Math.abs(roll()) < 3.0F + && lastTickPos != null && position().distanceTo(lastTickPos) < 0.05; + } + + /** + * The pilot's drop key, {@code unmountCrew}: with ropes, lets them down from a steady hover + * or pulls them up again; with {@code EnableParachuting}, starts or stops dropping the mobs + * aboard under parachutes; otherwise, or close to the ground, simply lets the mobs out. + * Server only. + * + * @return false if nothing could be done, so the pilot hears it + */ + public boolean dropCrew() { + AircraftDefinition def = definition(); + if (def == null || isDestroyed()) { + return false; + } + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + entityData.set(DATA_REPELLING, false); + return true; + } + if (hasBlockBelow(3, 4, false)) { + return unloadMobs(); + } + if (!canStartRepelling()) { + return false; + } + entityData.set(DATA_REPELLING, true); + repellingTicks = 0; + return true; + } + if (droppingTroops) { + droppingTroops = false; + return true; + } + if (def.parachuting && !hasBlockBelow(3, 10, false)) { + droppingTroops = true; + if (hatch != null && hatch.isOff()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } + return true; + } + return unloadMobs(); + } + + /** Lets every mob in a crew seat out, as in {@code unmountCrew(false)}. */ + private boolean unloadMobs() { + AircraftDefinition def = definition(); + boolean any = false; + for (int seat = 1; def != null && seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + mob.stopRiding(); + any = true; + } + } + return any; + } + + /** The next mob behind the second seat leaves at the drop point under a parachute. */ + private boolean dropNextTroop(AircraftDefinition def) { + for (int seat = 2; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + parachuteOut(mob, def); + return true; + } + } + return false; + } + + private void parachuteOut(Entity entity, AircraftDefinition def) { + Vec3 out = toWorld(def.dropPosition); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement()); + entity.needsSync = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.DROP); + } + + /** + * A passenger's own drop key, {@code MCH_ClientSeatTickHandler}: slide down a rope once the + * ropes are out, or jump with a parachute from behind the second seat of a vehicle with + * {@code EnableParachuting} and room below. Server only. + * + * @return false if neither is possible right now + */ + public boolean jumpOut(Player player) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (def == null || seat <= 0 || seat >= def.seatCount() || isDestroyed()) { + return false; + } + if (isRepelling() && repellingTicks > 50 && !def.repellingHooks.isEmpty()) { + lastRope = (lastRope + 1) % def.repellingHooks.size(); + rappel(player, lastRope); + return true; + } + if (def.parachuting && seat >= 2 && !hasBlockBelow(3, 13, false)) { + parachuteOut(player, def); + return true; + } + return false; + } + + /** {@code unmountEntityRepelling}: two blocks under the hook, onto a rope rider that lets them down. */ + private void rappel(Entity entity, int rope) { + Vec3 at = ropePosition(rope).add(0.0, -2.0, 0.0); + entity.stopRiding(); + entity.teleportTo(at.x, at.y, at.z); + entity.setDeltaMovement(Vec3.ZERO); + entity.resetFallDistance(); + RappelEntity rider = new RappelEntity(ModEntities.RAPPEL.get(), level()); + rider.attach(this, rope); + rider.setPos(at); + level().addFreshEntity(rider); + entity.startRiding(rider, true, true); + lastRope = rope; + } + + /** + * {@code mountMobToSeats}: when the pilot climbs in, mobs standing close by take the free crew + * seats, so villagers and the like can be flown somewhere. Server only. + */ + private void boardNearbyMobs() { + AircraftDefinition def = definition(); + if (def == null || def.uav) { + return; + } + for (Mob mob : level().getEntitiesOfClass(Mob.class, getBoundingBox().inflate(3.0, 2.0, 3.0), m -> !m.isPassenger() && !m.isVehicle())) { + for (int seat = 1; seat < def.seatCount(); seat++) { + if (isSeatFree(seat)) { + pendingSeat = seat; + mob.startRiding(this, true, true); + pendingSeat = EMPTY; + break; + } + } + } + } + + // Terrain helpers + + /** True if any solid block is within {@code depth} blocks below the vehicle. */ + protected boolean isNearGround(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(0, -1, 0); + if (!level().getBlockState(pos).getCollisionShape(level(), pos).isEmpty()) { + return true; + } + } + return false; + } + + /** + * Port of {@code MCH_Lib.getBlockY}: scans a {@code size} x {@code size} column from the + * vehicle's block down {@code depth} layers (the first layer being the vehicle's own). + * + * @param solidOnly ignore blocks without collision; fluids still count when this is false + */ + public boolean hasBlockBelow(int size, int depth, boolean solidOnly) { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < depth; y++) { + for (int x = -size / 2; x <= size / 2; x++) { + for (int z = -size / 2; z <= size / 2; z++) { + pos.set(px + x, py - y, pz + z); + BlockState state = level().getBlockState(pos); + if (state.isAir()) { + continue; + } + if (!solidOnly || !state.getCollisionShape(level(), pos).isEmpty() || !state.getFluidState().isEmpty()) { + return true; + } + } + } + } + return false; + } + + /** True if the first non-air block within {@code depth} below is water. */ + protected boolean isWaterBelow(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + BlockState state = level().getBlockState(pos); + if (!state.isAir()) { + return !state.getFluidState().isEmpty(); + } + pos.move(0, -1, 0); + } + return false; + } + + /** {@code getBlockY(this, 3, -2, false)}: standing on something that is neither air nor water. */ + protected boolean isOnSolidGround() { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < 2; y++) { + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + BlockState state = level().getBlockState(pos.set(px + x, py - y, pz + z)); + if (!state.isAir()) { + return state.getFluidState().isEmpty(); + } + } + } + } + return false; + } + + /** + * Port of {@code getWaterDepth}: the share of the hit box that is in water, from 0 to 1, sampled + * in five horizontal slices. {@code FloatOffset} raises the slices, so a hull with an offset rides + * higher. The float physics only pushes up once this reaches 1, which sets the waterline. + * + *

The original drew the model {@value #LEGACY_MODEL_LIFT} above the bottom of the hit box + * (its {@code yOffset}, half the 0.7 default height); this port draws it at the bottom. The + * slices are lowered by the same amount so boats sit in the water as they did in 1.0.3. + */ + protected double waterDepth() { + if (entityData.get(DATA_SINKING)) { + return 0.0; + } + AircraftDefinition def = definition(); + float offset = (def != null ? def.floatOffset : 0.0F) - LEGACY_MODEL_LIFT; + AABB box = getBoundingBox(); + double height = box.maxY - box.minY; + double depth = 0.0; + for (int i = 0; i < 5; i++) { + double bottom = box.minY + height * i / 5.0 - 0.125 + offset; + double top = box.minY + height * (i + 1) / 5.0 - 0.125 + offset; + AABB slice = new AABB(box.minX, bottom, box.minZ, box.maxX, top, box.maxZ); + if (level().getBlockStates(slice).anyMatch(state -> state.getFluidState().is(FluidTags.WATER))) { + depth += 1.0 / 5.0; + } + } + return depth; + } + + /** {@code applyOnGroundPitch}: eases pitch towards {@code OnGroundPitch} and roll towards level. */ + protected void applyOnGroundPitch(float factor) { + AircraftDefinition def = definition(); + if (def != null) { + float ground = def.onGroundPitch; + setXRot((getXRot() - ground) * factor + ground); + } + roll *= factor; + } + + /** {@code StepHeight}: how tall a ledge the vehicle drives up without jumping. */ + @Override + public float maxUpStep() { + AircraftDefinition def = definition(); + return def != null ? def.stepHeight : 0.0F; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith(@Nullable Entity other) { + return true; + } + + @Override + public boolean isPushable() { + return false; + } + + // Weapons + + public @Nullable VehicleWeapons weapons() { + definition(); + return weapons; + } + + public void publishWeaponSelection() { + if (weapons != null && !level().isClientSide()) { + entityData.set(DATA_WEAPON_SELECTION, weapons.encodeSelection()); + } + } + + public void publishWeaponUse(int stat) { + entityData.set(DATA_WEAPON_USE, stat); + } + + public int syncedWeaponUse() { + return entityData.get(DATA_WEAPON_USE); + } + + /** + * Whether {@code set} fires along the user's view instead of the hull: the shot direction and + * the muzzle positions are worked out as if the vehicle faced where the user looks. Static + * mounts do this; see {@link GroundVehicleEntity#aimsWithUser}. + */ + public boolean aimsWithUser(WeaponSet set) { + return false; + } + + /** {@code isInfinityAmmo}: creative players never run dry, nor does anyone when the server config says so. */ + public boolean hasInfiniteAmmo(@Nullable Entity user) { + return ServerConfig.infiniteAmmo() || user instanceof Player player && player.getAbilities().instabuild; + } + + /** Where the pilot looks relative to the hull: the turret's traverse. */ + public float turretYaw() { + return animation.seatYaw(0, 1.0F); + } + + public float stealth() { + AircraftDefinition def = definition(); + return def != null ? def.stealth : 0.0F; + } + + /** The TV missile the crew is flying, if it still exists. */ + public @Nullable TvMissileEntity tvMissile() { + if (level().isClientSide()) { + return level().getEntity(entityData.get(DATA_TV_MISSILE)) instanceof TvMissileEntity m && !m.isRemoved() ? m : null; + } + return tvMissile != null && !tvMissile.isRemoved() ? tvMissile : null; + } + + public void setTvMissile(@Nullable TvMissileEntity missile) { + tvMissile = missile; + entityData.set(DATA_TV_MISSILE, missile != null ? missile.getId() : 0); + } + + /** Whether the flares are burning right now, for the crew's display. */ + public boolean isFlareActive() { + return entityData.get(DATA_FLARE); + } + + /** Someone is locking on to this vehicle; its crew hears the warning. Server only. */ + public void warnLocked() { + if (alertCount == 0 && !isDestroyed() && weapons != null && weapons.flares.isFitted()) { + alertCount = 10; + for (int seat = 0; seat < 2; seat++) { + if (occupant(seat) instanceof ServerPlayer player) { + ModSounds.playTo(player, "locked", 1.0F, 1.0F); + } + } + } + } + + // Hit boxes + + /** + * Clips a ray against the main hit box and every {@code BoundingBox}, remembering the damage + * factor of the box hit first, as the original's composite bounding box did. + */ + public Optional clipHitBoxes(Vec3 from, Vec3 to, double inflate) { + hitBoxFactor = 1.0F; + Optional best = clip(getBoundingBox().inflate(inflate), from, to); + double bestDistance = best.map(from::distanceToSqr).orElse(Double.MAX_VALUE); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + Optional hit = clip(hullBox(box).inflate(inflate), from, to); + if (hit.isPresent() && from.distanceToSqr(hit.get()) < bestDistance) { + best = hit; + bestDistance = from.distanceToSqr(hit.get()); + hitBoxFactor = box.damageFactor(); + } + } + } + return best; + } + + /** + * One {@code BoundingBox} in world space. Like the original's, it stays axis aligned: the + * centre turns with the hull, the box itself does not. + */ + private AABB hullBox(BoundingBoxDefinition box) { + Vec3 c = toWorld(box.offset()); + double hw = box.width() / 2.0; + double hh = box.height() / 2.0; + return new AABB(c.x - hw, c.y - hh, c.z - hw, c.x + hw, c.y + hh, c.z + hw); + } + + /** The whole hull: the main hit box, every {@code BoundingBox} and the generated boxes, in world space. */ + public List hullBoxes() { + AircraftDefinition def = definition(); + if (def == null || def.hull().isEmpty()) { + return List.of(getBoundingBox()); + } + List boxes = new ArrayList<>(def.hull().size() + 1); + boxes.add(getBoundingBox()); + for (BoundingBoxDefinition box : def.hull()) { + boxes.add(hullBox(box)); + } + return boxes; + } + + /** The smallest box around the whole hull. */ + private AABB hullBounds() { + AABB bounds = getBoundingBox(); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + bounds = bounds.minmax(hullBox(box)); + } + } + return bounds; + } + + /** Whether any part of this hull touches any part of {@code other}'s, grown by {@code inflate}. */ + private boolean hullsTouch(AircraftEntity other, double inflate) { + // Hulls generated from big models run to a thousand boxes, so skip ours that are nowhere near. + AABB around = other.hullBounds().inflate(inflate); + List theirs = null; + for (AABB mine : hullBoxes()) { + AABB grown = mine.inflate(inflate); + if (!grown.intersects(around)) { + continue; + } + if (theirs == null) { + theirs = other.hullBoxes(); + } + for (AABB box : theirs) { + if (grown.intersects(box)) { + return true; + } + } + } + return false; + } + + /** + * Vehicles whose hulls block this one: everything nearby except its own carrier or cargo, and + * vehicles either of the two could load onto a rack, which have to drive into each other. + */ + private List solidNeighbours(AABB area) { + return level().getEntitiesOfClass(AircraftEntity.class, area.inflate(HULL_REACH), other -> other != this && !other.isRemoved() + && other.definition() != null && !other.isPassenger() && !isPassengerOfSameVehicle(other) + && other.getVehicle() != this && getVehicle() != other && !canCarry(other) && !other.canCarry(this)); + } + + /** Whether a free rack of this vehicle would take {@code cargo}. */ + private boolean canCarry(AircraftEntity cargo) { + AircraftDefinition def = definition(); + if (def == null) { + return false; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (acceptsCargo(cargo, seat)) { + return true; + } + } + return false; + } + + /** + * Not in the original, where only the main box at a vehicle's feet collided: the whole hull + * blocks other vehicles. A move that would push this hull into another vehicle's is undone, + * sideways first and then completely, so a helicopter can also set down on a parked truck. + * Hulls that already overlapped, say after two vehicles were placed into each other, may move + * apart. + */ + @Override + public void move(MoverType type, Vec3 delta) { + if (type != MoverType.SELF || isPassenger() || definition() == null) { + super.move(type, delta); + return; + } + List near = solidNeighbours(hullBounds().expandTowards(delta)); + if (near.isEmpty()) { + super.move(type, delta); + return; + } + List overlapping = new ArrayList<>(); + for (AircraftEntity other : near) { + if (hullsTouch(other, 0.0)) { + overlapping.add(other); + } + } + Vec3 before = position(); + super.move(type, delta); + if (!entersHull(near, overlapping)) { + return; + } + Vec3 m = getDeltaMovement(); + setPos(before.x, getY(), before.z); + setDeltaMovement(0.0, m.y, 0.0); + if (entersHull(near, overlapping)) { + setPos(before); + setDeltaMovement(Vec3.ZERO); + } + } + + private boolean entersHull(List near, List overlapping) { + for (AircraftEntity other : near) { + if (!overlapping.contains(other) && hullsTouch(other, 0.0)) { + return true; + } + } + return false; + } + + /** A ray that starts inside a box hits it where it starts, as it does for vanilla entity picking. */ + private static Optional clip(AABB box, Vec3 from, Vec3 to) { + return box.contains(from) ? Optional.of(from) : box.clip(from, to); + } + + public float consumeHitBoxFactor() { + float f = hitBoxFactor; + hitBoxFactor = 1.0F; + return f; + } + + // Damage + + public boolean isDestroyed() { + return definition() != null && hp() <= 0; + } + + /** + * Port of the original's {@code attackEntityFrom}. Armour caps a hit at {@code ArmorMaxDamage}, + * scales it by {@code ArmorDamageFactor} and the struck box's factor, then subtracts + * {@code ArmorMinDamage}. A player punching an empty vehicle packs it up instead. + */ + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + float boxFactor = consumeHitBoxFactor(); + if (testFlightStart != null || isRemoved() || isInvulnerableToBase(source) || timeSinceHit > 0) { + return false; + } + if (source.is(DamageTypes.IN_FIRE) || source.is(DamageTypes.CACTUS)) { + return false; + } + boolean lava = source.is(DamageTypes.LAVA); + boolean onFire = source.is(DamageTypes.ON_FIRE); + if (lava) { + damage *= random.nextInt(8) + 2; + timeSinceHit = 2; + } + if (source.is(DamageTypeTags.IS_EXPLOSION)) { + timeSinceHit = 1; + } else if (source.getEntity() != null && hasPassenger(source.getEntity())) { + return false; + } + if (onFire) { + timeSinceHit = 10; + } + + boolean creative = false; + boolean sneaking = false; + boolean packUp = false; + if (source.getEntity() instanceof Player player) { + creative = player.getAbilities().instabuild; + sneaking = player.isShiftKeyDown(); + if (source.is(DamageTypes.PLAYER_ATTACK)) { + packUp = creative || !hasPlayerPassenger(); + } + ModSounds.playAt(this, "hit", 1.0F, 1.0F); + } else { + ModSounds.playAt(this, "helidmg", 1.0F, 0.9F + random.nextFloat() * 0.1F); + } + + if (!isDestroyed()) { + if (!packUp) { + AircraftDefinition def = definition(); + if (def != null && !lava && !onFire) { + damage = Math.min(damage, def.armorMaxDamage); + if (boxFactor <= 1.0F) { + damage *= boxFactor; + } + damage *= def.armorDamageFactor; + damage -= def.armorMinDamage; + if (damage <= 0.0F) { + return false; + } + if (boxFactor > 1.0F) { + damage *= boxFactor; + } + } + damage *= ServerConfig.vehicleDamageFactor(); + entityData.set(DATA_HP, Math.max(hp() - (int) damage, 0)); + } + setHurtDir(-getHurtDir()); + // No markHurt(): it sends the server's motion to every client, which snaps a vehicle + // its pilot is flying back a step and looks like knockback on every hit. + setHurtTime(10); + if (hp() <= 0 || packUp) { + if (!packUp) { + entityData.set(DATA_HP, 0); + destroyVehicle(); + timeSinceHit = 20; + if (isAfloat() || isHeavy() && isNearGround(3)) { + startCookOff(source.getEntity()); + } else if (source.is(DamageTypes.IN_WALL) || source.is(DamageTypes.FLY_INTO_WALL) || source.is(DamageTypes.FALL)) { + crashExplosion(); + } else { + Explosions.explode(level, getX(), getY(), getZ(), + new Explosions.Params(null, source.getEntity(), 2.0F, 2.0F, true, true, 5, false, null)); + } + } else { + // In creative the item only drops when sneaking, as with the original's defaults. + if (!creative || sneaking) { + spawnAtLocation(level, getPickResult() != null ? getPickResult() : ItemStack.EMPTY); + } + discard(); + } + } + } else if (packUp && creative) { + discard(); + } + return true; + } + + private boolean hasPlayerPassenger() { + for (Entity passenger : getPassengers()) { + if (passenger instanceof Player) { + return true; + } + } + return false; + } + + /** {@code destroyAircraft}: the wreck tumbles, burns and disappears after 30 seconds. Server only. */ + private void destroyVehicle() { + AircraftDefinition def = definition(); + despawnCount = def != null && def.targetDrone ? 50 : WRECK_TICKS; + setHovering(false); + if (def != null && def.uav) { + Entity remote = occupant(0); + if (remote != null) { + remote.stopRiding(); + } + } + for (int seat = 0; seat < 2; seat++) { + Entity crew = occupant(seat); + if (crew != null && canEject(crew)) { + eject(crew); + } + } + } + + /** {@code explosionByCrash}: a wreck hitting the ground goes up in a fireball sized by its fuel. */ + private void crashExplosion() { + if (!(level() instanceof ServerLevel level)) { + return; + } + if (isHeavy()) { + startCookOff(null); + return; + } + exploded = true; + AircraftDefinition def = definition(); + float size = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + Vec3 at = def != null && def.fuelTankPosition != null ? toWorld(def.fuelTankPosition) : position(); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, null, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + if (!isInWater()) { + Explosions.crashFireball(level, at.x, at.y, at.z, size); + } + } + + /** + * Airliners, bombers and transports ({@code MaxFuel} of {@value HEAVY_FUEL} or more): like a + * boat, they go up in three blasts rather than one fireball. + */ + private boolean isHeavy() { + AircraftDefinition def = definition(); + return def != null && def.maxFuel >= HEAVY_FUEL; + } + + /** A floating vehicle sitting on the water, such as a patrol boat or a float plane at rest. */ + private boolean isAfloat() { + AircraftDefinition def = definition(); + return def != null && def.floats && waterDepth() > 0.0; + } + + /** + * A boat destroyed on the water, or a heavy aircraft wrecked on the ground, does not go up in + * one crash fireball; its fuel and magazine cook off in three blasts over about five seconds, + * then a hull in the water sinks. Not in the original, which only had the crash explosion. + */ + private void startCookOff(@Nullable Entity cause) { + exploded = true; + cookOffBlasts = 3; + cookOffDelay = 1; + cookOffCause = cause; + } + + private void tickCookOff(ServerLevel level) { + if (cookOffBlasts <= 0 || --cookOffDelay > 0) { + return; + } + AircraftDefinition def = definition(); + int blast = 4 - cookOffBlasts--; + float fuel = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + // The fuel tank first, then whatever the magazine still holds, then the hull goes up. + float size = switch (blast) { + case 1 -> fuel * 0.6F; + case 2 -> Math.max(munitionSize(), fuel * 0.4F); + default -> fuel; + }; + size = Mth.clamp(size, 1.0F, 15.0F); + Vec3 at = cookOffPoint(def); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, cookOffCause, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + // The blasts happen at the waterline, but the fire is above it, so show the full fireball. + Explosions.crashFireball(level, at.x, at.y, at.z, size); + if (cookOffBlasts > 0) { + // Three blasts inside five seconds: about 1.5 to 2.5 seconds apart. + cookOffDelay = 30 + random.nextInt(20); + } else { + // A boat or a flying boat goes under; a heavy aircraft on land stays where it burns. + if (isInWater() || waterDepth() > 0.0) { + entityData.set(DATA_SINKING, true); + } + cookOffCause = null; + } + } + + /** + * A cooked-off wreck going under, in place of the vehicle's own physics: it settles slowly, + * stern first with the bow raised and listing to one side, drifts to a stop and comes to rest + * on the bottom. Out of the water it simply falls. + */ + private void sink() { + Vec3 m = getDeltaMovement(); + double my = isInWater() ? Math.max(m.y - 0.003, -SINK_SPEED) : m.y - 0.04; + Vec3 motion = new Vec3(m.x * 0.9, my, m.z * 0.9); + move(MoverType.SELF, motion); + setDeltaMovement(onGround() ? motion.multiply(0.5, 0.0, 0.5) : motion); + if (!onGround()) { + // Which way it lists only has to be the same on every side, so take it from the id. + float list = (getId() & 1) == 0 ? SINK_LIST : -SINK_LIST; + setXRot(Mth.approach(getXRot(), -SINK_PITCH, 0.12F)); + roll = Mth.approach(roll, list, 0.15F); + } + } + + /** + * How big the ammunition aboard goes up: each weapon's {@code Explosion} scaled by how full its + * magazine and reserve still are. Guns and empty racks add nothing. + */ + private float munitionSize() { + if (weapons == null) { + return 0.0F; + } + float size = 0.0F; + for (WeaponSet set : weapons.sets()) { + int rounds = set.ammo() + set.restAmmo(); + if (rounds > 0 && set.definition().explosion > 0) { + size += set.definition().explosion * Math.min(1.0F, (float) rounds / Math.max(set.maxAmmo(), 1)); + } + } + return size; + } + + /** A random spot on the hull: one of its hit boxes, or the middle of the body. */ + private Vec3 cookOffPoint(@Nullable AircraftDefinition def) { + if (def == null || def.boundingBoxes.isEmpty() || random.nextInt(def.boundingBoxes.size() + 1) == 0) { + return position().add(0.0, def != null ? def.bodyHeight / 2.0 : 0.5, 0.0); + } + return toWorld(def.boundingBoxes.get(random.nextInt(def.boundingBoxes.size())).offset()); + } + + /** Destroyed state each tick: the wreck spins, sets its crew alight and explodes on impact. */ + private void tickDestroyed() { + if (!isDestroyed()) { + return; + } + input.clear(); + throttle *= 0.98; + crashSpin(); + if (!(level() instanceof ServerLevel level)) { + return; + } + tickCookOff(level); + if (ticksAlive % 20 == 0) { + AircraftDefinition def = definition(); + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null && !(seat == 0 && def != null && def.uav)) { + crew.igniteForSeconds(5.0F); + } + } + } + if (despawnCount > 0 && --despawnCount <= 1) { + ejectPassengers(); + discard(); + } + } + + /** + * Not in the original: a shot down plane rolls into a nose-down spiral and a helicopter spins + * with some wobble, until it is close to the ground. Tanks and cars just stop. + */ + private void crashSpin() { + if (!(this instanceof HelicopterEntity) && !(this instanceof PlaneEntity) || isNearGround(3) || isInWater()) { + return; + } + int direction = CrashSpin.direction(getId()); + int ticks = spinTicks++; + if (this instanceof HelicopterEntity) { + setYRot(getYRot() + CrashSpin.helicopterYawRate(ticks, direction)); + setXRot(Mth.approach(getXRot(), CrashSpin.helicopterWobblePitch(ticks, getId()), 2.0F)); + roll = Mth.approach(roll, CrashSpin.helicopterWobbleRoll(ticks, getId()), 3.0F); + } else { + setYRot(getYRot() + CrashSpin.planeYawRate(ticks, direction)); + setXRot(CrashSpin.planePitch(getXRot())); + roll = AircraftRotation.wrap(roll + CrashSpin.planeRollRate(ticks, direction)); + } + } + + /** {@code onUpdate_Particle2}: smoke from a damaged hull, thicker the lower its hit points. */ + private void damageSmoke(AircraftDefinition def) { + float ratio = (float) hp() / def.maxHp; + if (ratio > 0.5F) { + return; + } + int sources = 1 + def.boundingBoxes.size(); + int pick = random.nextInt(sources); + Vec3 at = pick == 0 ? position().add(0.0, def.bodyHeight / 2.0, 0.0) : toWorld(def.boundingBoxes.get(pick - 1).offset()); + if (ratio < random.nextFloat() / 2.0F) { + level().addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, at.x, at.y, at.z, + (random.nextDouble() - 0.5) * 0.3, random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.3); + } + if (isDestroyed()) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, at.x, at.y, at.z, 0.0, 0.07, 0.0); + if (random.nextInt(3) == 0) { + level().addParticle(ParticleTypes.FLAME, at.x + random.nextGaussian() * 0.5, at.y, at.z + random.nextGaussian() * 0.5, 0.0, 0.05, 0.0); + } + wreckFire(def); + } + } + + /** + * Not in the original: the wreck keeps burning until it disappears, with a fire glow that + * lights up the smoke at night and dies down over the last few seconds. Nothing burns under + * water. Client only. + */ + private void wreckFire(AircraftDefinition def) { + wreckTicks++; + if (isInWater()) { + return; + } + float left = Math.clamp((float) (WRECK_TICKS - wreckTicks) / WRECK_FADE_TICKS, 0.15F, 1.0F); + Vec3 centre = def.enginePosition != null ? toWorld(def.enginePosition) : position().add(0.0, def.bodyHeight / 2.0, 0.0); + if (wreckTicks % 8 == 0) { + // Each glow outlives the gap to the next, so the fire never goes dark in between. + float size = Math.clamp(def.bodyWidth * 0.8F, 2.0F, 8.0F) * left; + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size, 24), true, + centre.x, centre.y, centre.z, 0.0, 0.02, 0.0); + } + if (random.nextFloat() < left) { + double spread = def.bodyWidth * 0.4; + level().addParticle(ParticleTypes.FLAME, centre.x + random.nextGaussian() * spread, centre.y, + centre.z + random.nextGaussian() * spread, 0.0, 0.04, 0.0); + } + if (random.nextInt(10) == 0) { + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 20 + random.nextInt(20)), true, centre.x, centre.y, centre.z, + (random.nextDouble() - 0.5) * 0.15, 0.1 + random.nextDouble() * 0.15, (random.nextDouble() - 0.5) * 0.15); + } + } + + /** A vehicle that is destroyed, packed up or killed spills what it carried; one that unloads keeps it. */ + @Override + public void remove(RemovalReason reason) { + if (reason.shouldDestroy() && !level().isClientSide() && !isRemoved()) { + inventory.dropAll(); + } + super.remove(reason); + } + + @Override + protected Item getDropItem() { + AircraftDefinition def = definition(); + return def != null ? ModItems.forKind(def.kind()) : Items.AIR; + } + + @Override + public @Nullable ItemStack getPickResult() { + AircraftDefinition def = definition(); + return def != null ? AircraftItem.stackOf(def) : null; + } + + // Dimensions and syncing + + @Override + public EntityDimensions getDimensions(Pose pose) { + AircraftDefinition def = definition(); + return def != null ? EntityDimensions.scalable(def.bodyWidth, def.bodyHeight) : super.getDimensions(pose); + } + + @Override + public void onSyncedDataUpdated(EntityDataAccessor accessor) { + super.onSyncedDataUpdated(accessor); + if (DATA_TYPE.equals(accessor)) { + refreshDimensions(); + definition(); + } else if (DATA_SEATS.equals(accessor) && level().isClientSide()) { + decodeSeats(entityData.get(DATA_SEATS)); + } else if (DATA_WEAPON_SELECTION.equals(accessor) && level().isClientSide() && weapons() != null) { + weapons.decodeSelection(entityData.get(DATA_WEAPON_SELECTION), ClientAccess.localPlayer()); + } + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putString("Type", typeName()); + output.putString("Texture", texture()); + output.putInt("Hp", hp()); + output.putInt("Fuel", fuel()); + output.putFloat("Roll", roll); + output.putBoolean("Hovering", isHoveringMode()); + output.putBoolean("GunnerMode", isGunnerMode()); + output.putInt("Parts", partBits()); + output.putInt("DespawnCount", despawnCount); + output.putInt("CookOff", cookOffBlasts); + output.putBoolean("Sinking", entityData.get(DATA_SINKING)); + output.putBoolean("SearchLight", entityData.get(DATA_SEARCH_LIGHT)); + if (weapons != null) { + output.putIntArray("WeaponsAmmo", weapons.saveAmmo()); + } + inventory.save(output); + if (station != null && stationReturn != null) { + output.store("Station", UUIDUtil.CODEC, station); + output.store("StationReturn", Vec3.CODEC, stationReturn); + } + ValueOutput.TypedOutputList seats = output.list("Seats", Codec.STRING); + for (int i = 0; i < seatIds.length; i++) { + Entity e = occupant(i); + seats.add(e != null ? e.getUUID().toString() : ""); + } + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + entityData.set(DATA_TYPE, input.getStringOr("Type", "")); + entityData.set(DATA_TEXTURE, input.getStringOr("Texture", "")); + entityData.set(DATA_PARTS, input.getIntOr("Parts", 0)); + AircraftDefinition def = definition(); + entityData.set(DATA_HP, input.getIntOr("Hp", def != null ? def.maxHp : 1)); + // 1.0.0 and the earlier betas had no fuel; their vehicles come back with a full tank. + entityData.set(DATA_FUEL, input.getIntOr("Fuel", def != null ? def.maxFuel : 0)); + roll = input.getFloatOr("Roll", 0.0F); + setHovering(input.getBooleanOr("Hovering", false)); + entityData.set(DATA_GUNNER_MODE, input.getBooleanOr("GunnerMode", false)); + despawnCount = input.getIntOr("DespawnCount", 0); + if (hp() <= 0 && despawnCount <= 0) { + despawnCount = WRECK_TICKS; + } + cookOffBlasts = input.getIntOr("CookOff", 0); + cookOffDelay = cookOffBlasts > 0 ? 20 : 0; + entityData.set(DATA_SINKING, input.getBooleanOr("Sinking", false)); + entityData.set(DATA_SEARCH_LIGHT, input.getBooleanOr("SearchLight", false)); + exploded |= cookOffBlasts > 0 || entityData.get(DATA_SINKING); + input.getIntArray("WeaponsAmmo").ifPresent(ammo -> { + if (weapons != null) { + weapons.loadAmmo(ammo); + } else { + savedAmmo = ammo; + } + }); + inventory.load(input); + station = input.read("Station", UUIDUtil.CODEC).orElse(null); + stationReturn = input.read("StationReturn", Vec3.CODEC).orElse(null); + savedSeats.clear(); + List seats = input.listOrEmpty("Seats", Codec.STRING).stream().toList(); + for (int i = 0; i < seats.size(); i++) { + if (!seats.get(i).isEmpty()) { + try { + savedSeats.put(UUID.fromString(seats.get(i)), i); + } catch (IllegalArgumentException ignored) { + // A hand edited save; that occupant simply takes the first free seat. + } + } + } + for (PartState part : parts) { + part.force(part.status()); + } + refreshDimensions(); + } + + /** Raw pilot input, filled in by the client controller every tick. */ + public static final class ControlInput { + public boolean throttleUp; + public boolean throttleDown; + public boolean left; + public boolean right; + public boolean brake; + /** The pilot is looking around instead of steering; planes and tanks yaw with A/D then. */ + public boolean freeLook; + + public void clear() { + throttleUp = false; + throttleDown = false; + left = false; + right = false; + brake = false; + freeLook = false; + } + + public byte write() { + return (byte) ((throttleUp ? 1 : 0) | (throttleDown ? 2 : 0) | (left ? 4 : 0) | (right ? 8 : 0) + | (brake ? 16 : 0) | (freeLook ? 32 : 0)); + } + + public void read(byte bits) { + throttleUp = (bits & 1) != 0; + throttleDown = (bits & 2) != 0; + left = (bits & 4) != 0; + right = (bits & 8) != 0; + brake = (bits & 16) != 0; + freeLook = (bits & 32) != 0; + } + } +} From a8824e9e1c88f7e65e258b3835739b0834bc30d0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:40 +0800 Subject: [PATCH 0479/1461] Add entity/AnimationState.java to the 26.1 shared sources --- .../mcheli/entity/AnimationState.java | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java new file mode 100644 index 00000000..eda5ed7b --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java @@ -0,0 +1,202 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.RackDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; + +/** + * Purely visual state that every side advances once per tick: wheels, tracks, spinning parts and + * where each crew member is looking relative to the hull. + * + *

Ported from {@code updatePartWheel}, {@code updatePartCrawlerTrack} and + * {@code onUpdate_PartRotation}. Nothing in here affects physics, so it is never synced; each + * client derives it from the synced throttle, input and crew rotations. + */ +public final class AnimationState { + /** Road wheel roll in degrees. */ + public float wheelRoll; + public float prevWheelRoll; + /** Steering from -1 (full right) to 1 (full left). */ + public float steer; + public float prevSteer; + /** Track roller angle per side, index 1 is positive X. */ + public final float[] roller = new float[2]; + public final float[] prevRoller = new float[2]; + /** Track run per side, in link spacings from 0 to 1. */ + public final float[] track = new float[2]; + public final float[] prevTrack = new float[2]; + private final float[] trackSpeed = new float[2]; + public float[] rotatingParts = new float[0]; + public float[] prevRotatingParts = new float[0]; + public float throttle; + public float prevThrottle; + /** Per seat and rack: view yaw and pitch of the occupant relative to the hull. */ + public float[] seatYaw = new float[0]; + public float[] prevSeatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public float[] prevSeatPitch = new float[0]; + + void resize(AircraftDefinition def) { + rotatingParts = new float[def.rotatingParts.size()]; + prevRotatingParts = new float[def.rotatingParts.size()]; + int seats = def.seatAndRackCount(); + seatYaw = new float[seats]; + prevSeatYaw = new float[seats]; + seatPitch = new float[seats]; + prevSeatPitch = new float[seats]; + for (int i = def.seatCount(); i < seats; i++) { + RackDefinition rack = def.rack(i); + seatYaw[i] = prevSeatYaw[i] = rack.yaw(); + seatPitch[i] = prevSeatPitch[i] = rack.pitch(); + } + } + + void tick(AircraftEntity vehicle, AircraftDefinition def) { + AircraftEntity.ControlInput input = vehicle.input(); + float visibleThrottle = vehicle.visibleThrottle(); + float reverse = vehicle.visibleReverse(); + prevThrottle = throttle; + throttle = visibleThrottle; + + tickWheels(def, input, visibleThrottle, reverse); + tickTracks(def, input, visibleThrottle, reverse); + tickRotatingParts(def, vehicle.getControllingPassenger() != null); + tickCrewAim(vehicle, def); + } + + private void tickWheels(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + prevWheelRoll = wheelRoll; + prevSteer = steer; + if (def.enableBack && reverse > 0.01F && throttle <= 0.0) { + throttle = -reverse * 15.0F; + } + if (input.left && !input.right) { + steer = Math.min(steer + 0.1F, 1.0F); + } else if (input.right && !input.left) { + steer = Math.max(steer - 0.1F, -1.0F); + } else { + steer *= 0.9F; + } + wheelRoll += (float) (throttle * def.partWheelRot); + if (wheelRoll >= 360.0F) { + wheelRoll -= 360.0F; + prevWheelRoll -= 360.0F; + } else if (wheelRoll < 0.0F) { + wheelRoll += 360.0F; + prevWheelRoll += 360.0F; + } + } + + private void tickTracks(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + System.arraycopy(roller, 0, prevRoller, 0, 2); + System.arraycopy(track, 0, prevTrack, 0, 2); + double pivot = def.pivotTurnThrottle <= 0.0F ? 1.0 : def.pivotTurnThrottle * 0.1F; + boolean left = input.left; + boolean right = input.right; + int dir = 1; + if (def.enableBack && reverse > 0.0F && throttle <= 0.0) { + throttle = -reverse * 5.0F; + // Reversing swaps which track leads a turn. + if (left != right) { + boolean t = left; + left = right; + right = t; + dir = -1; + } + } + if (left && !right) { + double t = 0.2 * dir; + trackSpeed[0] += (float) t; + trackSpeed[1] -= (float) (pivot * t); + } else if (right && !left) { + double t = 0.2 * dir; + trackSpeed[0] -= (float) (pivot * t); + trackSpeed[1] += (float) t; + } else { + float t = (float) Mth.clamp(throttle, -0.2, 0.2); + trackSpeed[0] += t; + trackSpeed[1] += t; + } + + for (int i = 0; i < 2; i++) { + trackSpeed[i] = Mth.clamp(trackSpeed[i], -0.72F, 0.72F); + roller[i] += trackSpeed[i] * def.trackRollerRot; + if (roller[i] >= 360.0F) { + roller[i] -= 360.0F; + prevRoller[i] -= 360.0F; + } else if (roller[i] < 0.0F) { + roller[i] += 360.0F; + prevRoller[i] += 360.0F; + } + track[i] -= trackSpeed[i]; + while (track[i] >= 1.0F) { + track[i]--; + prevTrack[i]--; + } + while (track[i] < 0.0F) { + track[i]++; + } + while (prevTrack[i] < 0.0F) { + prevTrack[i]++; + } + trackSpeed[i] *= 0.75F; + } + } + + private void tickRotatingParts(AircraftDefinition def, boolean piloted) { + for (int i = 0; i < rotatingParts.length && i < def.rotatingParts.size(); i++) { + RotatingPart part = def.rotatingParts.get(i); + prevRotatingParts[i] = rotatingParts[i]; + if (part.always() || piloted) { + float r = rotatingParts[i] + part.speed(); + if (r < 0.0F) { + r += 360.0F; + prevRotatingParts[i] += 360.0F; + } else if (r >= 360.0F) { + r -= 360.0F; + prevRotatingParts[i] -= 360.0F; + } + rotatingParts[i] = r; + } + } + } + + private void tickCrewAim(AircraftEntity vehicle, AircraftDefinition def) { + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + float hullRoll = vehicle.roll(); + for (int i = 0; i < seatYaw.length && i < def.seatCount(); i++) { + prevSeatYaw[i] = seatYaw[i]; + prevSeatPitch[i] = seatPitch[i]; + Entity crew = vehicle.occupant(i); + if (crew == null) { + continue; + } + float yaw = Mth.wrapDegrees(crew.getYRot() - hullYaw); + double rad = Math.toRadians(yaw); + // The hull tilt along the crew member's view direction, as in the original tank code. + float tilt = (float) (hullPitch * Math.cos(rad) + hullRoll * Math.sin(rad)); + float pitch = crew.getXRot() - tilt; + // Keep the previous value on the same side of the seam so interpolation never spins. + prevSeatYaw[i] = yaw + Mth.wrapDegrees(prevSeatYaw[i] - yaw); + seatYaw[i] = yaw; + seatPitch[i] = pitch; + } + } + + public float seatYaw(int seat, float partialTicks) { + if (seat < 0 || seat >= seatYaw.length) { + return 0.0F; + } + return prevSeatYaw[seat] + (seatYaw[seat] - prevSeatYaw[seat]) * partialTicks; + } + + public float seatPitch(int seat, float partialTicks) { + if (seat < 0 || seat >= seatPitch.length) { + return 0.0F; + } + return prevSeatPitch[seat] + (seatPitch[seat] - prevSeatPitch[seat]) * partialTicks; + } +} From a5c9e55a8e03ea170471321c1f48121c7870f058 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:41 +0800 Subject: [PATCH 0480/1461] Add entity/ChainEntity.java to the 26.1 shared sources --- .../mcheli/entity/ChainEntity.java | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java new file mode 100644 index 00000000..4aaab44d --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java @@ -0,0 +1,193 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * A tow chain, {@code MCH_EntityChain}: it hangs above the vehicle that pulls and drags the towed + * one along, letting it drift up to the chain's length away and never more than two blocks past + * that. It goes when either end does. + */ +public class ChainEntity extends Entity { + public static final int MIN_LENGTH = 3; + public static final int MAX_LENGTH = 15; + /** Ticks a chain loaded from disk waits for both ends to load before it gives up. */ + private static final int SEARCH_TICKS = 50; + + private static final EntityDataAccessor DATA_TOW = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TOWED = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + + private @Nullable UUID towId; + private @Nullable UUID towedId; + private @Nullable Entity tow; + private @Nullable Entity towed; + private int length = 4; + private int searchTicks = SEARCH_TICKS; + + public ChainEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + /** Chains {@code towed} to {@code tow}, at the distance they are apart now. Server only. */ + public static ChainEntity connect(Level level, Entity towed, Entity tow) { + ChainEntity chain = new ChainEntity(ModEntities.CHAIN.get(), level); + chain.setPos(tow.position().add(towed.position()).scale(0.5)); + chain.length = Mth.clamp((int) towed.distanceTo(tow), MIN_LENGTH, MAX_LENGTH); + chain.attach(towed, tow); + level.addFreshEntity(chain); + ModSounds.playAt(chain.tow, "chain_ct", 1.0F, 1.0F); + return chain; + } + + /** Whether {@code entity} can pull the other end: only vehicles can. */ + public static boolean canTow(Entity entity) { + return entity instanceof AircraftEntity; + } + + /** + * Only a vehicle pulls. Hooked the other way round, a container would hold a helicopter down, + * so the ends swap whichever order they were clicked in (and chains saved that way are fixed + * when they load). + */ + private void attach(Entity towed, Entity tow) { + if (!canTow(tow) && canTow(towed)) { + Entity swap = tow; + tow = towed; + towed = swap; + } + this.towed = towed; + this.tow = tow; + this.towedId = towed.getUUID(); + this.towId = tow.getUUID(); + entityData.set(DATA_TOWED, towed.getId()); + entityData.set(DATA_TOW, tow.getId()); + searchTicks = 0; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_TOW, -1); + builder.define(DATA_TOWED, -1); + } + + /** The end that pulls; on a client, as far as it has loaded. */ + public @Nullable Entity tow() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOW)) : tow; + } + + /** The end that is pulled. */ + public @Nullable Entity towed() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOWED)) : towed; + } + + /** Whether {@code entity} is either end of this chain. */ + public boolean holds(Entity entity) { + return entity == tow() || entity == towed(); + } + + /** Takes the chain off, with the sound of it coming loose. Server only. */ + public void disconnect() { + ModSounds.playAt(this, "chain_ct", 1.0F, 1.0F); + discard(); + } + + /** The chain on {@code entity}, at either end, if there is one within reach. */ + public static @Nullable ChainEntity on(Entity entity) { + for (ChainEntity chain : entity.level().getEntitiesOfClass(ChainEntity.class, entity.getBoundingBox().inflate(25.0))) { + if (chain.holds(entity)) { + return chain; + } + } + return null; + } + + @Override + public void tick() { + super.tick(); + if (!(level() instanceof ServerLevel server)) { + return; + } + if (searchTicks > 0) { + searchTicks--; + Entity foundTowed = towedId != null ? server.getEntity(towedId) : null; + Entity foundTow = towId != null ? server.getEntity(towId) : null; + if (foundTowed != null && foundTow != null) { + attach(foundTowed, foundTow); + } + return; + } + if (tow == null || towed == null || tow.isRemoved() || towed.isRemoved() || tow == towed) { + disconnect(); + return; + } + towed.resetFallDistance(); + setPos(tow.getX(), tow.getY() + 2.0, tow.getZ()); + pull(tow, towed); + } + + /** {@code updateTowingEntityPosRot}: turns the towed end to face along the chain and drags it in. */ + private void pull(Entity tow, Entity towed) { + Vec3 d = towed.position().subtract(tow.position()); + double dist = d.length(); + if (dist <= length) { + return; + } + towed.setYRot((float) Math.toDegrees(Math.atan2(d.z, d.x)) + 90.0F); + double slack = dist - length; + towed.setDeltaMovement(towed.getDeltaMovement().subtract(d.scale(0.001 / slack))); + double max = length + 2.0; + if (dist > max) { + Vec3 at = tow.position().add(d.scale(max / dist)); + towed.setPos(at.x, at.y, at.z); + } + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + towId = input.read("Tow", UUIDUtil.CODEC).orElse(null); + towedId = input.read("Towed", UUIDUtil.CODEC).orElse(null); + length = Mth.clamp(input.getIntOr("Length", 4), MIN_LENGTH, MAX_LENGTH); + searchTicks = SEARCH_TICKS; + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + if (towId != null && towedId != null) { + output.store("Tow", UUIDUtil.CODEC, towId); + output.store("Towed", UUIDUtil.CODEC, towedId); + } + output.putInt("Length", length); + } +} From 6507fe29f108954193391cf354f1919c3cbf2b25 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:41 +0800 Subject: [PATCH 0481/1461] Add entity/ClientAccess.java to the 26.1 shared sources --- .../mcheli/entity/ClientAccess.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java new file mode 100644 index 00000000..28f14851 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * The few client facts common code needs, such as who the local player is. The client installs + * the real supplier at startup; a dedicated server keeps the empty default and never loads a + * client class through here. + */ +public final class ClientAccess { + private static Supplier<@Nullable Entity> localPlayer = () -> null; + private static PackReceiver packs = new PackReceiver() { + @Override + public void onPackList(PackListPacket packet) { + } + + @Override + public void onPackData(PackDataPacket packet) { + } + + @Override + public void onPackSound(PackSoundPacket packet) { + } + }; + + private ClientAccess() { + } + + public static void install(Supplier<@Nullable Entity> player, PackReceiver receiver) { + localPlayer = player; + packs = receiver; + } + + public static @Nullable Entity localPlayer() { + return localPlayer.get(); + } + + /** Where the server's content pack packets go. Called on the client's main thread. */ + public static PackReceiver packs() { + return packs; + } + + /** The client half of the content pack sync. */ + public interface PackReceiver { + void onPackList(PackListPacket packet); + + void onPackData(PackDataPacket packet); + + void onPackSound(PackSoundPacket packet); + } +} From 05da1e51b759b3c3db126bcc531c919a6325f55b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:41 +0800 Subject: [PATCH 0482/1461] Add entity/ContainerEntity.java to the 26.1 shared sources --- .../mcheli/entity/ContainerEntity.java | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java new file mode 100644 index 00000000..76880875 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java @@ -0,0 +1,152 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A cargo container, {@code MCH_EntityContainer}: a crate with a large chest's worth of room that + * sits on the ground, floats, can be pushed around, and rides the racks of the vehicles that carry + * {@code container}. Dropped from a rack high enough it comes down under a parachute. + */ +public class ContainerEntity extends Entity { + /** What racks call it in their {@code AddRack} lines. */ + public static final String CARGO_NAME = "container"; + private static final int SLOTS = 54; + /** Punches add up; the original broke it past 40 with 20 per point of damage. */ + private static final int BREAK_DAMAGE = 40; + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private final SimpleContainer items = new SimpleContainer(SLOTS); + private int damageTaken; + + public ContainerEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide() || isPassenger()) { + return; + } + if (damageTaken > 0) { + damageTaken--; + } + Vec3 m = getDeltaMovement(); + double my = m.y; + if (isInWater() || isInLava()) { + // Bobs up to the surface rather than sinking, as the original's boat physics did. + my = (my < 0.0 ? my / 2.0 : my) + (isEyeInFluid(FluidTags.WATER) ? 0.04 : 0.007); + } else { + my -= 0.04; + } + double mx = m.x; + double mz = m.z; + if (onGround()) { + mx *= 0.9; + mz *= 0.9; + } + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand, Vec3 location) { + if (player.getItemInHand(hand).getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (player.isSecondaryUseActive()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer serverPlayer) { + serverPlayer.openMenu(new SimpleMenuProvider((id, inventory, p) -> ChestMenu.sixRows(id, inventory, items), + Component.translatable("entity.mcheli.container"))); + } + return InteractionResult.SUCCESS; + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved() || !(source.getEntity() instanceof Player player)) { + // Only players take it apart; explosions and stray rounds leave the cargo alone. + return false; + } + ModSounds.playAt(this, "hit", 1.0F, 1.3F); + damageTaken += (int) (damage * 20.0F); + boolean creative = player.getAbilities().instabuild; + if (creative || damageTaken > BREAK_DAMAGE) { + if (!creative) { + spawnAtLocation(level, ModItems.CONTAINER.get()); + } + Containers.dropContents(level, this, items); + discard(); + } + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean isPushable() { + return !isPassenger(); + } + + @Override + public boolean canBeCollidedWith(@Nullable Entity other) { + return !isPassenger(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.CONTAINER.get()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + items.clearContent(); + ContainerHelper.loadAllItems(input, items.getItems()); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + ContainerHelper.saveAllItems(output, items.getItems(), false); + } +} From c7beeccfc1d53f5fe60378e7936bf1214ff2a1d9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:42 +0800 Subject: [PATCH 0483/1461] Add entity/EngineSoundSource.java to the 26.1 shared sources --- .../mcheli/entity/EngineSoundSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java new file mode 100644 index 00000000..6b25a388 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.entity; + +/** + * A vehicle with an engine loop. The client sound player polls these values every tick; the + * definition's {@code SoundVolume} and {@code SoundPitch} are applied on top. + */ +public interface EngineSoundSource { + /** Loudness before the definition's multiplier. Zero silences the loop. */ + float engineVolume(); + + /** Pitch before the definition's multiplier. */ + float enginePitch(); +} From 80e21bd573a7eb3a3688bbf58e9cf7ef99f820f3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:42 +0800 Subject: [PATCH 0484/1461] Add entity/GltdEntity.java to the 26.1 shared sources --- .../jnhprojects/mcheli/entity/GltdEntity.java | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java new file mode 100644 index 00000000..aa13dba7 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java @@ -0,0 +1,213 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.weapon.CasStrike; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The ground laser target designator, {@code MCH_EntityGLTD}: a tripod a player sits behind. Its + * sight looks up to 70 degrees either side of where it was set up, zooms and has night vision, + * and marking a spot calls in an A-10 for three gun runs over it. + */ +public class GltdEntity extends Entity { + /** How far the sight turns from the tripod's heading, either way, and up or down. */ + public static final float VIEW_LIMIT = 70.0F; + /** The CAS weapon the A-10s fire; the original hard coded it too. */ + private static final String WEAPON = "a10gau8"; + /** Added to the weapon's own {@code Delay} between two calls. */ + private static final int EXTRA_WAIT = 150; + private static final double SIGHT_RANGE = 300.0; + + private static final EntityDataAccessor DATA_WAIT = SynchedEntityData.defineId(GltdEntity.class, EntityDataSerializers.INT); + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private final CasStrike strike = new CasStrike(WEAPON); + private boolean nightVision; + + public GltdEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WAIT, 0); + } + + /** Ticks until the next A-10 can be called; the sight shows it. */ + public int waitTicks() { + return entityData.get(DATA_WAIT); + } + + /** The eye of the sight, a little in front of and above the tripod. */ + public Vec3 sightPosition(float partialTicks) { + float yaw = getYRot(partialTicks) * Mth.DEG_TO_RAD; + Vec3 pos = getPosition(partialTicks); + return new Vec3(pos.x - Mth.sin(yaw) * 0.6, pos.y + 0.7, pos.z + Mth.cos(yaw) * 0.6); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.04, 0.0); + if (onGround()) { + m = m.scale(0.5); + } + move(MoverType.SELF, m); + setDeltaMovement(m.x * 0.99, m.y * 0.95, m.z * 0.99); + + if (waitTicks() > 0) { + entityData.set(DATA_WAIT, waitTicks() - 1); + } + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (def != null && level() instanceof ServerLevel server) { + strike.tick(server, def); + } + Entity viewer = getFirstPassenger(); + if (viewer == null) { + nightVision = false; + } else if (nightVision) { + NightVision.topUp(viewer); + } + } + + /** + * {@code useCurrentWeapon}: marks the block {@code player} looks at through the sight and calls + * the A-10 on it. Server only. + * + * @return whether the strike was called; the sight may be waiting, or see nothing in range + */ + public boolean callStrike(ServerPlayer player) { + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (getFirstPassenger() != player || waitTicks() > 0 || def == null || !(level() instanceof ServerLevel server)) { + return false; + } + Vec3 from = sightPosition(1.0F); + Vec3 to = from.add(player.getLookAngle().scale(SIGHT_RANGE)); + BlockHitResult hit = server.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + strike.start(hit.getLocation(), player.getYRot(), this, player); + entityData.set(DATA_WAIT, def.delay + EXTRA_WAIT); + ModSounds.playAt(server, getX(), getY(), getZ(), "gltd", 0.5F, 1.0F); + if (def.sound != null) { + ModSounds.playTo(player, def.sound, 1.0F, 1.0F); + } + return true; + } + + public void setNightVision(Player player, boolean on) { + if (getFirstPassenger() != player) { + return; + } + nightVision = on; + if (!on) { + NightVision.end(player); + } + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand, Vec3 location) { + if (player.isSecondaryUseActive() || isVehicle()) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + player.setYRot(getYRot()); + player.setXRot(0.0F); + player.startRiding(this); + } + return InteractionResult.SUCCESS; + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (!level().isClientSide() && nightVision) { + NightVision.end(passenger); + nightVision = false; + } + } + + /** The viewer sits behind the tripod. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, -0.5).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + /** The original clamped the viewer's head the same way every tick. */ + @Override + public void onPassengerTurned(Entity passenger) { + float yaw = getYRot() + Mth.clamp(Mth.wrapDegrees(passenger.getYRot() - getYRot()), -VIEW_LIMIT, VIEW_LIMIT); + passenger.setYRot(yaw); + passenger.setYHeadRot(yaw); + passenger.setXRot(Mth.clamp(passenger.getXRot(), -VIEW_LIMIT, VIEW_LIMIT)); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved()) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + if (!creative) { + spawnAtLocation(level, ModItems.GLTD.get()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), "hit", 1.0F, 1.0F); + discard(); + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.GLTD.get()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } +} From c2492dd9736ead9404d1a0c3f4f178c489f851a1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:43 +0800 Subject: [PATCH 0485/1461] Add entity/GroundVehicleEntity.java to the 26.1 shared sources --- .../mcheli/entity/GroundVehicleEntity.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java new file mode 100644 index 00000000..30290141 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java @@ -0,0 +1,130 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Static guns and launchers, ported from {@code MCH_EntityVehicle}. + * + *

The mount itself does not turn; the gunner looks around and the {@code AddPart} tree turns + * with the view. {@code CanMove} lets W/S push the mount and A/D turn it, for the few that set it. + */ +public class GroundVehicleEntity extends AircraftEntity implements EngineSoundSource { + private static final double MIN_SPEED = 0.07; + private double currentSpeed = MIN_SPEED; + + public GroundVehicleEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable VehicleDefinition lookup(String name) { + return MCHeli.content().vehicle(name); + } + + @Override + public @Nullable VehicleDefinition definition() { + return (VehicleDefinition) super.definition(); + } + + /** + * {@code MCH_EntityVehicle.useCurrentWeapon}: the mount stays put while its {@code AddPart} + * tree turns with the gunner's view, so a weapon fires where the gunner looks. Only weapons + * with both yaw limits set aim like turrets instead. The original checked the first + * {@code AddWeapon} line of that weapon, and so does this. + */ + @Override + public boolean aimsWithUser(WeaponSet set) { + WeaponMount mount = set.mounts().getFirst(); + return mount.minYaw() == 0.0F || mount.maxYaw() == 0.0F; + } + + @Override + protected void simulate() { + VehicleDefinition def = definition(); + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + + if (getControllingPassenger() == null) { + throttle = Math.max(throttle - 0.00125, 0.0); + } else if (def.canMove) { + double x = 0.0; + double z = 0.0; + double yaw = Math.toRadians(getYRot()); + if (input.throttleUp) { + x += Math.sin(yaw); + z += Math.cos(yaw); + } + if (input.throttleDown) { + x -= Math.sin(yaw); + z -= Math.cos(yaw); + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + double d = Math.sqrt(x * x + z * z); + if (d > 1.0E-6) { + mx -= x / d * 0.03; + mz += z / d * 0.03; + } + } + if (isInWater()) { + setXRot(getXRot() * 0.9F); + } + + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public float engineVolume() { + return visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return visibleThrottle() * 0.5F; + } +} From 91af8444fa73632e14d01fcb0f4444373f590fc5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:43 +0800 Subject: [PATCH 0486/1461] Add entity/HelicopterEntity.java to the 26.1 shared sources --- .../mcheli/entity/HelicopterEntity.java | 553 ++++++++++++++++++ 1 file changed, 553 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java new file mode 100644 index 00000000..957bf9ef --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java @@ -0,0 +1,553 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A helicopter, ported from {@code MCH_EntityHeli}. + * + *

The flight model is the original one: throttle produces lift that weakens as the aircraft + * tilts, and pitching the nose down converts throttle into forward speed through a cubic curve. + * Hover mode holds full throttle, levels the aircraft and lets W/A/S/D slide it around instead. + * + *

Two deliberate differences from 1.7.10: + *

    + *
  • The original ran roll damping once per rendered frame, so it depended on frame rate. It + * now runs once per tick with constants tuned to match the feel at 60 FPS.
  • + *
  • Blades fold and unfold one after another at the same pace on both sides, from the + * synced fold state, instead of each client stepping its own copy.
  • + *
+ */ +public class HelicopterEntity extends AircraftEntity implements EngineSoundSource { + /** + * Rotation per mouse count. The original used {@code 0.06 * partialTicks} with the partial tick + * low-pass filtered to roughly 0.5. + */ + private static final float LOOK_SCALE = 0.03F; + /** Vanilla turns the view 0.15 degrees per mouse count, so this recovers the raw count. */ + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final float ROLL_DAMPING = 0.06F; + private static final float ROLL_DAMPING_HOVER = 0.1F; + private static final float ROLL_RATE = 1.8F; + private static final double MIN_SPEED = 0.07; + + private static final EntityDataAccessor DATA_FOLD = SynchedEntityData.defineId(HelicopterEntity.class, EntityDataSerializers.BYTE); + // DATA_FOLD values, as the original numbered them. + private static final byte FOLDED = 0; + private static final byte FOLDING = 1; + private static final byte UNFOLDED = 2; + private static final byte UNFOLDING = 3; + /** {@code MCH_Blade}: degrees a blade swings per tick while folding. */ + private static final float FOLD_SPEED = 2.0F; + private static final int FOLD_COOLDOWN = 40; + + private double currentSpeed = MIN_SPEED; + private float rotorAngle; + public float rotorAngleO; + /** Per rotor, per blade: where each blade points while they fold, and whether that rotor is folded. */ + private float[][] blades = new float[0][]; + private float[][] bladesO = new float[0][]; + private boolean[] rotorFolded = new boolean[0]; + private int foldCooldown; + private byte lastFold = -1; + + public HelicopterEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable HelicopterDefinition lookup(String name) { + return MCHeli.content().helicopter(name); + } + + @Override + public @Nullable HelicopterDefinition definition() { + return (HelicopterDefinition) super.definition(); + } + + @Override + public boolean steersWithMouse() { + return true; + } + + @Override + public float engineVolume() { + HelicopterDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return 0.2F + visibleThrottle() * 0.2F; + } + + /** Hovering or slow helicopters drop their gear sooner than ones passing over at speed. */ + @Override + protected float autoLowerGearThrottle() { + HelicopterDefinition def = definition(); + double dx = getX() - xo; + double dy = getY() - yo; + double dz = getZ() - zo; + float slow = def != null ? def.speed / 3.5F : 0.0F; + return dx * dx + dy * dy + dz * dz <= slow ? 0.8F : 0.3F; + } + + public float rotorAngle(float partialTicks) { + float prev = rotorAngleO; + if (rotorAngle - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - rotorAngle < -180.0F) { + prev += 360.0F; + } + return prev + (rotorAngle - prev) * partialTicks; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_FOLD, UNFOLDED); + } + + // Blade folding + + private byte foldState() { + return entityData.get(DATA_FOLD); + } + + private boolean hasFoldingRotors() { + HelicopterDefinition def = definition(); + return def != null && def.foldableBlades && !def.rotors.isEmpty(); + } + + /** {@code isFoldBlades}: folded, or on the way. */ + public boolean isFoldedBlades() { + return hasFoldingRotors() && foldState() == FOLDED; + } + + /** {@code canUseBlades}: the blades are out and none is still swinging into place. */ + public boolean canUseBlades() { + if (foldState() != UNFOLDED) { + return false; + } + for (int r = 0; r < rotorFolded.length; r++) { + if (rotorFolded[r]) { + return false; + } + } + return true; + } + + /** {@code canSwitchFoldBlades}: with the engine stopped and the last switch finished. */ + private boolean canSwitchFoldBlades() { + byte state = foldState(); + return hasFoldingRotors() && visibleThrottle() <= 0.01F && foldCooldown == 0 && (state == UNFOLDED || state == FOLDED); + } + + /** The fold key folds the blades of helicopters that can, and works the hatches otherwise. */ + @Override + public void toggleFold() { + if (canSwitchFoldBlades()) { + entityData.set(DATA_FOLD, foldState() == FOLDED ? UNFOLDING : FOLDING); + foldCooldown = FOLD_COOLDOWN; + return; + } + super.toggleFold(); + } + + /** + * {@code onUpdate_Rotor} with {@code MCH_Rotor.update} and {@code MCH_Blade}: spinning blades + * follow the rotor; folding ones swing one by one onto the tail, each a few degrees further + * than the one before, and swing out again until they meet the spinning rotor's angle. + */ + private void tickBlades(HelicopterDefinition def) { + int rotors = def.rotors.size(); + if (blades.length != rotors) { + blades = new float[rotors][]; + bladesO = new float[rotors][]; + rotorFolded = new boolean[rotors]; + for (int r = 0; r < rotors; r++) { + int count = Math.max(def.rotors.get(r).blades(), 1); + blades[r] = new float[count]; + bladesO[r] = new float[count]; + } + if (foldState() == FOLDED || foldState() == FOLDING) { + forceFold(def); + } + } + byte state = foldState(); + if (state != lastFold) { + lastFold = state; + foldCooldown = FOLD_COOLDOWN; + } else if (foldCooldown > 0) { + foldCooldown--; + } + boolean target = state == FOLDING || state == FOLDED; + boolean settled = true; + for (int r = 0; r < rotors; r++) { + RotorDefinition rotor = def.rotors.get(r); + boolean wanted = target && rotor.foldable(); + boolean done = true; + for (int b = 0; b < blades[r].length; b++) { + float base = b * Math.max(rotor.bladeStep(), 1); + float fold = 5.0F + b * 3.0F; + bladesO[r][b] = blades[r][b]; + if (!rotorFolded[r]) { + if (!wanted) { + blades[r][b] = wrap(rotorAngle + base); + done = false; + } else if (!foldBlade(r, b, fold)) { + done = false; + } + } else if (wanted) { + done = false; + } else if (!unfoldBlade(r, b, base)) { + done = false; + } + } + if (done) { + rotorFolded[r] = wanted; + } + if (rotorFolded[r] != wanted) { + settled = false; + } + } + if (settled && !level().isClientSide()) { + if (state == FOLDING) { + entityData.set(DATA_FOLD, FOLDED); + } else if (state == UNFOLDING) { + entityData.set(DATA_FOLD, UNFOLDED); + } + } + } + + /** {@code MCH_Blade.folding}: towards the nearer side of the tail. */ + private boolean foldBlade(int r, int b, float fold) { + float angle = blades[r][b]; + if (angle > fold && angle < 360.0F - fold) { + blades[r][b] = wrap(angle < 180.0F ? angle - FOLD_SPEED : angle + FOLD_SPEED); + return false; + } + return true; + } + + /** {@code MCH_Blade.unfolding}: back round until the blade meets its place on the turning rotor. */ + private boolean unfoldBlade(int r, int b, float base) { + float target = wrap(rotorAngle + base); + float before = blades[r][b]; + if (target <= 180.0F) { + blades[r][b] = wrap(before + FOLD_SPEED); + if (blades[r][b] >= target && before <= target) { + blades[r][b] = target; + return true; + } + } else { + blades[r][b] = wrap(before - FOLD_SPEED); + if (blades[r][b] <= target && before >= target) { + blades[r][b] = target; + return true; + } + } + return false; + } + + /** {@code forceFold}: a helicopter loaded with its blades folded starts with them folded. */ + private void forceFold(HelicopterDefinition def) { + for (int r = 0; r < blades.length; r++) { + if (!def.rotors.get(r).foldable()) { + continue; + } + for (int b = 0; b < blades[r].length; b++) { + float fold = 5.0F + b * 3.0F; + float angle = wrap(rotorAngle + b * Math.max(def.rotors.get(r).bladeStep(), 1)); + if (angle > fold && angle < 360.0F - fold) { + angle = angle < 180.0F ? fold : 360.0F - fold; + } + blades[r][b] = angle; + bladesO[r][b] = angle; + } + rotorFolded[r] = true; + } + } + + private static float wrap(float degrees) { + float d = degrees % 360.0F; + return d < 0.0F ? d + 360.0F : d; + } + + /** Where blade {@code blade} of rotor {@code rotor} points, for drawing. */ + public float bladeAngle(int rotor, int blade, int step, float partialTicks) { + if (rotor >= blades.length || blade >= blades[rotor].length) { + return rotorAngle(partialTicks) + blade * step; + } + float prev = bladesO[rotor][blade]; + float now = blades[rotor][blade]; + if (now - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - now < -180.0F) { + prev += 360.0F; + } + return prev + (now - prev) * partialTicks; + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + super.addAdditionalSaveData(output); + output.putBoolean("FoldBlade", foldState() == FOLDED || foldState() == FOLDING); + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + super.readAdditionalSaveData(input); + entityData.set(DATA_FOLD, input.getBooleanOr("FoldBlade", false) ? FOLDED : UNFOLDED); + } + + /** Pilot toggles hover mode. Only allowed while fairly level and not in gunner mode, as in the original. */ + public void toggleHovering() { + if (isHoveringMode()) { + setHovering(false); + } else if (!isGunnerMode() && isLevelEnough() && canUseBlades()) { + setHovering(true); + } + } + + private boolean isLevelEnough() { + return Math.abs(Mth.wrapDegrees(roll())) < 40.0F && Math.abs(Mth.wrapDegrees(getXRot())) < 40.0F; + } + + @Override + public boolean canSwitchGunnerMode() { + return super.canSwitchGunnerMode() && isLevelEnough() && canUseBlades(); + } + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + HelicopterDefinition def = definition(); + if (def == null) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + + float yaw = 0.0F; + float pitch = 0.0F; + float rollDelta = 0.0F; + if (!isNearGround(2)) { + yaw = clamp(x, 40.0F * def.mobilityYaw) * LOOK_SCALE; + } + // Standing on its skids or wheels a helicopter stays level; the mouse only tilts it in the air. + if (!isHovering() && !onGround()) { + pitch = clamp(y, 40.0F * def.mobilityPitch) * LOOK_SCALE; + rollDelta = clamp(x, 40.0F * def.mobilityRoll) * LOOK_SCALE; + } + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + // Keep yaw continuous so the pilot's own yaw never jumps across the +-180 seam. + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + @Override + protected void simulate() { + HelicopterDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + + updateThrottle(def, piloted); + updateAttitude(def); + updateMotion(def); + } + + /** {@code onUpdate_Control} and the hover/non-hover branches. */ + private void updateThrottle(HelicopterDefinition def, boolean piloted) { + if (piloted) { + Vec3 motion = getDeltaMovement(); + if (!canUseBlades()) { + // Folded blades give no lift; on the ground A/D taxi the helicopter round slowly. + throttle = Math.max(0.0, throttle - 0.02); + if (onGround() && isFoldedBlades() && !isDestroyed()) { + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + } + return; + } + if (!isHovering()) { + if (input.throttleUp) { + throttle = Math.min(1.0, throttle + 0.02 * def.throttleUpDown); + } else if (input.throttleDown) { + throttle = Math.max(0.0, throttle - (1.0 / 70.0) * def.throttleUpDown); + } + motion = strafe(motion, false, input.left, input.right, 0.02 * def.speed); + } else { + throttle = Math.min(1.0, throttle + 1.0 / 30.0); + motion = strafe(motion, input.throttleUp, input.left, input.right, 0.01 * def.speed); + if (input.throttleDown) { + motion = push(motion, getYRot() - 180.0F, 0.01 * def.speed); + } + } + setDeltaMovement(motion); + } else { + throttle = Math.max(0.0, throttle - 0.00125); + if (isHoveringMode() && !level().isClientSide()) { + setHovering(false); + } + } + } + + private Vec3 strafe(Vec3 motion, boolean forward, boolean left, boolean right, double amount) { + if (forward) { + motion = push(motion, getYRot(), amount); + } + if (left && !right) { + motion = push(motion, getYRot() - 90.0F, amount); + } else if (right && !left) { + motion = push(motion, getYRot() + 90.0F, amount); + } + return motion; + } + + /** Adds {@code amount} of horizontal motion towards {@code yaw}, Minecraft style (0 = south). */ + private static Vec3 push(Vec3 motion, float yaw, double amount) { + double rad = Math.toRadians(yaw); + return motion.add(-Math.sin(rad) * amount, 0.0, Math.cos(rad) * amount); + } + + /** Per-tick version of {@code onUpdateAngles}: roll levels out, A/D bank in the air. */ + private void updateAttitude(HelicopterDefinition def) { + float damping = 1.0F - (isHovering() ? ROLL_DAMPING_HOVER : ROLL_DAMPING); + if (Math.abs(roll) > 0.1F && Math.abs(roll) < 65.0F) { + roll *= damping; + } + if (getControllingPassenger() != null && !isNearGround(3)) { + if (input.left && !input.right) { + roll -= ROLL_RATE; + } else if (input.right && !input.left) { + roll += ROLL_RATE; + } + } + if (isHovering() && Math.abs(getXRot()) < 70.0F) { + setXRot(getXRot() * 0.95F); + } + if (def.limitRotation) { + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + /** {@code onUpdate_Server}: gravity, lift, forward thrust, speed limit and ground friction. */ + private void updateMotion(HelicopterDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + float groundPitch = def.onGroundPitch; + + if (!isHovering()) { + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + double yawRad = Math.toRadians(getYRot()); + float pitch = getXRot(); + if (isNearGround(3)) { + pitch -= groundPitch; + } + double thrust = 0.1 * currentSpeed * (pitch * pitch * pitch / 30000.0F) * throttle; + mx += Math.sin(yawRad) * -thrust; + mz += Math.cos(yawRad) * thrust; + + double tilt = (Math.abs(getXRot()) + Math.abs(roll)) * 0.6; + double lift = tilt <= 50.0 ? 1.0 - tilt / 50.0 : 0.0; + my += (lift * 0.025 + 0.03) * throttle; + } else { + levelOut(groundPitch); + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my *= 0.7; + } + my += 0.007; + } + } + } else { + if (random.nextInt(50) == 0) { + mx += (random.nextDouble() - 0.5) / 30.0; + } + if (random.nextInt(50) == 0) { + my += (random.nextDouble() - 0.5) / 50.0; + } + if (random.nextInt(50) == 0) { + mz += (random.nextDouble() - 0.5) / 30.0; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + double limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + levelOut(groundPitch); + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + private void levelOut(float groundPitch) { + if (Math.abs(getXRot()) < 40.0F) { + setXRot((getXRot() - groundPitch) * 0.9F + groundPitch); + } + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + } + + @Override + protected void tickAnimation() { + HelicopterDefinition def = definition(); + rotorAngleO = rotorAngle; + float spool = 1.0F - visibleThrottle(); + rotorAngle = (rotorAngle + (1.0F - spool * spool * spool) * def.rotorSpeed) % 360.0F; + tickBlades(def); + } +} From 9f377d84bd4fe28f2823b15e6b0cf787101a9045 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:44 +0800 Subject: [PATCH 0487/1461] Add entity/NightVision.java to the 26.1 shared sources --- .../mcheli/entity/NightVision.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java new file mode 100644 index 00000000..f52c4227 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; + +/** + * Night vision through a sight, {@code MCH_Camera.updateViewer}: a short effect kept topped up while + * the viewer has it switched on, and taken away again when they switch it off or leave. + */ +public final class NightVision { + private static final int TICKS = 300; + + private NightVision() { + } + + /** Called every tick for each viewer. It never runs below ten seconds, where vanilla starts to flicker the view. */ + public static void topUp(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current == null || current.getDuration() < TICKS - 40) { + living.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, TICKS, 0, true, false, false)); + } + } + } + + /** Removes ours, leaving a longer one from a potion alone. */ + public static void end(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current != null && current.getDuration() <= TICKS) { + living.removeEffect(MobEffects.NIGHT_VISION); + } + } + } +} From 73ca7d6dd3b2e678976c28d592458665e0b5892d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:44 +0800 Subject: [PATCH 0488/1461] Add entity/ParachuteEntity.java to the 26.1 shared sources --- .../mcheli/entity/ParachuteEntity.java | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java new file mode 100644 index 00000000..f7a7db39 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java @@ -0,0 +1,188 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A parachute with someone hanging under it, {@code MCH_EntityParachute}. It sinks at about two + * blocks a second, the rider steers it with the forward key, and it folds away a moment after it + * touches the ground or the water. + */ +public class ParachuteEntity extends Entity { + /** What the parachute looks like and how it opens, the original's model type. */ + public enum Kind { + /** From the parachute item: open from the start. */ + ITEM, + /** An ejection seat: the rider falls in the seat and the canopy opens once it drops. */ + EJECTION_SEAT, + /** Troops dropped from a rack or a cargo door: open from the start, no seat. */ + DROP + } + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(ParachuteEntity.class, EntityDataSerializers.INT); + private static final double MAX_SPEED = 0.35; + private static final double MIN_STEER = 0.07; + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private double steer = MIN_STEER; + private int onGroundCount; + /** Who boards on the next server tick; boarding in the same tick as the spawn can be lost. */ + private @Nullable Entity user; + + public ParachuteEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Opens a parachute above {@code user}, carrying over their speed, and puts them under it. + * Server only. + */ + public static ParachuteEntity open(Level level, Entity user, Kind kind) { + ParachuteEntity parachute = new ParachuteEntity(ModEntities.PARACHUTE.get(), level); + double lift = kind == Kind.ITEM ? 3.5 : 0.0; + parachute.setPos(user.getX(), user.getY() + lift, user.getZ()); + parachute.setYRot(user.getYRot()); + parachute.setDeltaMovement(user.getDeltaMovement()); + parachute.entityData.set(DATA_KIND, kind.ordinal()); + parachute.user = user; + user.resetFallDistance(); + level.addFreshEntity(parachute); + return parachute; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, Kind.ITEM.ordinal()); + } + + public Kind kind() { + Kind[] kinds = Kind.values(); + int i = entityData.get(DATA_KIND); + return i >= 0 && i < kinds.length ? kinds[i] : Kind.ITEM; + } + + /** An ejection seat only opens its canopy once it has started to fall. */ + public boolean isOpen() { + return kind() != Kind.EJECTION_SEAT || getDeltaMovement().y < -0.1; + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide()) { + return; + } + if (user != null) { + if (!user.isPassenger() && user.isAlive()) { + user.startRiding(this, true, true); + setYRot(user.getYRot()); + } + user = null; + } + Entity rider = getFirstPassenger(); + if (rider == null && tickCount > 20 || isInWater()) { + ejectPassengers(); + discard(); + return; + } + if (rider != null) { + rider.resetFallDistance(); + } + simulate(rider); + } + + /** {@code onUpdateServer}. */ + private void simulate(@Nullable Entity rider) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevSpeed = Math.sqrt(mx * mx + mz * mz); + boolean seatLaunch = kind() == Kind.EJECTION_SEAT && tickCount < 20; + my -= onGround() || seatLaunch ? 0.01 : 0.03; + + if (isOpen()) { + if (rider instanceof ServerPlayer player && player.getLastClientInput().forward()) { + double yaw = Math.toRadians(rider.getYRot()); + mx += -Math.sin(yaw) * steer * 0.05; + mz += Math.cos(yaw) * steer * 0.05; + } + double speed = Math.sqrt(mx * mx + mz * mz); + if (speed > MAX_SPEED) { + mx *= MAX_SPEED / speed; + mz *= MAX_SPEED / speed; + speed = MAX_SPEED; + } + // Holding course builds up speed; letting go settles back to a slow drift. + if (speed > prevSpeed) { + steer = Math.min(steer + (MAX_SPEED - steer) / 35.0, MAX_SPEED); + } else { + steer = Math.max(steer - (steer - MIN_STEER) / 35.0, MIN_STEER); + } + } + + if (onGround() && ++onGroundCount > 5) { + ejectPassengers(); + discard(); + return; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + my *= seatLaunch ? 0.95 : 0.9; + double drag = isOpen() ? 0.95 : 0.99; + setDeltaMovement(mx * drag, my, mz * drag); + if (rider != null) { + setYRot(rider.getYRot()); + } + } + + /** + * The server moves the parachute; clients ease towards each position it sends over three + * ticks, as for the vehicles. Without this a slow descent steps from one update to the next. + */ + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + entityData.set(DATA_KIND, input.getIntOr("Kind", Kind.ITEM.ordinal())); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putInt("Kind", kind().ordinal()); + } +} From ec430de331d2a4c890bae66e1780d961a50c5df6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:44 +0800 Subject: [PATCH 0489/1461] Add entity/PartState.java to the 26.1 shared sources --- .../jnhprojects/mcheli/entity/PartState.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/PartState.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/PartState.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/PartState.java new file mode 100644 index 00000000..54755d56 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/PartState.java @@ -0,0 +1,122 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; + +/** + * Open/closed state of a group of moving parts (canopy, landing gear, hatches, wings, VTOL nozzles). + * Ported from {@code MCH_Parts}. + * + *

The target state is one bit of a synched integer, so only the server changes it and every + * side sees the same value. The angle runs from 0 to {@link #max} at {@link #speed} degrees per tick + * and is advanced locally on every side, which keeps the animation smooth without sending angles. + */ +public final class PartState { + private final AircraftEntity owner; + private final int bit; + private final float max; + private final float speed; + private final Sounds sounds; + + private float angle; + private float prevAngle; + + PartState(AircraftEntity owner, int bit, float speed, Sounds sounds) { + this.owner = owner; + this.bit = bit; + this.max = 90.0F; + this.speed = speed; + this.sounds = sounds; + } + + /** Target state: {@code true} means open, folded, retracted or tilted, depending on the part. */ + public boolean status() { + return (owner.partBits() & (1 << bit)) != 0; + } + + /** Fully back in the default position. */ + public boolean isOff() { + return !status() && angle <= 0.02F; + } + + /** Fully in the switched position. */ + public boolean isOn() { + return status() && angle >= max - 0.02F; + } + + public float angle() { + return angle; + } + + public float angle(float partialTicks) { + return prevAngle + (angle - prevAngle) * partialTicks; + } + + /** Progress from 0 to 1. */ + public float factor() { + return angle / max; + } + + public float factor(float partialTicks) { + return angle(partialTicks) / max; + } + + /** Changes the target state. Server only; clients follow through synched data. */ + void set(boolean on, boolean playSound) { + if (owner.level().isClientSide() || status() == on) { + return; + } + owner.setPartBit(bit, on); + if (playSound) { + play(sounds.switching); + play(on ? sounds.startOn : sounds.startOff); + } + tick(); + } + + /** Jumps straight to {@code on} without animating, used for parts that start deployed. */ + void force(boolean on) { + if (!owner.level().isClientSide()) { + owner.setPartBit(bit, on); + } + angle = prevAngle = on ? max : 0.0F; + } + + void tick() { + prevAngle = angle; + if (status()) { + if (angle < max) { + angle = Math.min(angle + speed, max); + if (angle >= max) { + play(sounds.endOn); + } + } + } else if (angle > 0.0F) { + angle = Math.max(angle - speed, 0.0F); + if (angle <= 0.0F) { + play(sounds.endOff); + } + } + } + + private void play(Cue cue) { + if (cue != null && !owner.level().isClientSide()) { + ModSounds.playAt(owner, cue.sound(), 1.0F, cue.pitch()); + } + } + + /** One sound from {@code sounds.json} with its pitch. */ + record Cue(String sound, float pitch) { + } + + /** What plays when the part starts and finishes moving each way; {@code null} entries stay silent. */ + record Sounds(Cue startOn, Cue endOn, Cue startOff, Cue endOff, Cue switching) { + private static final Cue CLICK = new Cue("plane_cc", 1.0F); + private static final Cue CLICK_LOW = new Cue("plane_cc", 0.5F); + + static final Sounds HATCH = new Sounds(null, CLICK, null, CLICK, new Cue("plane_cv", 0.5F)); + static final Sounds CANOPY = new Sounds(null, CLICK, null, CLICK, null); + static final Sounds LANDING_GEAR = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.75F)); + static final Sounds NOZZLE = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.5F)); + static final Sounds WING = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, null); + } +} From f1ae851a440de20ac0c19d231785203c1c809165 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:45 +0800 Subject: [PATCH 0490/1461] Add entity/PlaneEntity.java to the 26.1 shared sources --- .../mcheli/entity/PlaneEntity.java | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java new file mode 100644 index 00000000..b80171b8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java @@ -0,0 +1,516 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A fixed wing aircraft, ported from {@code MCP_EntityPlane}. + * + *

Throttle pushes the plane along its nose, tilted ten degrees up, and the vertical part of + * that push is what keeps it in the air: below full throttle it sinks. VTOL capable planes tilt + * their nozzles (and tilt rotors) towards straight down, which turns the push into lift. + * + *

The original applied its attitude rules once per rendered frame with the partial tick + * filtered to about 0.5. Here they run three times per tick with that value, which is what a + * 60 FPS client did, so the handling is the same without depending on the frame rate. + */ +public class PlaneEntity extends AircraftEntity implements EngineSoundSource { + private static final float LOOK_SCALE = 0.03F; + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private @Nullable PartState nozzle; + private @Nullable PartState wing; + + private double currentSpeed = MIN_SPEED; + private float keyRoll; + private float rotorAngle; + public float rotorAngleO; + private float soundLevel; + + public PlaneEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable PlaneDefinition lookup(String name) { + return MCHeli.content().plane(name); + } + + @Override + public @Nullable PlaneDefinition definition() { + return (PlaneDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + PlaneDefinition def = (PlaneDefinition) definition; + nozzle = def.hasNozzleState() ? addPart(PART_NOZZLE, 1.5F, PartState.Sounds.NOZZLE) : null; + wing = def.wings.isEmpty() ? null : addPart(PART_WING, 2.5F, PartState.Sounds.WING); + if (nozzle != null && def.defaultVtol && !nozzle.status()) { + nozzle.force(true); + } + } + + @Override + public boolean steersWithMouse() { + return true; + } + + public @Nullable PartState nozzle() { + return nozzle; + } + + public @Nullable PartState wing() { + return wing; + } + + public float nozzleAngle() { + return nozzle != null ? nozzle.angle() : 0.0F; + } + + @Override + public int vtolMode() { + if (nozzle == null) { + return 0; + } + if (!nozzle.status()) { + return nozzle.angle() <= 0.005F ? 0 : 1; + } + return nozzle.angle() >= 89.995F ? 2 : 1; + } + + public float rotorAngle(float partialTicks) { + return rotorAngleO + (rotorAngle - rotorAngleO) * partialTicks; + } + + // Pilot actions, server side + + public boolean canSwitchVtol() { + PlaneDefinition def = definition(); + if (def == null || !def.vtol || nozzle == null || modeSwitchCooldown > 0 || vtolMode() == 1) { + return false; + } + return Math.abs(roll) <= 30.0F && !(onGround() && def.defaultVtol); + } + + public void toggleVtol() { + if (canSwitchVtol()) { + modeSwitchCooldown = 20; + setVtol(!nozzle.status()); + } + } + + private void setVtol(boolean on) { + PlaneDefinition def = definition(); + if (nozzle == null || def == null || (def.defaultVtol && onGround() && !on)) { + return; + } + nozzle.set(on, true); + } + + public boolean canFoldWing() { + PlaneDefinition def = definition(); + if (wing == null || def == null || modeSwitchCooldown > 0) { + return false; + } + if (def.variableSweepWing) { + // Sweep back only when fast and well clear of the ground, spread again only when slow. + if (!onGround() && !hasBlockBelow(3, 20, true)) { + if (throttle < 0.7F) { + return false; + } + } else if (throttle > 0.1F) { + return false; + } + } else { + if (!onGround() && !hasBlockBelow(3, 3, true)) { + return false; + } + if (throttle > 0.01F) { + return false; + } + } + return wing.isOff(); + } + + public boolean canUnfoldWing() { + return wing != null && modeSwitchCooldown <= 0 && wing.isOn(); + } + + @Override + public void toggleFold() { + PlaneDefinition def = definition(); + if (def == null) { + return; + } + if (!def.hatches.isEmpty()) { + super.toggleFold(); + } else if (canFoldWing()) { + wing.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldWing()) { + wing.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Folded wings ground the plane; swept wings only below 20% throttle. */ + private boolean canUseWing(PlaneDefinition def) { + if (wing == null) { + return true; + } + if (def.variableSweepWing) { + return throttle >= 0.2 || wing.isOff(); + } + return wing.isOff(); + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void updatePartsServer(AircraftDefinition definition) { + super.updatePartsServer(definition); + PlaneDefinition def = (PlaneDefinition) definition; + // Swept wings spread out again when the plane slows down or gets close to the ground. + if (wing != null && def.variableSweepWing && wing.isOn() && throttle >= 0.2F + && (throttle < 0.5 || hasBlockBelow(1, 10, true))) { + wing.set(false, true); + } + if (nozzle != null && def.defaultVtol && onGround() && vtolMode() == 0) { + nozzle.set(true, true); + } + } + + // Mouse + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + PlaneDefinition def = definition(); + if (def == null || ticksAlive() < 30 || isPassenger() || hasBlockBelow(3, 2, true)) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + boolean vtol = vtolMode() > 0; + float yawFactor = (vtol ? def.vtolYaw : 1.0F) * 0.8F; + float pitchFactor = (vtol ? def.vtolPitch : 1.0F) * 0.8F; + float rollInput = vtolMode() == 0 ? x * 0.5F : x; + + float yaw = clamp(x, 40.0F * def.mobilityYaw) * yawFactor * LOOK_SCALE; + float pitch = clamp(y, 40.0F * def.mobilityPitch) * pitchFactor * LOOK_SCALE; + float rollDelta = clamp(rollInput, 40.0F * def.mobilityRoll) * yawFactor * LOOK_SCALE; + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + // Simulation + + @Override + protected void simulate() { + PlaneDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + if (def.targetDrone && !piloted) { + input.throttleUp = true; + } + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + } + updateMotion(def); + } + + /** + * Gunner mode needs a plane in level flight with throttle up and nothing solid within five + * blocks below, so the autopilot has room to circle. + */ + @Override + public boolean canSwitchGunnerMode() { + if (!super.canSwitchGunnerMode()) { + return false; + } + float r = Math.abs(Mth.wrapDegrees(roll())); + float p = Math.abs(Mth.wrapDegrees(getXRot())); + return r <= 40.0F && p <= 40.0F && visibleThrottle() > 0.6F && !hasBlockBelow(3, 5, false); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlNotHovering}. */ + private void updateThrottle(PlaneDefinition def, boolean piloted) { + reverse *= 0.8F; + boolean canFly = piloted && isCanopyClosed() && canUseWing(def); + if (canFly && isGunnerMode()) { + // The autopilot holds the throttle the pilot left it at. + return; + } + if (canFly || def.targetDrone) { + float upDown = def.throttleUpDown; + boolean turning = input.left != input.right; + boolean up = input.throttleUp; + if (turning && throttle < def.pivotTurnThrottle && !up && !input.throttleDown) { + up = true; + upDown *= 2.0F; + } + if (up) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * def.throttleUpDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}. In gunner mode the autopilot levels out and circles by {@code AutoPilotRot}. */ + private void updateAttitude(PlaneDefinition def, float pt) { + boolean gunner = isGunnerMode(); + if (gunner) { + setXRot(getXRot() * 0.95F); + setYRot(getYRot() + def.autoPilotRot * 0.2F); + if (Math.abs(roll) > 20.0F) { + roll *= 0.95F; + } + } + boolean flying = !hasBlockBelow(3, 3, true); + if (!flying || input.freeLook || gunner || (def.floats && waterDepth() > 0.0)) { + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.6F * yawRate * pt); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.6F * yawRate * pt); + } + } else { + // A/D bank the plane; in VTOL mode the keys do nothing in the air. + float rate = vtolMode() != 0 ? 0.0F : 0.2F; + if (input.left && !input.right) { + keyRoll -= rate * pt; + } else if (input.right && !input.left) { + keyRoll += rate * pt; + } + roll += keyRoll * 0.5F * def.mobilityRoll; + } + keyRoll *= 1.0F - 0.1F * pt; + if (!flying && Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.97F); + } + if (nozzleAngle() > 0.001F) { + setXRot(getXRot() * (1.0F - 0.03F * pt)); + roll *= 1.0F - 0.1F * pt; + } + roll = AircraftRotation.wrap(roll); + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), def.minRotationPitch, def.maxRotationPitch)); + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + private float maxSpeed(PlaneDefinition def) { + float extra = 0.0F; + if (def.variableSweepWing) { + if (wing != null) { + extra = (def.sweepWingSpeed - def.speed) * wing.factor(); + } else if (hatch != null) { + extra = (def.sweepWingSpeed - def.speed) * hatch.factor(); + } + } + return def.speed + extra; + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(PlaneDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + // The gunner mode autopilot holds its altitude. + boolean levelOff = isGunnerMode(); + + if (depth == 0.0) { + if (def.targetDrone) { + levelOff = targetDroneAutopilot(def) || levelOff; + } + if (!levelOff) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else { + my *= 0.8; + } + } else { + setXRot(getXRot() * 0.8F); + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + } + + double push = throttle / 10.0; + float nozzleAngle = nozzleAngle(); + Vec3 v; + if (nozzleAngle > 0.001F) { + setXRot(getXRot() * 0.95F); + v = direction(getYRot(), getXRot() - nozzleAngle); + if (nozzleAngle >= 90.0F) { + v = new Vec3(v.x * 0.8, v.y, v.z * 0.8); + } + } else { + v = direction(getYRot(), getXRot() - 10.0F); + } + if (!levelOff) { + my += v.y * push / (nozzleAngle <= 0.01F ? 2.0 : 8.0); + } + + boolean canMove = def.canMoveOnGround || !isOnSolidGround(); + if (canMove) { + if (def.enableBack && reverse > 0.0F) { + mx -= v.x * reverse; + mz -= v.z * reverse; + } else { + mx += v.x * push; + mz += v.z * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = maxSpeed(def); + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + if (Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.8F); + } + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** Target drones circle at a safe height on their own. Returns whether to hold altitude. */ + private boolean targetDroneAutopilot(PlaneDefinition def) { + if (hasBlockBelow(3, 40, true)) { + if (!hasBlockBelow(3, 5, true)) { + setYRot(getYRot() + def.autoPilotRot * 2.0F); + if (getXRot() > -20.0F) { + setXRot(getXRot() - 0.5F); + } + } + return false; + } + setYRot(getYRot() + def.autoPilotRot); + setXRot(getXRot() * 0.95F); + if (!level().isClientSide() && canRaiseLandingGear() && modeSwitchCooldown <= 0) { + toggleLandingGear(); + } + return true; + } + + /** {@code MCH_Lib.Rot2Vec3}: unit vector along a yaw and pitch, Minecraft conventions. */ + private static Vec3 direction(float yaw, float pitch) { + double y = Math.toRadians(yaw); + double p = Math.toRadians(pitch); + return new Vec3(-Math.sin(y) * Math.cos(p), -Math.sin(p), Math.cos(y) * Math.cos(p)); + } + + // Animation and sound + + @Override + protected void tickAnimation() { + PlaneDefinition def = definition(); + float throttle = visibleThrottle(); + rotorAngleO = rotorAngle; + rotorAngle += throttle * def.rotorSpeed; + if (rotorAngle > 360.0F) { + rotorAngle -= 360.0F; + rotorAngleO -= 360.0F; + } else if (rotorAngle < 0.0F) { + rotorAngle += 360.0F; + rotorAngleO += 360.0F; + } + + float target = throttle; + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + PlaneDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return 0.6F + visibleThrottle() * 0.4F; + } +} From 47e6fea1ebe53e6b4fd4a4179e9cfdfc4e09d2b7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:45 +0800 Subject: [PATCH 0491/1461] Add entity/RappelEntity.java to the 26.1 shared sources --- .../mcheli/entity/RappelEntity.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java new file mode 100644 index 00000000..e5781817 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java @@ -0,0 +1,106 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Carries someone down a helicopter's rope, {@code MCH_EntityHide}. It stays under its hook while + * the helicopter drifts, slides down at a steady pace and lets its rider off at the ground or at + * the end of the rope. Pulling the ropes in drops whoever is still on them. + */ +public class RappelEntity extends Entity { + private static final EntityDataAccessor DATA_VEHICLE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_ROPE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + + public RappelEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_VEHICLE, 0); + builder.define(DATA_ROPE, -1); + } + + public void attach(AircraftEntity vehicle, int rope) { + entityData.set(DATA_VEHICLE, vehicle.getId()); + entityData.set(DATA_ROPE, rope); + } + + private @Nullable AircraftEntity vehicle() { + return level().getEntity(entityData.get(DATA_VEHICLE)) instanceof AircraftEntity a && a.isAlive() ? a : null; + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + AircraftEntity vehicle = vehicle(); + Entity rider = getFirstPassenger(); + if (rider != null) { + rider.resetFallDistance(); + } + if (level().isClientSide()) { + return; + } + if (vehicle == null || !vehicle.isRepelling() || rider == null) { + arrive(); + return; + } + Vec3 hook = vehicle.ropePosition(entityData.get(DATA_ROPE)); + Vec3 m = getDeltaMovement(); + double fall = (m.y - (onGround() ? 0.01 : 0.03)) * 0.9; + setPos(hook.x, getY(), hook.z); + if (onGround()) { + arrive(); + return; + } + move(MoverType.SELF, new Vec3(0.0, fall, 0.0)); + setDeltaMovement(0.0, fall, 0.0); + if (Math.abs(getY() - hook.y) > Math.abs(vehicle.ropeLength()) + 5.0F) { + arrive(); + } + } + + private void arrive() { + ejectPassengers(); + discard(); + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } +} From f0d582efa1d95ddb804eaa453b5305c324ea7c75 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:46 +0800 Subject: [PATCH 0492/1461] Add entity/TankEntity.java to the 26.1 shared sources --- .../jnhprojects/mcheli/entity/TankEntity.java | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java new file mode 100644 index 00000000..e729c7f5 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java @@ -0,0 +1,267 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.physics.GroundImpact; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Tanks and cars, ported from {@code MCH_EntityTank}. + * + *

The drive model is the plane's minus lift: throttle pushes along the hull, A/D turn it, + * S brakes and then reverses when {@code EnableBack} is set, and Space is a hand brake. The hull + * does not follow the mouse. Instead a {@link TerrainFollower} reads the terrain under the + * wheels and the hull eases towards that pitch and roll, while the crew look around freely and + * the turret follows their view. + */ +public class TankEntity extends AircraftEntity implements EngineSoundSource { + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private final TerrainFollower terrain = new TerrainFollower(this); + private double currentSpeed = MIN_SPEED; + /** + * Squared horizontal distance covered by last tick's move. The original read + * {@code posX - prevPosX} here, but vanilla resets {@code xo} before {@link #simulate()} runs, + * which left vehicles with {@code PivotTurnThrottle} unable to steer. + */ + private double lastStepSq; + private float soundLevel; + private float turnSound; + + public TankEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable TankDefinition lookup(String name) { + return MCHeli.content().tank(name); + } + + @Override + public @Nullable TankDefinition definition() { + return (TankDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + TankDefinition def = (TankDefinition) definition; + terrain.create(def.groundWheels, new Vector3(0.0, -0.35, def.weightedCenterZ)); + } + + /** Walls and long drops hurt tanks and cars, scaled by speed and height. */ + @Override + protected boolean takesGroundImpacts() { + return true; + } + + /** Tanks climb and tilt on slopes all the time; only rolling over damages them. */ + @Override + protected float crashTilt() { + return 91.0F; + } + + @Override + protected void simulate() { + TankDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), -90.0F, 90.0F)); + roll = Mth.clamp(roll, -90.0F, 90.0F); + } + } + updateMotion(def, piloted); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlSub}. */ + private void updateThrottle(TankDefinition def, boolean piloted) { + reverse *= 0.8F; + float upDown = def.throttleUpDown; + if (input.brake) { + reverse *= 0.5F; + throttle = Math.max(throttle - 0.02 * upDown, 0.0); + } + if ((piloted && isCanopyClosed()) || def.targetDrone) { + if (input.throttleUp || (def.targetDrone && !piloted)) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * upDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}: ease onto the terrain, steer with A/D. */ + private void updateAttitude(TankDefinition def, float pt) { + setXRot(getXRot() + (terrain.targetPitch - getXRot()) * pt); + roll += (terrain.targetRoll - roll) * pt; + + boolean flying = !hasBlockBelow(3, 3, true); + if (flying && !(def.floats && waterDepth() > 0.0)) { + return; + } + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + float pivot = def.pivotTurnThrottle; + double dist = lastStepSq; + // With PivotTurnThrottle set the vehicle has to be rolling before it can turn, like a car. + if (pivot <= 0.0F || throttle >= pivot || reverse >= pivot / 10.0F || dist > reverse * 0.01) { + float speedScale = pivot <= 0.0F ? 1.0F : (float) Math.sqrt(Math.min(dist, 1.0)); + float direction = !input.throttleUp && input.throttleDown && throttle < pivot + 0.05 ? -1.0F : 1.0F; + float step = 0.6F * yawRate * pt * direction * speedScale; + if (input.left && !input.right) { + setYRot(getYRot() - step); + } else if (input.right && !input.left) { + setYRot(getYRot() + step); + } + } + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(TankDefinition def, boolean piloted) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + + if (depth == 0.0) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double push = throttle / 10.0; + double yawRad = Math.toRadians(getYRot()); + double pitchRad = Math.toRadians(getXRot() - 10.0F); + double vx = -Math.sin(yawRad) * Math.cos(pitchRad); + double vy = -Math.sin(pitchRad); + double vz = Math.cos(yawRad) * Math.cos(pitchRad); + my += vy * push / 8.0; + + if (def.canMoveOnGround || !isOnSolidGround()) { + if (def.enableBack && reverse > 0.0F) { + mx -= vx * reverse; + mz -= vz * reverse; + } else { + mx += vx * push; + mz += vz * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + } + + if (!terrain.isEmpty()) { + Vec3 slide = terrain.move(mx, mz); + mx += slide.x; + mz += slide.z; + if (!piloted) { + // Nobody is easing the hull in per frame, so take the attitude straight away. + setXRot(terrain.targetPitch); + roll = terrain.targetRoll; + } + } + + double startX = getX(); + double startZ = getZ(); + move(MoverType.SELF, new Vec3(mx, my, mz)); + double stepX = getX() - startX; + double stepZ = getZ() - startZ; + lastStepSq = stepX * stepX + stepZ * stepZ; + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** The original's {@code applyOnGroundPitch} is a no-op for tanks; the terrain decides. */ + @Override + protected void applyOnGroundPitch(float factor) { + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void tickAnimation() { + float target = visibleThrottle(); + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + // Turning on the spot still revs the engine. + if (!input.left && !input.right && !input.throttleDown) { + turnSound *= 0.8F; + } else { + turnSound = Math.min(turnSound + 0.1F, 0.75F); + } + target = Math.max(target, turnSound); + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + TankDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return Math.max(0.5F + visibleThrottle() * 0.5F, 0.5F + turnSound * 0.5F); + } +} From 0ea8d81099dd97d361b6f980426c9b1cf71919ed Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:46 +0800 Subject: [PATCH 0493/1461] Add entity/TerrainFollower.java to the 26.1 shared sources --- .../mcheli/entity/TerrainFollower.java | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java new file mode 100644 index 00000000..92e8e59e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java @@ -0,0 +1,225 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * Works out the pitch and roll of a ground vehicle from where its wheels touch the terrain. + * Ported from {@code MCH_WheelManager}. + * + *

The original spawned an invisible entity per wheel and let each one fall onto the ground + * under its mount point. Here every wheel is a one block collision box moved with vanilla's + * collision code, which gives the same result without extra entities to sync. Each left/right + * pair spans a plane with the centre of gravity; the weighted average of their normals is the + * ground normal the hull leans towards. + */ +final class TerrainFollower { + private static final double STEP_HEIGHT = 1.5; + private static final double MAX_DRIFT = 2.0; + private static final double SNAP_DISTANCE = 8.0; + + private final AircraftEntity vehicle; + private Probe[] probes = new Probe[0]; + private Vector3 centre = Vector3.ZERO; + private double wheelBase = 1.0; + float targetPitch; + float targetRoll; + + TerrainFollower(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + /** + * @param wheels positive X wheel positions, front to back; the mirrored wheel is added for each + * @param centre centre of gravity in hull space + */ + void create(List wheels, Vector3 centre) { + this.centre = centre; + probes = new Probe[wheels.size() * 2]; + double minZ = Double.MAX_VALUE; + double maxZ = -Double.MAX_VALUE; + for (int i = 0; i < probes.length; i++) { + Vector3 w = wheels.get(i / 2); + Vector3 local = new Vector3(i % 2 == 0 ? w.x() : -w.x(), w.y(), w.z()); + Probe probe = new Probe(local, local.z() >= centre.z()); + Vec3 at = vehicle.toWorld(local); + probe.setPos(at.x, at.y + 1.0, at.z); + probes[i] = probe; + minZ = Math.min(minZ, local.z()); + maxZ = Math.max(maxZ, local.z()); + } + wheelBase = Math.max(maxZ - minZ, 1.0E-3); + } + + boolean isEmpty() { + return probes.length == 0; + } + + /** + * Drags the wheels along with the hull, lets them settle on the terrain and derives the target + * attitude from them. + * + * @return extra horizontal motion from sliding down steep slopes + */ + Vec3 move(double mx, double mz) { + for (Probe probe : probes) { + Vec3 target = vehicle.toWorld(probe.local); + if (target.distanceToSqr(probe.x, probe.y, probe.z) > SNAP_DISTANCE * SNAP_DISTANCE) { + // Teleported, or created before the vehicle was placed: start over under the hull. + probe.setPos(target.x, target.y + 1.0, target.z); + } + double dx = target.x - probe.x + mx; + double dy = (target.y - probe.y) * 0.15; + double dz = target.z - probe.z + mz; + probe.move(dx, dy, dz); + probe.move(0.0, -0.1, 0.0); + } + + int pairs = probes.length / 2; + // If neither half touches the ground, the outermost pair of that half carries the hull, + // which makes the vehicle tip over a crest instead of floating level. + int rear = pairs - 1; + for (int i = 0; i < pairs; i++) { + if (!probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + rear = -1; + break; + } + } + if (rear >= 0) { + probes[rear * 2].onGround = true; + probes[rear * 2 + 1].onGround = true; + } + int front = 0; + for (int i = pairs - 1; i >= 0; i--) { + if (probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + front = -1; + break; + } + } + if (front >= 0 && pairs > 0) { + probes[front * 2].onGround = true; + probes[front * 2 + 1].onGround = true; + } + + Vec3 wc = vehicle.toWorld(centre).subtract(vehicle.position()); + double cx = vehicle.getX() + wc.x; + double cy = vehicle.getY() + centre.y(); + double cz = vehicle.getZ() + wc.z; + Vec3 normal = Vec3.ZERO; + for (int i = 0; i < pairs; i++) { + Probe w1 = probes[i * 2]; + Probe w2 = probes[i * 2 + 1]; + Vec3 v1 = new Vec3(w1.x - cx, w1.y - cy, w1.z - cz); + Vec3 v2 = new Vec3(w2.x - cx, w2.y - cy, w2.z - cz); + Vec3 n = (w1.local.z() >= 0.0 ? v2.cross(v1) : v1.cross(v2)).normalize(); + double weight = w1.onGround || w2.onGround ? Math.abs(w1.local.z() / wheelBase) : 0.0; + normal = normal.add(n.scale(weight)); + } + normal = normal.normalize(); + + Vec3 slide = Vec3.ZERO; + if (normal.y > 0.01 && normal.y < 0.7) { + slide = new Vec3(normal.x / 50.0, 0.0, normal.z / 50.0); + } + + // Into the hull's frame, then read pitch and roll off the normal. + double yaw = Math.toRadians(vehicle.getYRot()); + double c = Math.cos(yaw); + double s = Math.sin(yaw); + double lx = normal.x * c + normal.z * s; + double lz = normal.z * c - normal.x * s; + float pitch = (float) (90.0 - Math.toDegrees(Math.atan2(normal.y, lz))); + float roll = (float) -(90.0 - Math.toDegrees(Math.atan2(normal.y, lx))); + if (normal.lengthSqr() < 1.0E-6) { + pitch = vehicle.getXRot(); + roll = vehicle.roll; + } + + var def = vehicle.definition(); + float pitchStep = def != null ? def.onGroundPitchFactor : 0.0F; + float rollStep = def != null ? def.onGroundRollFactor : 0.0F; + targetPitch = Mth.clamp(pitch, vehicle.getXRot() - pitchStep, vehicle.getXRot() + pitchStep); + targetRoll = Mth.clamp(roll, vehicle.roll - rollStep, vehicle.roll + rollStep); + + // Wheels that fell behind (stuck on a ledge) are pulled back under the hull. + for (Probe probe : probes) { + Vec3 v = vehicle.position().add(new Vec3(AircraftRotation.toWorld(vehicle.getYRot(), targetPitch, targetRoll, + probe.local.x(), probe.local.y(), probe.local.z()))); + boolean pulled = false; + double x = probe.x; + double z = probe.z; + if (x > v.x + MAX_DRIFT || x < v.x - MAX_DRIFT) { + x = Mth.clamp(x, v.x - MAX_DRIFT, v.x + MAX_DRIFT); + pulled = true; + } + if (z > v.z + MAX_DRIFT || z < v.z - MAX_DRIFT) { + z = Mth.clamp(z, v.z - MAX_DRIFT, v.z + MAX_DRIFT); + pulled = true; + } + if (pulled) { + probe.setPos(x, v.y + STEP_HEIGHT / 2.0, z); + } + } + return slide; + } + + /** One wheel: a one block box that falls onto the terrain and climbs steps up to 1.5 high. */ + private final class Probe { + final Vector3 local; + /** In front of the centre of gravity. */ + final boolean front; + double x; + double y; + double z; + boolean onGround; + + Probe(Vector3 local, boolean front) { + this.local = local; + this.front = front; + } + + void setPos(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + private AABB box() { + return new AABB(x - 0.5, y, z - 0.5, x + 0.5, y + 1.0, z + 0.5); + } + + void move(double dx, double dy, double dz) { + AABB box = box(); + Vec3 wanted = new Vec3(dx, dy, dz); + Vec3 moved = collide(wanted, box); + boolean blockedSideways = moved.x != dx || moved.z != dz; + boolean wasGrounded = onGround || (moved.y != dy && dy < 0.0); + if (blockedSideways && wasGrounded) { + // Same step-up rule as the original wheel entity: climb, move, settle back down. + Vec3 up = collide(new Vec3(0.0, STEP_HEIGHT, 0.0), box); + AABB raised = box.move(up); + Vec3 across = collide(new Vec3(dx, 0.0, dz), raised); + AABB shifted = raised.move(across); + Vec3 down = collide(new Vec3(0.0, -STEP_HEIGHT, 0.0), shifted); + Vec3 stepped = up.add(across).add(down); + if (stepped.horizontalDistanceSqr() > moved.horizontalDistanceSqr()) { + moved = stepped; + } + } + x += moved.x; + y += moved.y; + z += moved.z; + onGround = dy < 0.0 && moved.y != dy; + } + + private Vec3 collide(Vec3 movement, AABB box) { + return Entity.collideBoundingBox(vehicle, movement, box, vehicle.level(), List.of()); + } + } +} From dd0f9ee7526be0950233387e02ee9dc56d6b327a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:47 +0800 Subject: [PATCH 0494/1461] Add entity/UavStationEntity.java to the 26.1 shared sources --- .../mcheli/entity/UavStationEntity.java | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java new file mode 100644 index 00000000..221c9563 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java @@ -0,0 +1,339 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * The UAV station and the portable UAV controller, {@code MCH_EntityUavStation}. A player sits at + * it and puts a UAV in its slot: the drone appears at the station's launch offset and the player + * flies it from there. The portable controller only launches small UAVs and its lid opens and + * closes with Shift and right-click. + * + *

The original kept the player at the station and had the drone read its pilot from there. + * Here the pilot takes the drone's reserved seat for as long as they fly it, hidden and unhurt, + * so the drone uses the same controls, cameras, HUD and weapons as every other vehicle. Leaving + * the drone, or losing it, puts the pilot back beside the station. + */ +public class UavStationEntity extends Entity { + public static final int STATION = 1; + public static final int PORTABLE = 2; + /** Further out than this, measured flat, the link breaks and the drone is lost, as in the original. */ + public static final double RANGE = 123.0; + /** The launch offset goes up to this far from the station along each axis. */ + public static final int MAX_OFFSET = 50; + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_OPEN = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_OFFSET = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BLOCK_POS); + private static final EntityDataAccessor DATA_LINKED = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private final SimpleContainer slot = new SimpleContainer(1); + private @Nullable UUID lastUav; + /** The lid of the portable controller, 0 shut to 1 open. */ + private float cover; + private float previousCover; + + public UavStationEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, (byte) STATION); + builder.define(DATA_OPEN, true); + builder.define(DATA_OFFSET, new BlockPos(12, 2, 12)); + builder.define(DATA_LINKED, false); + } + + public int kind() { + return entityData.get(DATA_KIND); + } + + public void setKind(int kind) { + entityData.set(DATA_KIND, (byte) (kind == PORTABLE ? PORTABLE : STATION)); + } + + public boolean isOpen() { + return entityData.get(DATA_OPEN); + } + + /** Where a launched drone appears, relative to the station. */ + public BlockPos offset() { + return entityData.get(DATA_OFFSET); + } + + public void setOffset(BlockPos offset) { + entityData.set(DATA_OFFSET, new BlockPos(Mth.clamp(offset.getX(), -MAX_OFFSET, MAX_OFFSET), + Mth.clamp(offset.getY(), -MAX_OFFSET, MAX_OFFSET), Mth.clamp(offset.getZ(), -MAX_OFFSET, MAX_OFFSET))); + } + + /** Whether a drone is flown from here right now; the screen lights up. */ + public boolean isLinked() { + return entityData.get(DATA_LINKED); + } + + public void setLinked(boolean linked) { + entityData.set(DATA_LINKED, linked); + } + + public SimpleContainer slot() { + return slot; + } + + public float cover(float partialTicks) { + return Mth.lerp(partialTicks, previousCover, cover); + } + + /** + * {@code initUavPostion}: the first launch offset is twelve blocks out on the diagonal the + * station faces, two up. + */ + public void initOffset() { + int quarter = (int) (Mth.positiveModulo(getYRot() + 45.0F, 360.0F) / 90.0F) % 4; + int x = quarter == 0 || quarter == 3 ? 12 : -12; + int z = quarter == 0 || quarter == 1 ? 12 : -12; + setOffset(new BlockPos(x, 2, z)); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + previousCover = cover; + cover = Mth.clamp(cover + (isOpen() ? 0.1F : -0.1F), 0.0F, 1.0F); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.03, 0.0); + move(MoverType.SELF, new Vec3(0.0, m.y, 0.0)); + setDeltaMovement(0.0, onGround() ? 0.0 : m.y * 0.96, 0.0); + + Entity user = getFirstPassenger(); + if (user instanceof ServerPlayer player && !slot.getItem(0).isEmpty()) { + launch(player, slot.getItem(0)); + } + } + + /** What the slot holds, if it is a vehicle at all. */ + public static @Nullable AircraftDefinition definitionOf(ItemStack stack) { + String name = AircraftItem.vehicleOf(stack); + return name != null ? MCHeli.content().any(name) : null; + } + + /** Whether this station can put up the vehicle in {@code stack}. */ + public boolean accepts(@Nullable AircraftDefinition def) { + return def != null && def.uav && (kind() != PORTABLE || def.smallUav); + } + + /** {@code handleItem}: puts up the drone in the slot and hands it to {@code player}. */ + private void launch(ServerPlayer player, ItemStack stack) { + AircraftDefinition def = definitionOf(stack); + if (!accepts(def) || !(level() instanceof ServerLevel server)) { + return; + } + if (ServerConfig.isDisabled(def.name())) { + player.sendOverlayMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName())); + return; + } + AircraftEntity uav = ModEntities.typeFor(def).create(server, EntitySpawnReason.TRIGGERED); + if (uav == null) { + return; + } + BlockPos o = offset(); + double y = Math.max(getY() + o.getY(), server.getMinY() + 2.0); + uav.setType(def); + uav.snapTo(getX() + o.getX(), y, getZ() + o.getZ(), getYRot() - 180.0F, 0.0F); + if (!server.noCollision(uav, uav.getBoundingBox().deflate(0.1))) { + player.sendOverlayMessage(Component.translatable("message.mcheli.uav.blocked")); + slot.setItem(0, ItemStack.EMPTY); + Containers.dropItemStack(server, player.getX(), player.getY(), player.getZ(), stack); + return; + } + slot.setItem(0, ItemStack.EMPTY); + uav.setFuelLevel(def.targetDrone ? uav.maxFuel() : (int) (uav.maxFuel() * 0.05F)); + server.addFreshEntity(uav); + if (!def.targetDrone) { + player.closeContainer(); + control(player, uav); + } + } + + /** {@code controlLastAircraft}: takes the last drone flown from here again. */ + public boolean continueControl(ServerPlayer player) { + AircraftEntity uav = lastUav(); + if (uav == null || getFirstPassenger() != player || !uav.canTakeRemotePilot()) { + return false; + } + player.closeContainer(); + return control(player, uav); + } + + public @Nullable AircraftEntity lastUav() { + if (lastUav == null || !(level() instanceof ServerLevel server)) { + return null; + } + Entity e = server.getEntity(lastUav); + return e instanceof AircraftEntity uav && uav.isAlive() && !uav.isDestroyed() && inRange(uav) ? uav : null; + } + + public boolean inRange(Entity uav) { + double dx = uav.getX() - getX(); + double dz = uav.getZ() - getZ(); + return dx * dx + dz * dz <= RANGE * RANGE; + } + + private boolean control(ServerPlayer player, AircraftEntity uav) { + player.stopRiding(); + if (!uav.takeRemotePilot(player, this)) { + return false; + } + lastUav = uav.getUUID(); + setLinked(true); + player.setYRot(uav.getYRot()); + return true; + } + + /** Where a pilot comes back to when the drone is left or lost: the seat in front of the station. */ + public Vec3 returnPosition() { + float yaw = getYRot() * Mth.DEG_TO_RAD; + return new Vec3(getX() - Mth.sin(yaw) * 0.9, getY(), getZ() + Mth.cos(yaw) * 0.9); + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand, Vec3 location) { + if (isVehicle()) { + return InteractionResult.PASS; + } + if (kind() == PORTABLE && player.isSecondaryUseActive()) { + if (!level().isClientSide()) { + entityData.set(DATA_OPEN, !isOpen()); + } + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive() || kind() == PORTABLE && !isOpen()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer server) { + player.setYRot(getYRot() - 180.0F); + if (player.startRiding(this)) { + server.openMenu(new SimpleMenuProvider((id, inventory, p) -> new UavStationMenu(id, inventory, this), + Component.translatable(kind() == PORTABLE ? "item.mcheli.uav_station2" : "item.mcheli.uav_station"))); + } + } + return InteractionResult.SUCCESS; + } + + /** The original sat the player down in front of the console, lower for the portable one. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + double y = kind() == PORTABLE ? -0.6 : 0.35; + return new Vec3(0.0, y, 0.9).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved() || damage <= 0.0F) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + AircraftEntity uav = lastUav(); + if (uav != null) { + uav.releaseRemotePilot(); + } + Containers.dropContents(level, blockPosition(), slot); + if (!creative) { + spawnAtLocation(level, item()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), source.getEntity() instanceof Player ? "hit" : "helidmg", 1.0F, 1.0F); + discard(); + return true; + } + + private Item item() { + return kind() == PORTABLE ? ModItems.UAV_CONTROLLER.get() : ModItems.UAV_STATION.get(); + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith(@Nullable Entity other) { + return true; + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(item()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + setKind(input.getIntOr("Kind", STATION)); + entityData.set(DATA_OPEN, input.getBooleanOr("Open", true)); + setOffset(new BlockPos(input.getIntOr("UavX", 12), input.getIntOr("UavY", 2), input.getIntOr("UavZ", 12))); + lastUav = input.read("LastUav", UUIDUtil.CODEC).orElse(null); + slot.setItem(0, input.read("Item", ItemStack.CODEC).orElse(ItemStack.EMPTY)); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putInt("Kind", kind()); + output.putBoolean("Open", isOpen()); + BlockPos o = offset(); + output.putInt("UavX", o.getX()); + output.putInt("UavY", o.getY()); + output.putInt("UavZ", o.getZ()); + if (lastUav != null) { + output.store("LastUav", UUIDUtil.CODEC, lastUav); + } + if (!slot.getItem(0).isEmpty()) { + output.store("Item", ItemStack.CODEC, slot.getItem(0)); + } + } +} From cee2d7400afdf1131ba1496f2281f8813a40a794 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:47 +0800 Subject: [PATCH 0495/1461] Add entity/VehicleInventory.java to the 26.1 shared sources --- .../mcheli/entity/VehicleInventory.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java new file mode 100644 index 00000000..1fdc978e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java @@ -0,0 +1,145 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.supply.SupplyRules; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +/** + * What a vehicle carries besides its crew, {@code MCH_AircraftInventory} and the container half of + * {@code W_EntityContainer}: the equipment slots of the vehicle screen (three fuel cans and a + * parachute per ejection seat) and the cargo chest of vehicles with an {@code InventorySize}. + * + *

The cargo chest always has room for the largest size, 54 slots, and the screen shows as many + * rows as the definition asks for. Items in rows a content reload took away are kept rather than + * lost, and still drop when the vehicle is destroyed. + */ +public final class VehicleInventory { + public static final int FUEL_SLOTS = 3; + public static final int PARACHUTE_SLOTS = 2; + public static final int EQUIPMENT_SLOTS = FUEL_SLOTS + PARACHUTE_SLOTS; + public static final int CARGO_SLOTS = 54; + /** How far from the vehicle's centre a player outside may keep a screen open, past its width. */ + private static final double REACH = 12.0; + + private final AircraftEntity vehicle; + private final SimpleContainer equipment = new GuardedContainer(EQUIPMENT_SLOTS); + private final SimpleContainer cargo = new GuardedContainer(CARGO_SLOTS); + + VehicleInventory(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + public SimpleContainer equipment() { + return equipment; + } + + public SimpleContainer cargo() { + return cargo; + } + + /** {@code isUseableByPlayer}: the crew, or someone standing next to the vehicle. */ + public boolean stillValid(Player player) { + if (vehicle.isRemoved() || vehicle.definition() == null) { + return false; + } + if (player.getVehicle() == vehicle) { + return true; + } + double reach = REACH + vehicle.definition().bodyWidth / 2.0; + return player.distanceToSqr(vehicle) <= reach * reach; + } + + // Fuel + + /** + * {@code updateFuel}: every half second, while the vehicle stands on something, each fuel can + * in a fuel slot pours up to 100 units into the tank. Server only. + * + * @return whether any fuel went in + */ + boolean pourFuelCans(int ticks) { + if (ticks % SupplyRules.FUEL_SLOT_INTERVAL != 0 || !vehicle.canSupply()) { + return false; + } + boolean poured = false; + for (int slot = 0; slot < FUEL_SLOTS; slot++) { + ItemStack can = equipment.getItem(slot); + if (!(can.getItem() instanceof FuelItem)) { + continue; + } + int amount = SupplyRules.fuelFromCan(vehicle.maxFuel() - vehicle.fuel(), FuelItem.contents(can)); + if (amount > 0) { + vehicle.refuel(amount); + can.setDamageValue(can.getDamageValue() + amount); + poured = true; + } + } + if (poured) { + equipment.setChanged(); + } + return poured; + } + + // Parachutes + + /** Parachute slots the vehicle screen offers: one per ejection seat. */ + public static int parachuteSlots(AircraftDefinition def) { + return Math.min(def.ejectionSeats(), PARACHUTE_SLOTS); + } + + /** + * {@code consumeParachute}: takes one parachute out of the parachute slots for a crew member + * who bails out. + * + * @return false when there is none left + */ + boolean takeParachute() { + for (int slot = FUEL_SLOTS; slot < EQUIPMENT_SLOTS; slot++) { + if (equipment.getItem(slot).getItem() instanceof ParachuteItem) { + equipment.removeItem(slot, 1); + return true; + } + } + return false; + } + + // Saving and dropping + + void save(ValueOutput output) { + ContainerHelper.saveAllItems(output.child("Equipment"), equipment.getItems(), false); + ContainerHelper.saveAllItems(output.child("Cargo"), cargo.getItems(), false); + } + + void load(ValueInput input) { + equipment.clearContent(); + cargo.clearContent(); + input.child("Equipment").ifPresent(in -> ContainerHelper.loadAllItems(in, equipment.getItems())); + input.child("Cargo").ifPresent(in -> ContainerHelper.loadAllItems(in, cargo.getItems())); + } + + /** {@code setDead}: everything aboard falls out where the vehicle was. Server only. */ + void dropAll() { + Containers.dropContents(vehicle.level(), vehicle, equipment); + Containers.dropContents(vehicle.level(), vehicle, cargo); + } + + /** A container that only stays open for players {@link #stillValid(Player)} allows. */ + private final class GuardedContainer extends SimpleContainer { + GuardedContainer(int size) { + super(size); + } + + @Override + public boolean stillValid(Player player) { + return VehicleInventory.this.stillValid(player); + } + } +} From 4c39a584c94d334c1800a954aa249a21e0c67f50 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:47 +0800 Subject: [PATCH 0496/1461] Add entity/VehicleParticles.java to the 26.1 shared sources --- .../mcheli/entity/VehicleParticles.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java new file mode 100644 index 00000000..46ed88a8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java @@ -0,0 +1,180 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.ParticleSplash; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; + +/** + * The spray, dust and exhaust a vehicle throws up, all client side: + *

    + *
  • {@code onUpdate_ParticleSandCloud}: a helicopter's downwash raises a ring of dust off the + * ground or water below it; boats and amphibians with {@code EnableSeaSurfaceParticle} raise + * one off the sea while under way.
  • + *
  • {@code onUpdate_ParticleSplash}: each {@code AddParticleSplash} point sprays while it moves + * through water.
  • + *
  • {@code onUpdate_ParticleNozzle}: jet nozzles smoke with the throttle, more when the blast + * hits the ground, as on a Harrier in the hover.
  • + *
  • {@code onUpdate_ParticleLandingGear}: a plane rolling fast kicks up bits of the ground.
  • + *
+ */ +final class VehicleParticles { + private static final int WHITE = 0xFFFFFF; + + private VehicleParticles() { + } + + static void tick(AircraftEntity vehicle, AircraftDefinition def) { + if (vehicle.isDestroyed()) { + return; + } + switch (vehicle) { + case HelicopterEntity heli -> sandCloud(heli, def, false); + case PlaneEntity plane -> { + if (def instanceof PlaneDefinition planeDef) { + nozzles(plane, planeDef); + } + landingGear(plane); + splashes(plane, def); + sandCloud(plane, def, true); + } + case TankEntity tank -> { + splashes(tank, def); + sandCloud(tank, def, true); + } + default -> { + } + } + } + + private static void sandCloud(AircraftEntity vehicle, AircraftDefinition def, boolean seaOnly) { + if (seaOnly && !def.seaSurfaceParticles || def.particlesScale <= 0.01F) { + return; + } + Level level = vehicle.level(); + RandomSource random = vehicle.getRandom(); + float scale = def.particlesScale * 3.0F * (seaOnly ? 2.0F : 1.0F); + double throttle = Math.min(vehicle.visibleThrottle() * 2.0, 1.0); + // A boat needs some water under it before it sprays; the original counted water blocks. + int count = seaOnly ? (int) (scale * 7.0F) : 0; + int range = (int) (scale * 10.0F) + 1; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + int bx = Mth.floor(vehicle.getX() + 0.5); + int by = Mth.floor(vehicle.getY() + 0.5); + int bz = Mth.floor(vehicle.getZ() + 0.5); + int y = 0; + double groundY = 0.0; + boolean found = false; + for (; y < range && !found; y++) { + for (int x = -1; x <= 1 && !found; x++) { + for (int z = -1; z <= 1 && !found; z++) { + BlockState state = level.getBlockState(pos.set(bx + x, by - y, bz + z)); + if (state.isAir()) { + continue; + } + if (seaOnly && state.getFluidState().is(FluidTags.WATER)) { + count--; + } + if (count <= 0) { + groundY = vehicle.getY() + 1.0 + scale / 5.0F - y; + found = true; + } + } + } + } + if (!found) { + return; + } + double density = (range - y + 1) / (5.0 * scale) / 2.0; + for (int k = 0; k < (int) (throttle * 6.0 * density); k++) { + float angle = random.nextFloat() * Mth.TWO_PI; + double dx = Mth.cos(angle); + double dz = Mth.sin(angle); + level.addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, scale * 5.0F, (int) (10.0F * scale) + 1), true, + vehicle.getX() + dx * scale * 3.0, groundY + (random.nextDouble() - 0.5) * scale, vehicle.getZ() + dz * scale * 3.0, + scale * dx * 0.3, scale * -0.02, scale * dz * 0.3); + } + } + + private static void splashes(AircraftEntity vehicle, AircraftDefinition def) { + if (def.splashes.isEmpty()) { + return; + } + double mx = vehicle.getX() - vehicle.xo; + double mz = vehicle.getZ() - vehicle.zo; + double dist = Math.min(mx * mx + mz * mz, 1.0); + RandomSource random = vehicle.getRandom(); + for (ParticleSplash splash : def.splashes) { + for (int i = 0; i < splash.count(); i++) { + if (dist > 0.03 + random.nextFloat() * 0.1) { + splash(vehicle, splash, -mx * splash.acceleration(), -mz * splash.acceleration(), splash.size() * (0.5 + dist * 0.5)); + } + } + } + } + + private static void splash(AircraftEntity vehicle, ParticleSplash splash, double mx, double mz, double size) { + RandomSource random = vehicle.getRandom(); + Vec3 at = vehicle.toWorld(splash.position()).add(random.nextDouble() - 0.5, (random.nextDouble() - 0.5) * 0.5, random.nextDouble() - 0.5); + BlockPos block = BlockPos.containing(at.x + 0.5, at.y, at.z + 0.5); + if (!vehicle.level().getFluidState(block).is(FluidTags.WATER)) { + return; + } + int age = splash.age() + (int) (random.nextFloat() * 0.5F * splash.age()); + vehicle.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, (float) size * (random.nextFloat() * 0.2F + 0.8F), age), true, + at.x, at.y, at.z, mx + (random.nextFloat() - 0.5) * 0.7, splash.motionY(), mz + (random.nextFloat() - 0.5) * 0.7); + } + + private static void nozzles(PlaneEntity plane, PlaneDefinition def) { + float throttle = plane.visibleThrottle(); + if (def.nozzles.isEmpty() || throttle <= 0.1F) { + return; + } + RandomSource random = plane.getRandom(); + PartState nozzle = plane.nozzle(); + float tilt = nozzle != null ? nozzle.angle() * Mth.DEG_TO_RAD : 0.0F; + // Out of the back, turned down as the nozzles swing towards the hover. + Vector3f blast = plane.toWorldOffset(new Vector3(0.0, -Mth.sin(tilt), -Mth.cos(tilt))); + for (MovingPart part : def.nozzles) { + if (random.nextFloat() > throttle * 1.5F) { + continue; + } + Vec3 at = plane.toWorld(part.position()).add(blast.x, blast.y, blast.z); + BlockPos ahead = BlockPos.containing(at.x + blast.x * 3.0, at.y + blast.y * 3.0, at.z + blast.z * 3.0); + float spread = plane.level().getBlockState(ahead).isAir() ? 0.7F : 2.0F; + plane.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, 5.0F * def.particlesScale, 30), true, at.x, at.y, at.z, + blast.x + (random.nextFloat() - 0.5F) * spread, blast.y, blast.z + (random.nextFloat() - 0.5F) * spread); + } + } + + private static void landingGear(PlaneEntity plane) { + double dx = plane.getX() - plane.xo; + double dz = plane.getZ() - plane.zo; + if (!plane.onGround() || dx * dx + dz * dz <= 0.01) { + return; + } + BlockPos below = BlockPos.containing(plane.getX(), plane.getY() - 0.5, plane.getZ()); + BlockState state = plane.level().getBlockState(below); + if (state.getRenderShape() == RenderShape.INVISIBLE) { + return; + } + RandomSource random = plane.getRandom(); + float width = plane.getBbWidth(); + plane.level().addParticle(new BlockParticleOption(ParticleTypes.BLOCK, state), + plane.getX() + (random.nextFloat() - 0.5) * width, plane.getBoundingBox().minY + 0.1, plane.getZ() + (random.nextFloat() - 0.5) * width, + -dx * 4.0, 1.5, -dz * 4.0); + } +} From 2e5855f8a525fc13dc4f20d23cb7bc2da71d3f3c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:48 +0800 Subject: [PATCH 0497/1461] Add entity/projectile/A10Entity.java to the 26.1 shared sources --- .../mcheli/entity/projectile/A10Entity.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java new file mode 100644 index 00000000..e72e952e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java @@ -0,0 +1,160 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import org.jspecify.annotations.Nullable; + +/** + * The A-10 that answers a close air support call, {@code MCH_EntityA10}. It dives in for a second, + * strafes the target with forty GAU-8 rounds and climbs away. + */ +public class A10Entity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_DIRECTION = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.INT); + private static final int LIFETIME = 70; + + private int despawnCount; + private int shotCount; + public int power = 32; + public int explosionPower = 1; + public float acceleration = 4.0F; + private @Nullable Entity shooter; + private @Nullable Entity user; + + public A10Entity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_DIRECTION, 0); + } + + /** + * @param direction 0 heading south (+Z), then clockwise in steps of 90 degrees + */ + public void setup(String weapon, int direction, double mx, double mz, @Nullable Entity shooter, @Nullable Entity user) { + entityData.set(DATA_WEAPON, weapon); + entityData.set(DATA_DIRECTION, direction); + setYRot(90 * direction); + yRotO = getYRot(); + setDeltaMovement(mx, 0.0, mz); + this.shooter = shooter; + this.user = user; + } + + public int direction() { + return entityData.get(DATA_DIRECTION); + } + + /** Hidden during the first second, while it is still out of sight. */ + public boolean isVisible() { + return despawnCount > 20; + } + + @Override + public void tick() { + super.tick(); + despawnCount++; + if (!level().isClientSide()) { + if (despawnCount > LIFETIME) { + discard(); + return; + } + if (shotCount < 40) { + for (int i = 0; i < 2; i++) { + fireGau8(shotCount); + shotCount++; + } + if (shotCount == 38) { + ModSounds.playAt(this, "gau-8_snd", 150.0F, 1.0F); + } + } + } + if (despawnCount <= 20) { + setDeltaMovement(getDeltaMovement().x, -0.3, getDeltaMovement().z); + } else { + setPos(getX() + getDeltaMovement().x, getY() + getDeltaMovement().y, getZ() + getDeltaMovement().z); + setDeltaMovement(getDeltaMovement().add(0.0, 0.02, 0.0)); + } + } + + private void fireGau8(int count) { + int dir = direction(); + double x = getX(); + double z = getZ(); + double tx = random.nextDouble() - 0.5; + double ty = -2.6; + double tz = random.nextDouble() - 0.5; + switch (dir) { + case 0 -> { + tz += 10.0; + z += count * 0.6; + } + case 1 -> { + tx -= 10.0; + x -= count * 0.6; + } + case 2 -> { + tz -= 10.0; + z -= count * 0.6; + } + default -> { + tx += 10.0; + x += count * 0.6; + } + } + BulletEntity round = ModEntities.BULLET.get().create(level(), EntitySpawnReason.TRIGGERED); + if (round == null) { + return; + } + round.launch(x, getY(), z, tx, ty, tz, 90 * dir, 30.0F, acceleration); + round.setWeapon(entityData.get(DATA_WEAPON)); + round.explosionPower = shotCount % 4 == 0 ? explosionPower : 0; + round.power = power; + round.setOwners(shooter, user); + level().addFreshEntity(round); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 512.0 * 512.0; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From ff2dd967789c4c81d4ce36782c23baffe3b9ab1d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:48 +0800 Subject: [PATCH 0498/1461] Add entity/projectile/AAMissileEntity.java to the 26.1 shared sources --- .../entity/projectile/AAMissileEntity.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java new file mode 100644 index 00000000..a9d41520 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** An air to air missile homing on its locked target, {@code MCH_EntityAAMissile}. */ +public class AAMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 3422500.0; + + public AAMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (age() > 4 && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize * 0.5F); + } + if (level().isClientSide() || w == null || isRemoved()) { + return; + } + Entity t = target(); + if (user() == null || t == null || t.isRemoved()) { + discard(); + return; + } + double d = distanceToSqr(t); + if (d > MAX_DISTANCE_SQ) { + discard(); + } else if (age() > w.rigidityTime) { + // The original compared the squared distance with the plain fuse distance. + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + setPos((t.getX() + getX()) / 2.0, (t.getY() + getY()) / 2.0, (t.getZ() + getZ()) / 2.0); + impact(new Impact(position(), t, null, Direction.UP), 1.0F); + } else { + guideTo(t.getX(), t.getY(), t.getZ()); + } + } + } + + @Override + public String defaultModel() { + return "aamissile"; + } +} From b4bd35fc2e904937a9867a801fd4df3e735801d4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:49 +0800 Subject: [PATCH 0499/1461] Add entity/projectile/ASMissileEntity.java to the 26.1 shared sources --- .../entity/projectile/ASMissileEntity.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java new file mode 100644 index 00000000..e5f46c96 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java @@ -0,0 +1,91 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * An air to surface missile flying to the block the gunner aimed at, {@code MCH_EntityASMissile}. + * Powered ones climb for half a second first; gliding bombs only steer sideways. + */ +public class ASMissileEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + + public ASMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public float gravity() { + return isBomblet() ? -0.03F : super.gravity(); + } + + @Override + public float gravityInWater() { + return isBomblet() ? -0.03F : super.gravityInWater(); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && !isBomblet()) { + trail(w.trajectoryParticle, 3, 10.0F * w.smokeSize * 0.5F); + } + if (w != null && !level().isClientSide() && !isBomblet() && !isRemoved()) { + BlockPos aim = BlockPos.containing(targetX, targetY, targetZ); + if (!level().getBlockState(aim).isAir()) { + double dist = Math.sqrt(distanceToSqr(targetX, targetY, targetZ)); + if (dist < w.proximityFuseDist) { + if (w.bomblet > 0) { + for (int i = 0; i < w.bomblet; i++) { + sprinkleBomblet(); + } + } else { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + } + discard(); + } else if (gravity() == 0.0F) { + double up = age() < 10 ? 20.0 : 0.0; + aimAlong(targetX - getX(), targetY + up - getY(), targetZ - getZ()); + } else { + double dx = targetX - getX(); + double dy = (targetY - getY()) * 0.3; + double dz = targetZ - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + Vec3 m = getDeltaMovement(); + setDeltaMovement(dx * acceleration / d, m.y, dz * acceleration / d); + } + } + } + faceMotion(); + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + ASMissileEntity child = spawnChild(ModEntities.AS_MISSILE.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x * 0.5 + (random.nextFloat() - 0.5F) * spread, + m.y * 0.5 / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z * 0.5 + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From 4a2b5c25b8539db3f89cf89131991de68fc0258f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:49 +0800 Subject: [PATCH 0500/1461] Add entity/projectile/ATMissileEntity.java to the 26.1 shared sources --- .../entity/projectile/ATMissileEntity.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java new file mode 100644 index 00000000..d7031ecf --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java @@ -0,0 +1,84 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** + * An anti tank missile, {@code MCH_EntityATMissile}. In the default mode it flies straight at the + * locked target; in top attack mode it climbs 150 blocks above the shooter and dives. + */ +public class ATMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 2250000.0; + + /** 0 direct, 1 top attack. */ + public int guidanceType; + + public ATMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && tickCount >= w.trajectoryParticleStartTick) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + if (!level().isClientSide() && !isRemoved()) { + Entity t = target(); + if (user() != null && t != null && !t.isRemoved() && w != null) { + steer(w, t); + } else { + discard(); + } + } + faceMotion(); + } + + private void steer(WeaponDefinition w, Entity t) { + double x = t.getX() - getX(); + double y = t.getY() - getY(); + double z = t.getZ() - getZ(); + double d = x * x + y * y + z * z; + if (d > MAX_DISTANCE_SQ) { + discard(); + return; + } + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + return; + } + int rigidity = w.rigidityTime; + if (age() <= rigidity) { + return; + } + float af = age() < rigidity + w.trajectoryParticleStartTick ? 0.5F : 1.0F; + if (guidanceType == 1) { + Entity shooter = user(); + if (age() <= rigidity + 20) { + guideTo(t.getX(), shooter.getY() + 150.0, t.getZ()); + } else if (age() <= rigidity + 30) { + guideTo(t.getX(), shooter.getY(), t.getZ()); + } else { + if (age() == rigidity + 35) { + power = (int) (power * 1.2F); + if (explosionPower > 0) { + explosionPower++; + } + } + guideTo(t.getX(), t.getY(), t.getZ()); + } + } else { + d = Math.sqrt(d); + setDeltaMovement(x * acceleration / d * af, y * acceleration / d * af, z * acceleration / d * af); + } + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From da8b31b3aca370dd11242dc7306af13adeace648 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:50 +0800 Subject: [PATCH 0501/1461] Add entity/projectile/BombEntity.java to the 26.1 shared sources --- .../mcheli/entity/projectile/BombEntity.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java new file mode 100644 index 00000000..c758d75b --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** A free fall bomb, {@code MCH_EntityBomb}. Cluster bombs split into bomblets. */ +public class BombEntity extends ProjectileEntity { + public BombEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (!level().isClientSide() && w != null && !isRemoved()) { + Vec3 m = getDeltaMovement(); + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + float fuse = w.proximityFuseDist; + if (fuse > 0.1F && age() % 10 == 0) { + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(fuse), this::canHit)) { + if (entity instanceof LivingEntity) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + BombEntity child = spawnChild(ModEntities.BOMB.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + /** A cluster bomb that has not split yet is not drawn when it splits right away. */ + public boolean isVisible() { + WeaponDefinition w = weapon(); + return w == null || isBomblet() || w.bomblet <= 0 || w.bombletSTime > 0; + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From 10fef9d1708533652d91f9f25bc6404dc18e5bf7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:50 +0800 Subject: [PATCH 0502/1461] Add entity/projectile/BulletEntity.java to the 26.1 shared sources --- .../entity/projectile/BulletEntity.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java new file mode 100644 index 00000000..b2b418ac --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** A gun round, {@code MCH_EntityBullet}. High explosive rounds may carry a proximity fuse. */ +public class BulletEntity extends ProjectileEntity { + public BulletEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return true; + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (isRemoved() || level().isClientSide() || age() <= 1 || w == null || explosionPower <= 0) { + return; + } + float fuse = w.proximityFuseDist; + if (fuse > 0.1F) { + fuse++; + float range = fuse + Math.abs(w.acceleration); + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(range), this::canHit)) { + if (entity.distanceToSqr(this) < fuse * fuse) { + setPos((entity.getX() + getX()) / 2.0, (entity.getY() + getY()) / 2.0, (entity.getZ() + getZ()) / 2.0); + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + + @Override + public String defaultModel() { + return "bullet"; + } +} From bd02881d637bd6058d7aa072d5999761484d28d8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:50 +0800 Subject: [PATCH 0503/1461] Add entity/projectile/DispensedItemEntity.java to the 26.1 shared sources --- .../projectile/DispensedItemEntity.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java new file mode 100644 index 00000000..69a2f674 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java @@ -0,0 +1,121 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseFireBlock; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A canister that uses an item on every block around where it lands, {@code MCH_EntityDispensedItem}: + * water bombers put out fires, crop dusters spread bone meal or seeds, and so on. + */ +public class DispensedItemEntity extends ProjectileEntity { + public DispensedItemEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize); + } + if (!level().isClientSide() && w != null) { + Vec3 m = getDeltaMovement(); + if (acceleration < 1.0E-4) { + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + } + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + } + tickBomblets(); + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + return; + } + WeaponDefinition w = weapon(); + Item item = w != null ? LegacyItems.item(w.dispenseItem, w.dispenseDamage) : null; + if (w != null && item != null && user() instanceof ServerPlayer player && !player.isRemoved()) { + BlockPos center = hit.block() != null ? hit.block() : blockPosition(); + int range = w.dispenseRange - 1; + for (int x = -range; x <= range; x++) { + for (int y = -range; y <= range; y++) { + for (int z = -range; z <= range; z++) { + int dist = x * x + y * y + z * z; + if (dist <= range * range && (dist <= 0.5 * range * range || random.nextInt(2) == 0)) { + useOn(server, center.offset(x, y, z), item); + } + } + } + } + } + discard(); + } + + private void useOn(ServerLevel server, BlockPos pos, Item item) { + if (pos.getY() < server.getMinY() || pos.getY() >= server.getMaxY()) { + return; + } + BlockState state = server.getBlockState(pos); + if (state.isAir()) { + return; + } + if (item == Items.WATER_BUCKET) { + // A water drop douses fire and quenches lava instead of placing water. + FluidState fluid = server.getFluidState(pos); + if (state.getBlock() instanceof BaseFireBlock) { + server.removeBlock(pos, false); + } else if (fluid.is(FluidTags.LAVA)) { + server.setBlockAndUpdate(pos, fluid.isSource() ? Blocks.OBSIDIAN.defaultBlockState() : Blocks.COBBLESTONE.defaultBlockState()); + } + return; + } + // Used from above with nobody holding it, the way a dispenser block would. + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(pos).add(0.0, 0.5, 0.0), Direction.UP, pos, false); + item.useOn(new UseOnContext(server, null, InteractionHand.MAIN_HAND, new ItemStack(item), hit)); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + DispensedItemEntity child = spawnChild(ModEntities.DISPENSED_ITEM.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From f3e8a80c2c0576cfb1146d165cafd774510b4ea0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:51 +0800 Subject: [PATCH 0504/1461] Add entity/projectile/FlareEntity.java to the 26.1 shared sources --- .../mcheli/entity/projectile/FlareEntity.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java new file mode 100644 index 00000000..a564a1fc --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java @@ -0,0 +1,161 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A burning flare, or with a fuse a smoke grenade that bursts into a cloud. Port of + * {@code MCH_EntityFlare}. Flares only look the part: decoying happens in the dispenser. + */ +public class FlareEntity extends Entity { + private static final EntityDataAccessor DATA_FUSE = SynchedEntityData.defineId(FlareEntity.class, EntityDataSerializers.INT); + + private double gravity = -0.013; + private double airResistance = 0.992; + private float size = 6.0F; + + public FlareEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_FUSE, 0); + } + + /** + * @param fuse ticks until a smoke grenade bursts, 0 for a flare + * @param slow the long hanging type 4 flare + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float size, int fuse, boolean slow) { + snapTo(x, y, z, random.nextFloat() * 360.0F, random.nextFloat() * 360.0F); + setDeltaMovement(mx, my, mz); + this.size = size; + entityData.set(DATA_FUSE, fuse); + if (slow) { + gravity *= 0.6; + airResistance = 0.995; + } + } + + private int fuse() { + return entityData.get(DATA_FUSE); + } + + /** A smoke grenade rather than a burning flare. */ + public boolean isSmoke() { + return fuse() > 0; + } + + @Override + public void tick() { + if (fuse() > 0 && tickCount >= fuse()) { + // Removal bursts the grenade on the client. + discard(); + return; + } + boolean server = !level().isClientSide(); + if (server && (!level().isLoaded(blockPosition()) || tickCount > 300)) { + discard(); + return; + } + super.tick(); + Vec3 m = getDeltaMovement(); + if (server) { + HitResult hit = level().clip(new ClipContext(position(), position().add(m), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.MISS) { + discard(); + return; + } + } else { + trail(); + } + setPos(getX() + m.x, getY() + m.y, getZ() + m.z); + setDeltaMovement(m.x * airResistance, m.y + gravity, m.z * airResistance); + if (server && (isInWater() || onGround())) { + discard(); + } + } + + private void trail() { + double x = (getX() - xo) / 2.0; + double y = (getY() - yo) / 2.0; + double z = (getZ() - zo) / 2.0; + for (int i = 0; i < 2; i++) { + if (size < 5.0F && random.nextInt(2) == 0) { + continue; + } + level().addAlwaysVisibleParticle(fuse() > 0 ? ParticleTypes.CLOUD : ParticleTypes.LARGE_SMOKE, true, + xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + if (fuse() == 0) { + level().addAlwaysVisibleParticle(ParticleTypes.FLAME, true, xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + } + } + } + + /** A smoke grenade bursting into a cloud that hangs around for ten seconds or more. */ + private void burst() { + if (!level().isClientSide()) { + return; + } + for (int i = 0; i < 20; i++) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + getX() + (random.nextDouble() - 0.5) * 10.0, + getY() + (random.nextDouble() - 0.5) * 10.0, + getZ() + (random.nextDouble() - 0.5) * 10.0, + (random.nextDouble() - 0.5) * 0.045, 0.005, (random.nextDouble() - 0.5) * 0.045); + } + } + + @Override + public void remove(RemovalReason reason) { + if (level().isClientSide() && fuse() > 0) { + burst(); + entityData.set(DATA_FUSE, 0); + } + super.remove(reason); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isInvulnerable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From c6a66e6909abc90d6b1be4ee8b4d1fc0b116fb3d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:51 +0800 Subject: [PATCH 0505/1461] Add entity/projectile/MarkerRocketEntity.java to the 26.1 shared sources --- .../entity/projectile/MarkerRocketEntity.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java new file mode 100644 index 00000000..eb723310 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java @@ -0,0 +1,139 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; + +/** + * A marking rocket, {@code MCH_EntityMarkerRocket}. Where it lands under open sky it pops red + * smoke, and five seconds later a stick of bombs falls on the mark. + */ +public class MarkerRocketEntity extends ProjectileEntity { + private static final EntityDataAccessor DATA_STATUS = SynchedEntityData.defineId(MarkerRocketEntity.class, EntityDataSerializers.BYTE); + + /** 0 inert, 1 flying, 2 marking. */ + private int countDown; + + public MarkerRocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_STATUS, (byte) 0); + } + + public void setStatus(int status) { + if (!level().isClientSide()) { + entityData.set(DATA_STATUS, (byte) status); + } + } + + public int status() { + return entityData.get(DATA_STATUS); + } + + @Override + public void tick() { + int status = status(); + if (level().isClientSide()) { + if (weapon() == null) { + super.tick(); + } + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && status != 0) { + if (status == 1) { + super.tick(); + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } else { + float gb = random.nextFloat() * 0.3F; + int rgb = (int) ((random.nextFloat() * 0.2F + 0.8F) * 255) << 16 | (int) (gb * 255) << 8 | (int) (gb * 255); + for (int i = 0; i < 5; i++) { + level().addAlwaysVisibleParticle(new DustParticleOptions(rgb, 4.0F), true, getX(), getY(), getZ(), + (random.nextFloat() - 0.5F) * 0.7F, 0.3F + random.nextFloat() * 0.3F, (random.nextFloat() - 0.5F) * 0.7F); + } + } + } + } else if (status == 0 || isInWater()) { + discard(); + } else if (status == 1) { + super.tick(); + } else if (countDown > 0) { + countDown--; + if (countDown == 40) { + dropBombs(); + } + } else { + discard(); + } + } + + private void dropBombs() { + int count = 6 + random.nextInt(2); + for (int i = 0; i < count; i++) { + BombEntity bomb = ModEntities.BOMB.get().create(level(), EntitySpawnReason.TRIGGERED); + if (bomb == null) { + continue; + } + bomb.launch(getX() + (random.nextFloat() - 0.5F) * 15.0F, 260.0F + random.nextFloat() * 10.0F + i * 30, + getZ() + (random.nextFloat() - 0.5F) * 15.0F, 0.0, -0.5, 0.0, 0.0F, 90.0F, 4.0); + bomb.setWeapon(weaponName()); + bomb.explosionPower = 3 + random.nextInt(2); + bomb.explosionPowerInWater = 0; + bomb.power = 30; + bomb.piercing = 0; + bomb.setOwners(shooter, user); + level().addFreshEntity(bomb); + } + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (level().isClientSide() || hit.entity() != null || hit.block() == null) { + return; + } + BlockPos mark = hit.block().relative(hit.side()); + if (!level().getBlockState(mark).isAir()) { + discard(); + return; + } + level().setBlockAndUpdate(mark, Blocks.FIRE.defaultBlockState()); + int solid = 0; + for (int y = mark.getY() + 1; y < level().getMaxY(); y++) { + if (!level().getBlockState(new BlockPos(mark.getX(), y, mark.getZ())).isAir() && ++solid >= 5) { + break; + } + } + if (solid < 5) { + setStatus(2); + setPos(mark.getX() + 0.5, mark.getY() + 1.1, mark.getZ() + 0.5); + xo = getX(); + yo = getY(); + zo = getZ(); + countDown = 100; + } else { + discard(); + } + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From 9b46a22f05f1c4d3cd76a18ee0fe5e6a8dbafa45 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:52 +0800 Subject: [PATCH 0506/1461] Add entity/projectile/ProjectileEntity.java to the 26.1 shared sources --- .../entity/projectile/ProjectileEntity.java | 790 ++++++++++++++++++ 1 file changed, 790 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java new file mode 100644 index 00000000..30f171c5 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java @@ -0,0 +1,790 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Combat; +import com.jnhprojects.mcheli.weapon.Explosions; +import com.jnhprojects.mcheli.weapon.ModDamageTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Everything a weapon fires: rounds, rockets, missiles, bombs and torpedoes. A port of + * {@code MCH_EntityBaseBullet}. + * + *

Projectiles fly on both sides, the client only for looks; hits, fuses and guidance are decided + * on the server. They are never saved: like the original, one found in a loaded chunk vanishes. + */ +public abstract class ProjectileEntity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TARGET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BOMBLET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.BOOLEAN); + /** Who fired, for clients: their rounds must not stop on their own vehicle. */ + private static final EntityDataAccessor DATA_SHOOTER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_USER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + + /** Beyond 5820 blocks from whoever fired it, a projectile is dropped. */ + private static final double MAX_RANGE_SQ = 3.38724E7; + + protected @Nullable Entity shooter; + protected @Nullable Entity user; + protected @Nullable Entity target; + private @Nullable WeaponDefinition weapon; + + public int explosionPower; + public int explosionPowerInWater; + public int power = 1; + public int piercing; + public int delayFuse; + public int sprinkleTime; + public double acceleration = 1.0; + public double accelerationFactor = 1.0; + private int age; + /** Client only: the round hit something here and waits, hidden, for the server to remove it. */ + private boolean spent; + + public Vec3 prevMotion = Vec3.ZERO; + /** Position two ticks ago, used to spread trail particles over the distance covered. */ + protected double xo2; + protected double yo2; + protected double zo2; + + protected ProjectileEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_TARGET, 0); + builder.define(DATA_BOMBLET, false); + builder.define(DATA_SHOOTER, 0); + builder.define(DATA_USER, 0); + } + + /** + * The second constructor of the original: place the projectile and point it along + * {@code (mx, my, mz)} at {@code acceleration} blocks per tick, capped at 3.9. + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float yaw, float pitch, double acceleration) { + snapTo(x, y, z, yaw, pitch); + xo2 = x; + yo2 = y; + zo2 = z; + double a = Math.min(acceleration, 3.9); + double d = Math.sqrt(mx * mx + my * my + mz * mz); + Vec3 motion = d > 0.0 ? new Vec3(mx * a / d, my * a / d, mz * a / d) : Vec3.ZERO; + setDeltaMovement(motion); + prevMotion = motion; + this.acceleration = a; + } + + // Weapon + + public void setWeapon(String name) { + WeaponDefinition def = MCHeli.content().weapon(name); + if (def == null) { + return; + } + weapon = def; + if (!level().isClientSide()) { + entityData.set(DATA_WEAPON, name); + } + if (def.bomblet > 0) { + sprinkleTime = def.bombletSTime; + } + piercing = def.piercing; + if (boostsAcceleration() && def.acceleration > 4.0F) { + accelerationFactor = def.acceleration / 4.0F; + } + } + + /** Rounds and rockets faster than 4 blocks a tick move several steps per tick instead. */ + protected boolean boostsAcceleration() { + return false; + } + + public @Nullable WeaponDefinition weapon() { + return weapon; + } + + /** Model drawn when the weapon names none, one of the original's {@code MCH_DefaultBulletModels}. */ + public abstract String defaultModel(); + + /** The model to draw: {@code ModelBomblet} for bomblets, else {@code ModelBullet}, else the default. */ + public String model() { + if (weapon == null) { + return defaultModel(); + } + String name = isBomblet() ? weapon.bombletModel : weapon.bulletModel; + return name.isEmpty() ? defaultModel() : name; + } + + public String weaponName() { + return entityData.get(DATA_WEAPON); + } + + /** {@code setParameterFromWeapon}: power and charges from the weapon file, and who to credit. */ + public void setParameters(WeaponDefinition def, @Nullable Entity shooter, @Nullable Entity user) { + explosionPower = def.explosion; + explosionPowerInWater = def.explosionInWater; + power = def.power; + piercing = def.piercing; + setOwners(shooter, user); + } + + /** The same, inherited from a parent projectile, for bomblets. */ + public void setParameters(ProjectileEntity parent) { + explosionPower = parent.explosionPower; + explosionPowerInWater = parent.explosionPowerInWater; + power = parent.power; + piercing = parent.piercing; + setOwners(parent.shooter, parent.user); + } + + public void setOwners(@Nullable Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + entityData.set(DATA_SHOOTER, shooter != null ? shooter.getId() : 0); + entityData.set(DATA_USER, user != null ? user.getId() : 0); + } + + public @Nullable Entity user() { + return user; + } + + public @Nullable Entity shooter() { + return shooter; + } + + public boolean isBomblet() { + return entityData.get(DATA_BOMBLET); + } + + public void setBomblet() { + sprinkleTime = 0; + entityData.set(DATA_BOMBLET, true); + } + + public void setTarget(@Nullable Entity entity) { + target = entity; + if (!level().isClientSide()) { + entityData.set(DATA_TARGET, entity != null ? entity.getId() : 0); + } + } + + public @Nullable Entity target() { + return target; + } + + /** A flare seduced this missile: it loses its target and is gone. */ + public void decoy() { + target = null; + discard(); + } + + public int age() { + return age; + } + + public float gravity() { + return weapon != null ? weapon.gravity : 0.0F; + } + + public float gravityInWater() { + return weapon != null ? weapon.gravityInWater : 0.0F; + } + + // Tick + + @Override + public void tick() { + if (spent) { + return; + } + if (level().isClientSide() && age == 0) { + int targetId = entityData.get(DATA_TARGET); + if (targetId > 0) { + target = level().getEntity(targetId); + } + } + Vec3 motion = getDeltaMovement(); + if (!motion.equals(prevMotion)) { + faceMotion(); + } + prevMotion = motion; + age++; + xo2 = xo; + yo2 = yo; + zo2 = zo; + super.tick(); + + if (weapon == null) { + if (age >= 2) { + discard(); + return; + } + setWeapon(weaponName()); + if (weapon == null) { + return; + } + } + + if (!level().isClientSide()) { + if (getY() <= level().getMaxY() && !level().isLoaded(blockPosition())) { + if (weapon.delayFuse <= 0) { + discard(); + return; + } + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + if (delayFuse > 0) { + delayFuse--; + if (delayFuse == 0) { + onTimeout(); + discard(); + return; + } + } + if (!isValid()) { + discard(); + return; + } + if (weapon.timeFuse > 0 && age > weapon.timeFuse) { + onTimeout(); + discard(); + return; + } + if (weapon.explosionAltitude > 0 && isGroundWithin(weapon.explosionAltitude)) { + impact(new Impact(position(), null, null, Direction.DOWN), 1.0F); + } + } + + motion = getDeltaMovement(); + setDeltaMovement(motion.x, motion.y + (isInWater() ? gravityInWater() : gravity()), motion.z); + if (!isRemoved()) { + checkCollisions(); + } + motion = getDeltaMovement().scale(accelerationFactor); + double nx = getX() + motion.x; + double ny = getY() + motion.y; + double nz = getZ() + motion.z; + if (level().isClientSide()) { + splash(nx, ny, nz); + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + level().addParticle(ParticleTypes.BUBBLE, nx - m.x * 0.25, ny - m.y * 0.25, nz - m.z * 0.25, m.x, m.y, m.z); + } + setPos(nx, ny, nz); + } + + /** Turns the nose along the flight path, the way every projectile class did after moving. */ + protected void faceMotion() { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + + /** {@code setMotion}: flies straight along {@code (dx, dy, dz)} at full speed. */ + protected void aimAlong(double dx, double dy, double dz) { + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d > 0.0) { + setDeltaMovement(dx * acceleration / d, dy * acceleration / d, dz * acceleration / d); + } + } + + /** {@code guidanceToTarget}: turns a seventh of the way towards the target each tick. */ + protected void guideTo(double tx, double ty, double tz) { + double dx = tx - getX(); + double dy = ty - getY(); + double dz = tz - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d <= 0.0) { + return; + } + Vec3 m = getDeltaMovement(); + setDeltaMovement((m.x * 6.0 + dx * acceleration / d) / 7.0, + (m.y * 6.0 + dy * acceleration / d) / 7.0, + (m.z * 6.0 + dz * acceleration / d) / 7.0); + faceMotion(); + } + + /** {@code checkValid}: whoever fired it still exists and is not too far away. */ + private boolean isValid() { + if (shooter == null && user == null) { + return false; + } + if (user != null && user.isRemoved()) { + return false; + } + Entity origin = user != null ? user : shooter; + double dx = getX() - origin.getX(); + double dz = getZ() - origin.getZ(); + return dx * dx + dz * dz < MAX_RANGE_SQ; + } + + private boolean isGroundWithin(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(Direction.DOWN); + if (!level().getBlockState(pos).isAir()) { + return true; + } + } + return false; + } + + /** Spawns {@code count} trail particles spread over the last two ticks of flight. */ + protected void trail(String name, int count, float size) { + if (!level().isClientSide() || name.isEmpty() || count < 1 || count > 50) { + return; + } + ParticleOptions particle = trailParticle(name); + double x = (getX() - xo) / count; + double y = (getY() - yo) / count; + double z = (getZ() - zo) / count; + double x2 = (xo - xo2) / count; + double y2 = (yo - yo2) / count; + double z2 = (zo - zo2) / count; + for (int i = 0; i < count; i++) { + double px = (xo + x * i + xo2 + x2 * i) / 2.0; + double py = (yo + y * i + yo2 + y2 * i) / 2.0; + double pz = (zo + z * i + zo2 + z2 * i) / 2.0; + level().addAlwaysVisibleParticle(particle, true, px, py, pz, 0.0, 0.0, 0.0); + } + } + + /** + * The original's own smoke particle ({@code explode}) was a large white puff; the cloud is the + * closest vanilla one. Other names are 1.7.10 particle names. + */ + private static ParticleOptions trailParticle(String name) { + return switch (name) { + case "explode", "cloud" -> ParticleTypes.CLOUD; + case "flame" -> ParticleTypes.FLAME; + case "largesmoke" -> ParticleTypes.LARGE_SMOKE; + case "heart" -> ParticleTypes.HEART; + case "fireworksspark" -> ParticleTypes.FIREWORK; + default -> ParticleTypes.SMOKE; + }; + } + + private void splash(double nx, double ny, double nz) { + if (weapon == null || weapon.power <= 0) { + return; + } + boolean wasDry = !level().getFluidState(BlockPos.containing(xo + 0.5, yo + 0.5, zo + 0.5)).is(FluidTags.WATER); + if (wasDry && level().getFluidState(BlockPos.containing(nx + 0.5, ny + 0.5, nz + 0.5)).is(FluidTags.WATER)) { + float pwr = Math.min(weapon.power, 20); + int n = random.nextInt(1 + (int) pwr / 3) + (int) pwr / 2 + 1; + for (int i = 0; i < n; i++) { + level().addParticle(ParticleTypes.SPLASH, nx, ny + 0.5, nz, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3, + pwr * 0.03 * (random.nextDouble() * 0.5 + 0.5) * 1.8, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3); + } + } + } + + /** {@code onUpdateTimeout}: the fuse ran out in mid air. */ + protected void onTimeout() { + if (!(level() instanceof ServerLevel server) || weapon == null) { + return; + } + if (isInWater()) { + if (explosionPowerInWater > 0) { + explode(server, getX(), getY(), getZ(), explosionPowerInWater, explosionPowerInWater, true); + } + } else if (explosionPower > 0) { + explode(server, getX(), getY(), getZ(), explosionPower, weapon.explosionBlock, false); + } else if (explosionPower < 0) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } + + /** {@code onUpdateBomblet}: once the sprinkle timer runs out the carrier splits up. */ + protected void tickBomblets() { + if (!level().isClientSide() && sprinkleTime > 0 && !isRemoved() && weapon != null) { + sprinkleTime--; + if (sprinkleTime == 0) { + for (int i = 0; i < weapon.bomblet; i++) { + sprinkleBomblet(); + } + discard(); + } + } + } + + protected void sprinkleBomblet() { + } + + /** Creates a copy of this projectile's kind for a bomblet, flying from here. */ + protected @Nullable T spawnChild(EntityType type, double mx, double my, double mz, float yaw, float pitch) { + T child = type.create(level(), EntitySpawnReason.TRIGGERED); + if (child == null) { + return null; + } + child.launch(getX(), getY(), getZ(), mx, my, mz, yaw, pitch, acceleration); + child.setWeapon(weaponName()); + child.setParameters(this); + return child; + } + + // Collision + + /** Where something was hit. {@code entity} is set for entity hits, {@code block} for block hits. */ + public record Impact(Vec3 location, @Nullable Entity entity, @Nullable BlockPos block, Direction side) { + } + + /** {@code onUpdateCollided}: the swept ray of this tick against blocks, then entities. */ + protected void checkCollisions() { + if (weapon == null) { + return; + } + Vec3 motion = getDeltaMovement().scale(accelerationFactor); + Vec3 from = position(); + Vec3 to = from.add(motion); + BlockHitResult blockHit = null; + for (int i = 0; i < 5; i++) { + blockHit = level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (blockHit.getType() != HitResult.Type.BLOCK) { + blockHit = null; + break; + } + BlockState state = level().getBlockState(blockHit.getBlockPos()); + if (user == null || level().isClientSide() || !Combat.isBulletBreakable(state)) { + break; + } + level().destroyBlock(blockHit.getBlockPos(), true, user); + } + + if (weapon.delayFuse > 0) { + if (blockHit != null) { + bounce(blockHit.getDirection()); + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + return; + } + + if (blockHit != null) { + to = blockHit.getLocation(); + } + Impact entityHit = findEntity(from, to, motion); + if (entityHit != null) { + // A vehicle applies the factor of the box struck itself, when it takes the damage. + impact(entityHit, 1.0F); + } else if (blockHit != null) { + impact(new Impact(blockHit.getLocation(), null, blockHit.getBlockPos(), blockHit.getDirection()), 1.0F); + } + } + + private @Nullable Impact findEntity(Vec3 from, Vec3 to, Vec3 motion) { + AABB area = getBoundingBox().expandTowards(motion).inflate(21.0); + Entity best = null; + Vec3 bestAt = null; + double bestDistance = 0.0; + List clipped = new ArrayList<>(); + for (Entity candidate : level().getEntities(this, area, this::canHit)) { + Optional at; + if (candidate instanceof AircraftEntity vehicle) { + at = vehicle.clipHitBoxes(from, to, 0.3); + clipped.add(vehicle); + } else { + at = candidate.getBoundingBox().inflate(0.3).clip(from, to); + } + if (at.isPresent()) { + double d = from.distanceTo(at.get()); + if (best == null || d < bestDistance) { + best = candidate; + bestAt = at.get(); + bestDistance = d; + } + } + } + // Only the vehicle actually hit keeps the factor of the box the ray struck. + for (AircraftEntity vehicle : clipped) { + if (vehicle != best) { + vehicle.consumeHitBoxFactor(); + } + } + return best != null ? new Impact(bestAt, best, null, Direction.UP) : null; + } + + /** Clients only learn who fired through synched data; look them up once it has arrived. */ + private void resolveOwners() { + if (shooter == null && entityData.get(DATA_SHOOTER) > 0) { + shooter = level().getEntity(entityData.get(DATA_SHOOTER)); + } + if (user == null && entityData.get(DATA_USER) > 0) { + user = level().getEntity(entityData.get(DATA_USER)); + } + } + + /** {@code canBeCollidedEntity}: never the shooter, its crew, or rounds fired by the same side. */ + public boolean canHit(Entity entity) { + if (level().isClientSide()) { + resolveOwners(); + } + if (!entity.isPickable() || entity == user || entity == shooter) { + return false; + } + if (entity instanceof ProjectileEntity other) { + if (level().isClientSide()) { + return false; + } + if (other.shooter != null && other.shooter == shooter || other.user != null && other.user == user) { + return false; + } + } + return !(shooter instanceof AircraftEntity vehicle) || !vehicle.hasPassenger(entity); + } + + /** {@code boundBullet}: a delayed fuse round bounces off what it hits. */ + private void bounce(Direction side) { + Vec3 m = getDeltaMovement(); + float bound = weapon != null ? weapon.bound : 0.0F; + switch (side) { + case DOWN -> { + if (m.y > 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case UP -> { + if (m.y < 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case NORTH -> { + if (m.z > 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case SOUTH -> { + if (m.z < 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case WEST -> { + if (m.x > 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + case EAST -> { + if (m.x < 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + } + } + + /** {@code onImpact}: damage what was hit, then explode or pierce through. */ + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + if (weapon != null && (weapon.explosion == 0 || weapon.modeNum >= 2) && hit.block() != null) { + debris(hit); + } + if (piercing <= 0) { + // The server decides what the hit does; until its removal arrives the round stays + // where it struck instead of flying on through the hull. + spent = true; + setPos(hit.location()); + setDeltaMovement(Vec3.ZERO); + } + return; + } + if (weapon == null) { + discard(); + return; + } + if (hit.entity() != null) { + hitEntity(hit.entity(), damageFactor); + piercing = 0; + } + float expPower = explosionPower * damageFactor; + float expPowerInWater = explosionPowerInWater * damageFactor; + Vec3 at = hit.location(); + if (piercing > 0) { + piercing--; + if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, 1.0F, 1.0F, false); + } + return; + } + if (expPowerInWater == 0.0F) { + if (weapon.fae) { + explodeFae(server, expPower); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } else if (hit.entity() != null) { + if (isInWater()) { + explode(server, at.x, at.y, at.z, expPowerInWater, expPowerInWater, true); + } else { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } + } else if (isInWater() || hit.block() != null && isWaterAround(hit.block())) { + BlockPos b = hit.block() != null ? hit.block() : blockPosition(); + explode(server, b.getX(), b.getY(), b.getZ(), expPowerInWater, expPowerInWater, true); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + discard(); + } + + /** {@code MCH_Lib.isBlockInWater}: water on any side of the block. */ + private boolean isWaterAround(BlockPos pos) { + for (Direction d : Direction.values()) { + if (level().getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return level().getFluidState(pos).is(FluidTags.WATER); + } + + /** Client side chips flying off the block a round struck. */ + private void debris(Impact hit) { + if (weapon == null || hit.block() == null) { + return; + } + BlockState state = level().getBlockState(hit.block()); + if (state.isAir()) { + return; + } + float p = weapon.power; + Vec3 m = getDeltaMovement(); + var particle = new BlockParticleOption(ParticleTypes.BLOCK, state); + for (int i = 0; i < p / 3.0F; i++) { + level().addParticle(particle, + hit.location().x + (random.nextFloat() - 0.5) * p / 10.0, + hit.location().y + 0.1, + hit.location().z + (random.nextFloat() - 0.5) * p / 10.0, + -m.x * p / 2.0, p / 2.0F, -m.z * p / 2.0); + } + } + + /** {@code onImpactEntity}. */ + protected void hitEntity(Entity entity, float damageFactor) { + if (!entity.isRemoved()) { + DamageSource source = ModDamageTypes.bullet(level(), this, user); + Combat.hurt(entity, source, power * damageFactor, weapon); + } + confirmHit(); + } + + /** {@code notifyHitBullet}: the shooter hears that a round connected. */ + protected void confirmHit() { + if (user instanceof ServerPlayer player) { + ModSounds.playTo(player, "hit", 1.0F, 1.0F); + } + } + + protected void explode(ServerLevel server, double x, double y, double z, float size, float blockSize, boolean inWater) { + if (weapon == null) { + return; + } + boolean destroyBlocks = !isBomblet() || random.nextInt(3) == 0; + Explosions.Params p = new Explosions.Params(this, user, size, blockSize, weapon.flaming, destroyBlocks, 0, inWater, weapon); + if (Explosions.explode(server, x, y, z, p)) { + confirmHit(); + } + } + + /** Fuel air explosives burn whatever they catch. */ + private void explodeFae(ServerLevel server, float size) { + Explosions.Params p = new Explosions.Params(this, user, size, weapon.explosionBlock, weapon.flaming, true, 15, false, weapon); + if (Explosions.explode(server, getX(), getY(), getZ(), p)) { + confirmHit(); + } + } + + // Entity plumbing + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + // Something thrown at a projectile sets it off, as in the original. + if (damage > 0.0F && source.is(DamageTypes.THROWN) && !isRemoved()) { + impact(new Impact(position().add(0.5, 0.5, 0.5), null, blockPosition(), Direction.UP), 1.0F); + return true; + } + return false; + } + + /** Client only: whether the round already hit something and should no longer be drawn. */ + public boolean isSpent() { + return spent; + } + + @Override + public boolean isPickable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putString("WeaponName", weaponName()); + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From ee60958975431ad8ba42829d5d20357510ca7674 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:52 +0800 Subject: [PATCH 0507/1461] Add entity/projectile/RocketEntity.java to the 26.1 shared sources --- .../entity/projectile/RocketEntity.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java new file mode 100644 index 00000000..c39fa6fe --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** An unguided rocket, {@code MCH_EntityRocket}, which may burst into bomblets. */ +public class RocketEntity extends ProjectileEntity { + public RocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return !isBomblet(); + } + + @Override + public void tick() { + super.tick(); + tickBomblets(); + WeaponDefinition w = weapon(); + if (!isBomblet() && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + RocketEntity child = spawnChild(ModEntities.ROCKET.get(), m.x, m.y, m.z, getYRot(), getXRot()); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(child.getDeltaMovement().add( + (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread)); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From e6d18ebf543eaa7443573196a96a44f80efe4bcb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:53 +0800 Subject: [PATCH 0508/1461] Add entity/projectile/ThrowableEntity.java to the 26.1 shared sources --- .../entity/projectile/ThrowableEntity.java | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java new file mode 100644 index 00000000..479ead5a --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java @@ -0,0 +1,200 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.Explosions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ARGB; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A thrown grenade, {@code MCH_EntityThrowable}. It flies under its definition's gravity, bounces + * off blocks losing speed, and when its fuse runs out either explodes or, for a smoke grenade, + * pours out coloured smoke until it disappears. + */ +public class ThrowableEntity extends Entity { + private static final EntityDataAccessor DATA_NAME = SynchedEntityData.defineId(ThrowableEntity.class, EntityDataSerializers.STRING); + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private @Nullable Entity thrower; + private @Nullable ThrowableDefinition definition; + private int definitionGeneration = -1; + + public ThrowableEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Throws {@code def} from {@code thrower}'s eyes the way it looks. + * + * @param strength 0.2 for a flick up to 1.4 for a full draw, the original's acceleration + */ + public static ThrowableEntity thrown(Level level, Entity thrower, ThrowableDefinition def, float strength) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + Vec3 look = thrower.getLookAngle(); + entity.setPos(thrower.getX() + look.x * 0.3, thrower.getEyeY() - 0.1, thrower.getZ() + look.z * 0.3); + entity.setYRot(thrower.getYRot()); + entity.setDeltaMovement(look.scale(1.5 * strength).add(thrower.getDeltaMovement())); + entity.thrower = thrower; + entity.setDefinition(def); + return entity; + } + + /** One fired from a dispenser: no thrower, straight along {@code direction}. */ + public static ThrowableEntity dispensed(Level level, Vec3 at, Vec3 direction, ThrowableDefinition def) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + entity.setPos(at); + entity.setDeltaMovement(direction.normalize().scale(def.dispenseAcceleration)); + entity.setDefinition(def); + return entity; + } + + private void setDefinition(ThrowableDefinition def) { + entityData.set(DATA_NAME, def.name()); + definition = def; + definitionGeneration = MCHeli.content().generation(); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_NAME, ""); + } + + /** The definition by name, looked up again after a content reload. */ + public @Nullable ThrowableDefinition definition() { + if (definitionGeneration != MCHeli.content().generation()) { + definition = MCHeli.content().throwable(entityData.get(DATA_NAME)); + definitionGeneration = MCHeli.content().generation(); + } + return definition; + } + + public String definitionName() { + return entityData.get(DATA_NAME); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + ThrowableDefinition def = definition(); + if (def == null) { + // The original gave a missing definition ten ticks to arrive. + if (tickCount > 10 && !level().isClientSide()) { + discard(); + } + return; + } + if (level().isClientSide()) { + if (def.smokes() && tickCount >= def.timeFuse) { + smoke(def); + } + return; + } + if (tickCount >= def.timeFuse && !def.smokes()) { + Explosions.explode((ServerLevel) level(), getX(), getY(), getZ(), new Explosions.Params(this, thrower, + def.explosion, def.explosion, false, true, 0, isInWater(), null)); + discard(); + return; + } + if (tickCount >= def.aliveTime) { + discard(); + return; + } + fly(def); + } + + /** {@code onUpdate} and {@code boundBullet}: gravity of its own, and a bounce off whatever it hits. */ + private void fly(ThrowableDefinition def) { + boolean water = isInWater(); + Vec3 m = getDeltaMovement().add(0.0, water ? def.gravityInWater : def.gravity, 0.0); + Vec3 from = position(); + move(MoverType.SELF, m); + // Whichever axis came up short hit something. + Vec3 moved = position().subtract(from); + double mx = blocked(moved.x, m.x) ? -m.x * def.bound : m.x; + double my = m.y; + double mz = blocked(moved.z, m.z) ? -m.z * def.bound : m.z; + if (blocked(moved.y, m.y)) { + mx *= 0.9; + mz *= 0.9; + // Off the floor or the ceiling it keeps a share of its speed, unless that would be a crawl. + my = Math.abs(my) > 0.05 ? -my * def.bound : 0.0; + } + double drag = water ? 0.8 : 0.99; + setDeltaMovement(mx * drag, my * drag, mz * drag); + } + + private static boolean blocked(double moved, double wanted) { + return Math.abs(moved) < Math.abs(wanted) - 1.0E-6; + } + + private void smoke(ThrowableDefinition def) { + int color = def.smokeColor; + for (int i = 0; i < def.smokeNum; i++) { + // A little noise per puff, keeping greys grey. + float r = ARGB.redFloat(color) * 0.9F + random.nextFloat() * 0.1F; + float g = ARGB.greenFloat(color) * 0.9F + random.nextFloat() * 0.1F; + float b = ARGB.blueFloat(color) * 0.9F + random.nextFloat() * 0.1F; + if (ARGB.red(color) == ARGB.green(color)) { + g = r; + } + if (ARGB.red(color) == ARGB.blue(color)) { + b = r; + } + if (ARGB.green(color) == ARGB.blue(color)) { + b = g; + } + float size = def.smokeSize + random.nextFloat() * def.smokeSize / 3.0F; + level().addAlwaysVisibleParticle(new SmokeParticleOptions(ARGB.colorFromFloat(1.0F, r, g, b) & 0xFFFFFF, size), true, + getX(), getY() + 0.5, getZ(), + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2, + def.smokeVelocityVertical * random.nextFloat() * 0.2, + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2); + } + } + + /** The server moves it; clients ease towards each position it sends. */ + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From ab205342d306bad2ac6b362b83d8bcb0d23b78f4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:53 +0800 Subject: [PATCH 0509/1461] Add entity/projectile/TorpedoEntity.java to the 26.1 shared sources --- .../entity/projectile/TorpedoEntity.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java new file mode 100644 index 00000000..a89f7c01 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java @@ -0,0 +1,56 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A torpedo, {@code MCH_EntityTorpedo}. Once in the water it settles on its running speed and + * either holds course or, when guided, runs to the point the gunner aimed at. + */ +public class TorpedoEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + public double accelerationInWater = 2.0; + + public TorpedoEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + boolean guided = w != null && w.guidedTorpedo; + if (!level().isClientSide() && isInWater()) { + if (acceleration < accelerationInWater) { + acceleration += 0.1; + } else if (acceleration > accelerationInWater + 0.2F) { + acceleration -= 0.1; + } + if (guided) { + aimAlong(targetX - getX(), targetY - getY(), targetZ - getZ()); + } else { + Vec3 m = getDeltaMovement(); + aimAlong(m.x, m.y * 0.8F, m.z); + } + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + if (guided) { + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + } + + @Override + public String defaultModel() { + return "torpedo"; + } +} From 2d6e897d833ec2721db22aa0002eb4e17eb7fd4f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:54 +0800 Subject: [PATCH 0510/1461] Add entity/projectile/TvMissileEntity.java to the 26.1 shared sources --- .../entity/projectile/TvMissileEntity.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java new file mode 100644 index 00000000..a06fef1a --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A wire guided missile flown by the gunner's view, {@code MCH_EntityTvMissile}. It steers while + * it is the vehicle's active TV missile; once another is fired it carries on straight. + */ +public class TvMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 1440000.0; + + public TvMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + Entity aimer = user(); + if (aimer != null) { + if (distanceToSqr(aimer) > MAX_DISTANCE_SQ) { + discard(); + } + if (!level().isClientSide() && !isRemoved()) { + steer(aimer); + } + } else if (!level().isClientSide()) { + discard(); + } + } + + private void steer(Entity aimer) { + if (aimer.isRemoved() || !(shooter() instanceof AircraftEntity vehicle) || vehicle.tvMissile() != this) { + return; + } + Vec3 dir = Vec3.directionFromRotation(aimer.getXRot(), aimer.getYRot()); + aimAlong(dir.x, dir.y, dir.z); + setYRot(aimer.getYRot()); + setXRot(aimer.getXRot()); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From e7a7168d607aef3e25a2e3da70dce7f9348b6fa0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:54 +0800 Subject: [PATCH 0511/1461] Add item/AircraftItem.java to the 26.1 shared sources --- .../jnhprojects/mcheli/item/AircraftItem.java | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java new file mode 100644 index 00000000..7b04e4a4 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java @@ -0,0 +1,146 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.ChatFormatting; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.component.TooltipDisplay; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Places a vehicle, the same way a boat is placed. + * + *

There is one item per vehicle kind ({@code mcheli:helicopter}, {@code mcheli:plane} and so + * on). Which vehicle a stack places is stored in its {@link ModDataComponents#VEHICLE} component, + * and its look comes from the {@code item_model} component pointing at the model the import wrote + * for that vehicle. Items are registered once at startup, so this is what lets definitions be + * added or reloaded while the game runs. + */ +public class AircraftItem extends Item { + private final String kind; + + public AircraftItem(String kind, Properties properties) { + super(properties); + this.kind = kind; + } + + /** The vehicle kind this item holds, such as {@code helicopter}. */ + public String kind() { + return kind; + } + + /** + * A stack that places {@code def}. Vehicles from the import show the item model the import + * wrote for them; item models only load with the resources, so pack vehicles keep their kind's + * own model. + */ + public static ItemStack stackOf(AircraftDefinition def) { + ItemStack stack = new ItemStack(ModItems.forKind(def.kind())); + stack.set(ModDataComponents.VEHICLE.get(), def.name()); + if (def.pack().equals(ContentSource.ORIGINAL)) { + stack.set(DataComponents.ITEM_MODEL, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, def.name())); + } + return stack; + } + + /** The definition name a stack places, or {@code null} if it is not a vehicle item or has none. */ + public static @Nullable String vehicleOf(ItemStack stack) { + return stack.getItem() instanceof AircraftItem item ? item.definitionName(stack) : null; + } + + protected @Nullable String definitionName(ItemStack stack) { + return stack.get(ModDataComponents.VEHICLE.get()); + } + + @Override + public Component getName(ItemStack stack) { + String name = definitionName(stack); + if (name == null) { + return super.getName(stack); + } + AircraftDefinition def = MCHeli.content().any(name); + // Vehicles added at runtime may bring no lang entry; their DisplayName stands in. + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + /** Who made a vehicle from a content pack. The import's vehicles have their credits in the docs. */ + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display, Consumer out, TooltipFlag flag) { + super.appendHoverText(stack, context, display, out, flag); + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + PackManifest credit = def != null ? def.credit() : null; + if (credit != null) { + out.accept((credit.author().isEmpty() + ? Component.translatable("item.mcheli.vehicle.from_pack", credit.name()) + : Component.translatable("item.mcheli.vehicle.from_pack_by", credit.name(), credit.author())) + .withStyle(ChatFormatting.GRAY)); + } + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + HitResult hit = getPlayerPOVHitResult(level, player, ClipContext.Fluid.ANY); + if (!(hit instanceof BlockHitResult blockHit) || hit.getType() != HitResult.Type.BLOCK) { + return InteractionResult.PASS; + } + + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + if (!level.isClientSide()) { + player.sendOverlayMessage(Component.translatable("message.mcheli.vehicle.unknown", String.valueOf(name))); + } + return InteractionResult.FAIL; + } + if (!level.isClientSide() && ServerConfig.isDisabled(def.name())) { + player.sendOverlayMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName())); + return InteractionResult.FAIL; + } + + AircraftEntity vehicle = ModEntities.typeFor(def).create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (vehicle == null) { + return InteractionResult.FAIL; + } + Vec3 at = blockHit.getLocation(); + vehicle.setType(def); + vehicle.snapTo(at.x, at.y, at.z, player.getYRot(), 0.0F); + if (!level.noCollision(vehicle, vehicle.getBoundingBox())) { + return InteractionResult.FAIL; + } + + if (!level.isClientSide()) { + level.addFreshEntity(vehicle); + level.gameEvent(player, GameEvent.ENTITY_PLACE, at); + stack.consume(1, player); + } + player.awardStat(Stats.ITEM_USED.get(this)); + return InteractionResult.SUCCESS; + } +} From e7797f8bf5ee5eb55b3d0cca7f17cd6c5a84b5bf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:54 +0800 Subject: [PATCH 0512/1461] Add item/ChainItem.java to the 26.1 shared sources --- .../jnhprojects/mcheli/item/ChainItem.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java new file mode 100644 index 00000000..2f18676a --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java @@ -0,0 +1,85 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.ChainEntity; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; + +import java.util.Map; +import java.util.UUID; +import java.util.WeakHashMap; + +/** + * The tow chain, {@code MCH_ItemChain}. Use it on both ends, in either order, two to sixteen + * blocks apart: the vehicle pulls, so one end must be a vehicle. Use it on either end of a chain + * to take the chain off, or on the hooked end again to let go of it. Vehicles and containers + * hand the click over here, and every step is told to the player above the hotbar. + */ +public class ChainItem extends Item { + private static final double MIN_DISTANCE = 2.0; + private static final double MAX_DISTANCE = 16.0; + + /** The first end each player has hooked, until they hook the second. Server only. */ + private static final Map HOOKED = new WeakHashMap<>(); + + public ChainItem(Properties properties) { + super(properties.stacksTo(1)); + } + + /** A click with the chain on {@code target}. */ + public static InteractionResult useOn(Entity target, Player player) { + if (!(player.level() instanceof ServerLevel level)) { + return InteractionResult.SUCCESS; + } + ChainEntity existing = ChainEntity.on(target); + if (existing != null) { + MCHeli.LOGGER.debug("Chain: {} takes the chain off {}", player.getName().getString(), target); + existing.disconnect(); + HOOKED.remove(player); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.removed")); + return InteractionResult.SUCCESS; + } + UUID firstId = HOOKED.get(player); + Entity first = firstId != null ? level.getEntity(firstId) : null; + // An end hooked far away was most likely forgotten: start over from this one. + if (first == null || first.isRemoved() || target.distanceTo(first) > MAX_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} hooks {}", player.getName().getString(), target); + HOOKED.put(player, target.getUUID()); + ModSounds.playAt(target, "chain", 1.0F, 1.0F); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.hooked", target.getDisplayName())); + return InteractionResult.SUCCESS; + } + if (first == target) { + MCHeli.LOGGER.debug("Chain: {} lets go of {}", player.getName().getString(), target); + HOOKED.remove(player); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.released")); + return InteractionResult.SUCCESS; + } + double distance = target.distanceTo(first); + if (distance < MIN_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} and {} are only {} blocks apart", first, target, distance); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.too_close", String.format("%.1f", distance))); + return InteractionResult.FAIL; + } + if (!ChainEntity.canTow(first) && !ChainEntity.canTow(target)) { + MCHeli.LOGGER.debug("Chain: neither {} nor {} can tow", first, target); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.no_vehicle")); + return InteractionResult.FAIL; + } + MCHeli.LOGGER.debug("Chain: {} chains {} and {}", player.getName().getString(), first, target); + ChainEntity chain = ChainEntity.connect(level, first, target); + HOOKED.remove(player); + Entity towed = chain.towed(); + Entity tow = chain.tow(); + player.sendOverlayMessage(Component.translatable("message.mcheli.chain.connected", + towed != null ? towed.getDisplayName() : first.getDisplayName(), tow != null ? tow.getDisplayName() : target.getDisplayName())); + return InteractionResult.SUCCESS; + } +} From ae8cb41235ec5f0240056b6162c1513bec393f83 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:55 +0800 Subject: [PATCH 0513/1461] Add item/FuelItem.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/item/FuelItem.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java new file mode 100644 index 00000000..efa8f295 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +/** + * The fuel can, {@code MCH_ItemFuel}. It holds 600 units; the durability bar shows how much is + * left. Using it refills it from the coal and charcoal in the inventory, as in the original. + * + *

A can in a fuel slot of the vehicle screen empties into the tank while the vehicle stands on + * something, as in the original. Not in the original: right-clicking a vehicle with the can pours + * up to 100 units at once, which saves opening the screen. + */ +public class FuelItem extends Item { + /** Units a full can holds, the original's maximum damage. */ + public static final int CAPACITY = 600; + /** Units poured into a vehicle per click: what the original moved per fuel slot per update. */ + private static final int POUR = 100; + private static final int PER_COAL = 100; + private static final int PER_CHARCOAL = 75; + + public FuelItem(Properties properties) { + super(properties.durability(CAPACITY)); + } + + public static int contents(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + /** Fills a vehicle's tank from the can. Server only. */ + public static InteractionResult pour(ItemStack stack, AircraftEntity vehicle, Player player) { + int added = vehicle.refuel(player.getAbilities().instabuild ? POUR : Math.min(contents(stack), POUR)); + if (added <= 0) { + return InteractionResult.FAIL; + } + if (!player.getAbilities().instabuild) { + stack.setDamageValue(stack.getDamageValue() + added); + } + vehicle.level().playSound(null, vehicle.getX(), vehicle.getY(), vehicle.getZ(), SoundEvents.BUCKET_EMPTY, SoundSource.NEUTRAL, 1.0F, 1.0F); + return InteractionResult.SUCCESS; + } + + /** Charcoal first, then coal, each one worth part of the can, until it is full. */ + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + if (!stack.isDamaged() || player.getAbilities().instabuild) { + return InteractionResult.PASS; + } + if (!level.isClientSide()) { + refill(stack, player.getInventory(), Items.CHARCOAL, PER_CHARCOAL); + refill(stack, player.getInventory(), Items.COAL, PER_COAL); + } + return InteractionResult.SUCCESS; + } + + private static void refill(ItemStack can, Inventory inventory, Item coal, int worth) { + for (int slot = 0; slot < inventory.getContainerSize() && can.isDamaged(); slot++) { + ItemStack item = inventory.getItem(slot); + while (item.is(coal) && !item.isEmpty() && can.isDamaged()) { + can.setDamageValue(Math.max(can.getDamageValue() - worth, 0)); + item.shrink(1); + } + } + } +} From c07ca29bb7bb59281c36c6f41429f883c2920aee Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:55 +0800 Subject: [PATCH 0514/1461] Add item/LauncherItem.java to the 26.1 shared sources --- .../jnhprojects/mcheli/item/LauncherItem.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java new file mode 100644 index 00000000..a83ff5bd --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; + +import java.util.function.Supplier; + +/** + * A shoulder launched missile, {@code MCH_ItemLightWeaponBase}: the FIM-92 Stinger and the + * FGM-148 Javelin. Held up it is a sight that locks onto targets; the attack key fires once + * locked. It holds one missile, and its bar shows whether it is loaded: an empty launcher reloads + * from a missile in the inventory after two seconds held but not raised. + */ +public class LauncherItem extends Item { + /** The original's durability: 0 is loaded, the maximum is empty. */ + private static final int EMPTY = 10; + + private final String weapon; + private final Supplier missile; + + /** + * @param weapon the weapon definition it fires, such as {@code fim92} + * @param missile the item it reloads from + */ + public LauncherItem(String weapon, Supplier missile, Properties properties) { + super(properties.durability(EMPTY)); + this.weapon = weapon; + this.missile = missile; + } + + public String weapon() { + return weapon; + } + + public Item missile() { + return missile.get(); + } + + public static boolean isLoaded(ItemStack stack) { + return stack.getDamageValue() < stack.getMaxDamage(); + } + + public static void setLoaded(ItemStack stack, boolean loaded) { + stack.setDamageValue(loaded ? 0 : stack.getMaxDamage()); + } + + /** Not from a vehicle seat or the GLTD, as in the original. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity) && !(player.getVehicle() instanceof GltdEntity); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResult.PASS; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.BOW; + } +} From 92d6c5870c501ae17cc68a7f49dc5f8f5c000a6e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:56 +0800 Subject: [PATCH 0515/1461] Add item/LegacyAircraftItem.java to the 26.1 shared sources --- .../mcheli/item/LegacyAircraftItem.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java new file mode 100644 index 00000000..0a286838 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * A vehicle item as 1.0.0 saved it: one item per vehicle, such as {@code mcheli:ah-64}. + * + *

These are still registered for the vehicles imported at startup, so that stacks in 1.0.0 + * worlds load instead of vanishing. They work as before, are not listed anywhere, and turn into + * the generic item of their kind ({@link AircraftItem#stackOf}) as soon as they sit in a player's + * inventory. Stacks in chests convert once a player takes them out. + */ +public final class LegacyAircraftItem extends AircraftItem { + private final String definitionName; + + public LegacyAircraftItem(String kind, String definitionName, Properties properties) { + super(kind, properties); + this.definitionName = definitionName; + } + + @Override + protected @Nullable String definitionName(ItemStack stack) { + return definitionName; + } + + @Override + public void inventoryTick(ItemStack stack, ServerLevel level, Entity owner, @Nullable EquipmentSlot slot) { + if (!(owner instanceof Player player)) { + return; + } + AircraftDefinition def = MCHeli.content().any(definitionName); + if (def == null) { + return; + } + Inventory inventory = player.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + if (inventory.getItem(i) == stack) { + inventory.setItem(i, convert(stack, def)); + return; + } + } + } + + /** The generic stack for {@code def}, keeping count, custom name and anything else set on the old one. */ + static ItemStack convert(ItemStack legacy, AircraftDefinition def) { + ItemStack converted = AircraftItem.stackOf(def); + converted.applyComponents(legacy.getComponentsPatch()); + converted.set(ModDataComponents.VEHICLE.get(), def.name()); + converted.setCount(legacy.getCount()); + return converted; + } +} From db69ed650659404b167aaad96e6ae0c86d18c449 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:56 +0800 Subject: [PATCH 0516/1461] Add item/ParachuteItem.java to the 26.1 shared sources --- .../mcheli/item/ParachuteItem.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java new file mode 100644 index 00000000..2af7b79b --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; + +/** The parachute, {@code MCH_ItemParachute}: used in mid-air, it opens above the player. */ +public class ParachuteItem extends Item { + public ParachuteItem(Properties properties) { + super(properties.stacksTo(1)); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (player.isPassenger() || player.onGround()) { + return InteractionResult.PASS; + } + if (!level.isClientSide()) { + ParachuteEntity.open(level, player, ParachuteEntity.Kind.ITEM); + player.getItemInHand(hand).consume(1, player); + } + return InteractionResult.SUCCESS; + } +} From a01b91ddbb08dc45cccd8b4a8b217d91315b2ff9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:57 +0800 Subject: [PATCH 0517/1461] Add item/PlacedEntityItem.java to the 26.1 shared sources --- .../mcheli/item/PlacedEntityItem.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java new file mode 100644 index 00000000..34c524d5 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java @@ -0,0 +1,62 @@ +package com.jnhprojects.mcheli.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; + +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Sets up a piece of equipment that lives in the world as an entity: the GLTD, the container, the + * UAV station. Like the originals' items it goes on top of the block clicked, facing the way the + * player looks, and only where it fits. + */ +public class PlacedEntityItem extends Item { + private final Supplier> type; + + public PlacedEntityItem(Supplier> type, Properties properties) { + super(properties); + this.type = type; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + Player player = context.getPlayer(); + BlockPos above = context.getClickedPos().above(); + Entity entity = type.get().create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (entity == null) { + return InteractionResult.FAIL; + } + entity.snapTo(above.getX() + 0.5, above.getY(), above.getZ() + 0.5, player != null ? player.getYRot() : 0.0F, 0.0F); + prepare(entity, player); + if (!level.noCollision(entity, entity.getBoundingBox().deflate(0.1)) || !level.getEntities(entity, entity.getBoundingBox()).isEmpty()) { + return InteractionResult.FAIL; + } + if (!level.isClientSide()) { + level.addFreshEntity(entity); + level.gameEvent(player, GameEvent.ENTITY_PLACE, entity.position()); + } + ItemStack stack = context.getItemInHand(); + if (player != null) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, player); + return InteractionResult.SUCCESS; + } + + /** Sets up the new entity before it is checked for room and spawned. */ + protected void prepare(Entity entity, @Nullable Player player) { + } +} From fef0b615b572c3599043e1cf7e89debe09d865ef Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:57 +0800 Subject: [PATCH 0518/1461] Add item/RangefinderItem.java to the 26.1 shared sources --- .../mcheli/item/RangefinderItem.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java new file mode 100644 index 00000000..6a4fb996 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; + +/** + * The laser rangefinder, {@code MCH_ItemRangeFinder}. Held up to the eye it is a scope that reads + * the range to whatever it points at; the attack key spots targets for the team, using one of + * its ten charges. Crafting it with redstone charges it again. + */ +public class RangefinderItem extends Item { + public static final int CHARGES = 10; + + public RangefinderItem(Properties properties) { + super(properties.durability(CHARGES)); + } + + /** Not from a vehicle seat: the vehicle's own sight does that job. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity); + } + + /** Charges left, from none to {@link #CHARGES}. */ + public static int charges(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResult.PASS; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.SPYGLASS; + } +} From cfa0e5b2c75f962ed0b37cffdcdf2b3af316a130 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:57 +0800 Subject: [PATCH 0519/1461] Add item/ThrowableItem.java to the 26.1 shared sources --- .../mcheli/item/ThrowableItem.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java new file mode 100644 index 00000000..df8f33f8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java @@ -0,0 +1,123 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.Direction; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.dispenser.BlockSource; +import net.minecraft.core.dispenser.DefaultDispenseItemBehavior; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Hand grenades, {@code MCH_ItemThrowable}. Like the vehicles there is one item, and the + * {@link ModDataComponents#THROWABLE} component says which definition a stack throws, so pack + * throwables need no item of their own. Held like a bow: the longer the draw, the further it goes. + */ +public class ThrowableItem extends Item { + private static final int FULL_DRAW = 35; + + public ThrowableItem(Properties properties) { + super(properties); + } + + /** A stack of {@code def}, showing the item model the import wrote for it. */ + public static ItemStack stackOf(ThrowableDefinition def) { + ItemStack stack = new ItemStack(ModItems.THROWABLE.get()); + stack.set(ModDataComponents.THROWABLE.get(), def.name()); + stack.set(DataComponents.MAX_STACK_SIZE, def.stackSize); + if (def.pack().equals(ContentSource.ORIGINAL)) { + stack.set(DataComponents.ITEM_MODEL, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, def.name())); + } + return stack; + } + + private static @Nullable ThrowableDefinition definition(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + return name != null ? MCHeli.content().throwable(name) : null; + } + + @Override + public Component getName(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + if (name == null) { + return super.getName(stack); + } + ThrowableDefinition def = MCHeli.content().throwable(name); + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (definition(player.getItemInHand(hand)) == null) { + return InteractionResult.FAIL; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.BOW; + } + + @Override + public boolean releaseUsing(ItemStack stack, Level level, LivingEntity entity, int remainingTime) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return false; + } + int drawn = Math.max(getUseDuration(stack, entity) - remainingTime, 5); + float strength = drawn <= FULL_DRAW ? drawn / 25.0F : 1.0F; + level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), SoundEvents.ARROW_SHOOT, SoundSource.PLAYERS, + 0.5F, 0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)); + if (!level.isClientSide()) { + level.addFreshEntity(ThrowableEntity.thrown(level, entity, def, strength)); + } + if (entity instanceof Player player) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, entity); + return true; + } + + /** Dispensers throw them straight out, {@code MCH_ItemThrowableDispenseBehavior}. */ + public static class DispenseBehavior extends DefaultDispenseItemBehavior { + @Override + protected ItemStack execute(BlockSource source, ItemStack stack) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return super.execute(source, stack); + } + Direction facing = source.state().getValue(DispenserBlock.FACING); + Vec3 at = source.center().add(facing.getStepX() * 0.7, facing.getStepY() * 0.7, facing.getStepZ() * 0.7); + Vec3 direction = new Vec3(facing.getStepX(), facing.getStepY() + 0.1, facing.getStepZ()); + source.level().addFreshEntity(ThrowableEntity.dispensed(source.level(), at, direction, def)); + stack.shrink(1); + return stack; + } + } +} From 440ca3a26db080642606544bdd052a552fb58b50 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:58 +0800 Subject: [PATCH 0520/1461] Add item/UavStationItem.java to the 26.1 shared sources --- .../mcheli/item/UavStationItem.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java new file mode 100644 index 00000000..7e3bc1c8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java @@ -0,0 +1,33 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station and the portable UAV controller, {@code MCH_ItemUavStation}. Both place the same + * entity; the kind decides its model and which drones it launches. + */ +public class UavStationItem extends PlacedEntityItem { + private final int kind; + + public UavStationItem(int kind, Properties properties) { + super(ModEntities.UAV_STATION, properties); + this.kind = kind; + } + + /** Squared to the nearest quarter turn, facing the player, as the original placed it. */ + @Override + protected void prepare(Entity entity, @Nullable Player player) { + if (entity instanceof UavStationEntity station) { + station.setKind(kind); + float yaw = player != null ? player.getYRot() : 0.0F; + int quarter = Math.floorMod(Math.round(Mth.wrapDegrees(yaw) / 90.0F), 4); + station.setYRot(quarter * 90.0F - 180.0F); + station.initOffset(); + } + } +} From 0d8ecad29098b56a5c1f18314cc8f72078da1abf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:58 +0800 Subject: [PATCH 0521/1461] Add item/VehicleIngredient.java to the 26.1 shared sources --- .../mcheli/item/VehicleIngredient.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java new file mode 100644 index 00000000..c2619049 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemStackTemplate; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Matches one vehicle, such as an AH-6 in the recipe that turns it into an AH-6X. Every vehicle of + * a kind shares an item, so a plain item ingredient would accept any of them. In JSON: + *

{"type": "mcheli:vehicle", "vehicle": "ah-6"}
+ * with the type also under {@code neoforge:ingredient_type} and {@code fabric:type}, since each + * loader names custom ingredients its own way. + * + *

This is the loader-independent half; each loader module wraps it in its own ingredient class. + */ +public final class VehicleIngredient { + public static final Identifier ID = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "vehicle"); + + private VehicleIngredient() { + } + + /** The JSON fields, given a way to build the loader's ingredient from a vehicle name. */ + public static MapCodec codec(Function create, Function vehicle) { + return Codec.STRING.fieldOf("vehicle").xmap(create, vehicle); + } + + public static StreamCodec streamCodec(Function create, Function vehicle) { + return ByteBufCodecs.STRING_UTF8.map(create, vehicle).cast(); + } + + /** The items a vehicle can be: the generic item of each kind. */ + @SuppressWarnings("deprecation") + public static HolderSet kindItems() { + return HolderSet.direct(Item::builtInRegistryHolder, + ModItems.HELICOPTER.get(), ModItems.PLANE.get(), ModItems.TANK.get(), ModItems.VEHICLE.get()); + } + + public static Stream> items() { + return kindItems().stream(); + } + + public static boolean matches(String vehicle, @Nullable ItemStack stack) { + return stack != null && vehicle.equals(AircraftItem.vehicleOf(stack)); + } + + /** What recipe books show: the vehicle itself, or nothing while it is not loaded. */ + public static SlotDisplay display(String vehicle) { + AircraftDefinition def = MCHeli.content().any(vehicle); + return def != null ? new SlotDisplay.ItemStackSlotDisplay(ItemStackTemplate.fromNonEmptyStack(AircraftItem.stackOf(def))) + : SlotDisplay.Empty.INSTANCE; + } +} From b04a7f3ed97b4c67c2d666cec4deee98f7f8641a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:59 +0800 Subject: [PATCH 0522/1461] Add item/WrenchItem.java to the 26.1 shared sources --- .../jnhprojects/mcheli/item/WrenchItem.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java new file mode 100644 index 00000000..8ad8f03f --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java @@ -0,0 +1,42 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +/** + * The wrench, {@code MCH_ItemWrench}. Right-clicking a damaged vehicle repairs 10 hit points, once + * a second, at one point of the wrench's durability. Sneaking switches the vehicle to its next + * paint scheme. + * + *

The original repaired while the use key was held; a click per repair works the same with + * today's item use, which stops as soon as the cursor leaves the hull. + */ +public class WrenchItem extends Item { + /** An iron tool's durability, the original's tool material. */ + private static final int DURABILITY = 250; + private static final int REPAIR = 10; + private static final int COOLDOWN = 20; + + public WrenchItem(Properties properties) { + super(properties.durability(DURABILITY)); + } + + /** Server only. */ + public static InteractionResult useOn(ItemStack stack, AircraftEntity vehicle, Player player, InteractionHand hand) { + if (player.isSecondaryUseActive()) { + return vehicle.nextTexture() ? InteractionResult.SUCCESS : InteractionResult.FAIL; + } + if (player.getCooldowns().isOnCooldown(stack) || !vehicle.repair(REPAIR)) { + return InteractionResult.FAIL; + } + player.getCooldowns().addCooldown(stack, COOLDOWN); + stack.hurtAndBreak(1, player, hand); + ModSounds.playAt(vehicle, "wrench", 1.0F, 0.9F + vehicle.getRandom().nextFloat() * 0.2F); + return InteractionResult.SUCCESS; + } +} From 81584c4f285012c59575c2f22d99499debb22b56 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:02:59 +0800 Subject: [PATCH 0523/1461] Add menu/DraftingTableMenu.java to the 26.1 shared sources --- .../mcheli/menu/DraftingTableMenu.java | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java new file mode 100644 index 00000000..ec1b4d89 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java @@ -0,0 +1,148 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.ResultContainer; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; + +import java.util.List; + +/** + * The drafting table, {@code MCH_DraftingTableGuiContainer}: the player's inventory and one output + * slot. The screen browses the MCHeli recipes in the player's recipe book and asks for one by its + * display id; the server finds the recipe, takes the ingredients from the inventory and puts the + * result in the output slot. + */ +public class DraftingTableMenu extends AbstractContainerMenu { + public static final int OUTPUT_X = 178; + public static final int OUTPUT_Y = 90; + + private final ContainerLevelAccess access; + private final ResultContainer output = new ResultContainer(); + private final int outputSlot; + + /** The client side, from the open screen packet. */ + public DraftingTableMenu(int containerId, Inventory inventory) { + this(containerId, inventory, ContainerLevelAccess.NULL); + } + + public DraftingTableMenu(int containerId, Inventory inventory, ContainerLevelAccess access) { + super(ModMenus.DRAFTING_TABLE.get(), containerId); + this.access = access; + addStandardInventorySlots(inventory, 30, 140); + outputSlot = slots.size(); + addSlot(new Slot(output, 0, OUTPUT_X, OUTPUT_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + } + + /** + * {@code createRecipeItem}: makes one of the recipe behind {@code id}, if it is ours, the output + * slot is free and the player carries the ingredients. Creative players need neither. + */ + public void craft(ServerPlayer player, RecipeDisplayId id) { + boolean creative = player.getAbilities().instabuild; + Slot slot = slots.get(outputSlot); + if (slot.hasItem() && !creative) { + return; + } + RecipeManager.ServerDisplayInfo info = player.level().getServer().getRecipeManager().getRecipeFromDisplay(id); + if (info == null) { + return; + } + RecipeHolder holder = info.parent(); + if (!holder.id().identifier().getNamespace().equals(MCHeli.MOD_ID) || !(holder.value() instanceof CraftingRecipe recipe)) { + return; + } + List ingredients = recipe.placementInfo().ingredients(); + if (!creative && !take(player.getInventory(), ingredients, true)) { + return; + } + if (!creative) { + take(player.getInventory(), ingredients, false); + } + slot.set(recipe.assemble(CraftingInput.EMPTY)); + broadcastChanges(); + } + + /** + * Finds, and unless {@code dryRun} removes, one item per ingredient. A stack serves several + * ingredients as far as its count goes. + */ + private static boolean take(Inventory inventory, List ingredients, boolean dryRun) { + int[] used = new int[inventory.getContainerSize()]; + for (Ingredient ingredient : ingredients) { + int found = -1; + for (int i = 0; i < inventory.getContainerSize() && found < 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && stack.getCount() > used[i] && ingredient.test(stack)) { + found = i; + } + } + if (found < 0) { + return false; + } + used[found]++; + } + if (!dryRun) { + for (int i = 0; i < used.length; i++) { + if (used[i] > 0) { + inventory.removeItem(i, used[i]); + } + } + } + return true; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(access, player, ModBlocks.DRAFTING_TABLE.get()); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + if (index != outputSlot) { + return ItemStack.EMPTY; + } + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (!moveItemStackTo(stack, 0, outputSlot, true)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** What is left in the output goes back to the player, as in the original. */ + @Override + public void removed(Player player) { + super.removed(player); + if (!player.level().isClientSide()) { + clearContainer(player, output); + } + } +} From 913aba36f1852d0c725b371f7b98e633a5d66253 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:00 +0800 Subject: [PATCH 0524/1461] Add menu/UavStationMenu.java to the 26.1 shared sources --- .../mcheli/menu/UavStationMenu.java | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java new file mode 100644 index 00000000..4c5f7bd8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java @@ -0,0 +1,154 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_ContainerUavStation}: one slot for the drone to put up, the + * launch offset with its -10, -1, +1 and +10 buttons per axis, and Continue to take the last drone + * again. The player sits at the station while it is open, so the client finds it as their vehicle. + */ +public class UavStationMenu extends AbstractContainerMenu { + /** Button ids: axis times four plus the step, then Continue. */ + public static final int[] STEPS = {-10, -1, 1, 10}; + public static final int BUTTON_CONTINUE = 12; + + private static final int DATA_SIZE = 4; + private static final int DATA_CAN_CONTINUE = 3; + + private final @Nullable UavStationEntity station; + private final ContainerData data; + + /** The client side, from the open screen packet. */ + public UavStationMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof UavStationEntity s ? s : null, + new SimpleContainer(1), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public UavStationMenu(int containerId, Inventory inventory, UavStationEntity station) { + this(containerId, inventory, station, station.slot(), new ServerData(station)); + } + + private UavStationMenu(int containerId, Inventory inventory, @Nullable UavStationEntity station, Container slot, ContainerData data) { + super(ModMenus.UAV_STATION.get(), containerId); + this.station = station; + this.data = data; + checkContainerSize(slot, 1); + addSlot(new Slot(slot, 0, 20, 20) { + @Override + public boolean mayPlace(ItemStack stack) { + return AircraftItem.vehicleOf(stack) != null; + } + + @Override + public int getMaxStackSize() { + return 1; + } + }); + addStandardInventorySlots(inventory, 8, 84); + addDataSlots(data); + } + + public @Nullable UavStationEntity station() { + return station; + } + + /** The launch offset along {@code axis} (0 x, 1 y, 2 z), as the server last reported it. */ + public int offset(int axis) { + return (short) data.get(axis); + } + + public boolean canContinue() { + return data.get(DATA_CAN_CONTINUE) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (station == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CONTINUE) { + return station.continueControl(server); + } + if (id < 0 || id >= 12) { + return false; + } + int[] pos = {station.offset().getX(), station.offset().getY(), station.offset().getZ()}; + pos[id / 4] += STEPS[id % 4]; + station.setOffset(new BlockPos(pos[0], pos[1], pos[2])); + return true; + } + + @Override + public boolean stillValid(Player player) { + return station != null && station.isAlive() && player.getVehicle() == station; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (index == 0) { + if (!moveItemStackTo(stack, 1, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (AircraftItem.vehicleOf(stack) == null || !moveItemStackTo(stack, 0, 1, false)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** Whatever is left in the slot goes back to the player, as with a crafting table. */ + @Override + public void removed(Player player) { + super.removed(player); + if (station != null && !player.level().isClientSide()) { + clearContainer(player, station.slot()); + } + } + + private record ServerData(UavStationEntity station) implements ContainerData { + @Override + public int get(int index) { + BlockPos o = station.offset(); + return switch (index) { + case 0 -> o.getX(); + case 1 -> o.getY(); + case 2 -> o.getZ(); + default -> station.lastUav() != null ? 1 : 0; + }; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } +} From 027996e46d83a3b7aab905ba5d3bbf89e73d9c01 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:00 +0800 Subject: [PATCH 0525/1461] Add menu/VehicleMenu.java to the 26.1 shared sources --- .../jnhprojects/mcheli/menu/VehicleMenu.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java new file mode 100644 index 00000000..b2dba1e4 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java @@ -0,0 +1,217 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.weapon.Resupply; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGuiContainer}: fuel slots, a parachute slot per ejection + * seat, reloading the weapons from the crew member's own items and the way into the cargo chest. + * + *

Only crew open it, so the client finds the vehicle as the one its player rides. The weapon + * rounds and which weapons can be reloaded right now come from the server through data slots, + * since a client only hears about the ammunition of the seat it sits in. + */ +public class VehicleMenu extends AbstractContainerMenu { + /** Weapons the screen can show; more than any vehicle in the original has. */ + public static final int MAX_WEAPONS = 32; + /** Button ids from zero up reload that weapon; this one opens the cargo chest. */ + public static final int BUTTON_CARGO = 1000; + + public static final int FUEL_X = 8; + public static final int FUEL_Y = 30; + public static final int PARACHUTE_X = 8; + public static final int PARACHUTE_Y = 66; + public static final int INVENTORY_Y = 102; + + private static final int DATA_RELOADABLE = MAX_WEAPONS; + private static final int DATA_SIZE = MAX_WEAPONS + 2; + + private final @Nullable AircraftEntity vehicle; + private final Container equipment; + private final ContainerData data; + private final int parachuteSlots; + private boolean supplied; + + /** The client side, from the open screen packet. */ + public VehicleMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof AircraftEntity v ? v : null, + new SimpleContainer(VehicleInventory.EQUIPMENT_SLOTS), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public VehicleMenu(int containerId, Inventory inventory, AircraftEntity vehicle) { + this(containerId, inventory, vehicle, vehicle.inventory().equipment(), new ServerData(vehicle, inventory.player)); + } + + private VehicleMenu(int containerId, Inventory inventory, @Nullable AircraftEntity vehicle, Container equipment, ContainerData data) { + super(ModMenus.VEHICLE.get(), containerId); + this.vehicle = vehicle; + this.equipment = equipment; + this.data = data; + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + parachuteSlots = def != null ? VehicleInventory.parachuteSlots(def) : 0; + checkContainerSize(equipment, VehicleInventory.EQUIPMENT_SLOTS); + + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + addSlot(new Slot(equipment, i, FUEL_X + i * 18, FUEL_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() instanceof FuelItem; + } + }); + } + for (int i = 0; i < VehicleInventory.PARACHUTE_SLOTS; i++) { + boolean active = i < parachuteSlots; + addSlot(new Slot(equipment, VehicleInventory.FUEL_SLOTS + i, PARACHUTE_X + i * 18, PARACHUTE_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return active && stack.getItem() instanceof ParachuteItem; + } + + @Override + public boolean isActive() { + return active; + } + }); + } + addStandardInventorySlots(inventory, 8, INVENTORY_Y); + addDataSlots(data); + } + + public @Nullable AircraftEntity vehicle() { + return vehicle; + } + + /** Parachute slots in use: one per ejection seat. */ + public int parachuteSlots() { + return parachuteSlots; + } + + /** Magazine and reserve of a weapon together, as the server last reported them. */ + public int rounds(int weapon) { + return weapon >= 0 && weapon < MAX_WEAPONS ? data.get(weapon) : 0; + } + + /** Whether the server would reload this weapon now: the vehicle is down and the items are there. */ + public boolean canReload(int weapon) { + if (weapon < 0 || weapon >= MAX_WEAPONS) { + return false; + } + int bits = (data.get(DATA_RELOADABLE) & 0xFFFF) | (data.get(DATA_RELOADABLE + 1) & 0xFFFF) << 16; + return (bits & 1 << weapon) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (vehicle == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CARGO) { + return vehicle.openCargo(server); + } + if (id >= 0 && id < MAX_WEAPONS && Resupply.supply(server, vehicle, id)) { + supplied = true; + return true; + } + return false; + } + + @Override + public boolean stillValid(Player player) { + return vehicle != null && equipment.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + int equipmentEnd = VehicleInventory.EQUIPMENT_SLOTS; + if (index < equipmentEnd) { + if (!moveItemStackTo(stack, equipmentEnd, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof FuelItem) { + if (!moveItemStackTo(stack, 0, VehicleInventory.FUEL_SLOTS, false)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof ParachuteItem && parachuteSlots > 0) { + int first = VehicleInventory.FUEL_SLOTS; + if (!moveItemStackTo(stack, first, first + parachuteSlots, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** As in the original, closing the screen after a resupply loads every magazine from its reserve. */ + @Override + public void removed(Player player) { + super.removed(player); + if (supplied && vehicle != null && vehicle.weapons() != null && !player.level().isClientSide()) { + vehicle.weapons().reloadAll(); + } + } + + /** Rounds per weapon and the reload bits, worked out on the server whenever the menu syncs. */ + private record ServerData(AircraftEntity vehicle, Player player) implements ContainerData { + @Override + public int get(int index) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons == null) { + return 0; + } + if (index < MAX_WEAPONS) { + WeaponSet set = weapons.set(index); + // Data slots travel as shorts. + return set != null ? Math.min(set.ammo() + set.restAmmo(), Short.MAX_VALUE) : 0; + } + int bits = 0; + if (player instanceof ServerPlayer server) { + for (int i = 0; i < Math.min(weapons.sets().size(), MAX_WEAPONS); i++) { + if (Resupply.canSupply(server, vehicle, i)) { + bits |= 1 << i; + } + } + } + return index == DATA_RELOADABLE ? bits & 0xFFFF : bits >>> 16; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } +} From f5cfd4463ade3e0f857ef69ee58399fa45c1ef5f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:00 +0800 Subject: [PATCH 0526/1461] Add network/BoardVehiclePacket.java to the 26.1 shared sources --- .../mcheli/network/BoardVehiclePacket.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java new file mode 100644 index 00000000..ba56ca3e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.Vec3; + +/** + * Client to server: the player right-clicked a vehicle's hull outside its main hit box. The + * server repeats the reach check against the hull boxes, then treats it as an ordinary click. + * + * @param location where on the hull the click landed, in world coordinates + */ +public record BoardVehiclePacket(int entityId, Vec3 location) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("board_vehicle"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(BoardVehiclePacket::encode, BoardVehiclePacket::decode); + + /** The same slack vanilla allows on top of the player's reach for a click on an entity. */ + private static final double REACH_SLACK = 3.0; + + public static void encode(BoardVehiclePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeDouble(msg.location.x); + buf.writeDouble(msg.location.y); + buf.writeDouble(msg.location.z); + } + + public static BoardVehiclePacket decode(FriendlyByteBuf buf) { + return new BoardVehiclePacket(buf.readVarInt(), new Vec3(buf.readDouble(), buf.readDouble(), buf.readDouble())); + } + + public static void handle(BoardVehiclePacket msg, ServerPlayer sender) { + if (sender == null || !(sender.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) || vehicle.isRemoved()) { + return; + } + Vec3 eye = sender.getEyePosition(); + double reach = sender.entityInteractionRange() + REACH_SLACK; + // The click must land on the hull, and the hull must be within reach. + if (eye.distanceToSqr(msg.location) > reach * reach) { + return; + } + Vec3 through = eye.add(msg.location.subtract(eye).scale(1.05)); + boolean onHull = vehicle.clipHitBoxes(eye, through, 0.5).isPresent(); + vehicle.consumeHitBoxFactor(); + if (onHull) { + vehicle.interact(sender, InteractionHand.MAIN_HAND, msg.location.subtract(vehicle.position())); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 8cd338f6539a07a875dedcc981d42a40bdc64f6d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:01 +0800 Subject: [PATCH 0527/1461] Add network/CrewActionPacket.java to the 26.1 shared sources --- .../mcheli/network/CrewActionPacket.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java new file mode 100644 index 00000000..1b565e21 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java @@ -0,0 +1,164 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A one-off request from a crew member: lower the gear, fold the wings, change seats and so on. + * + *

The client only asks; the server checks that the sender sits in that vehicle, in a seat + * allowed to do it, and then applies the same conditions the original checked before switching. + * The result reaches every client through synched data. + */ +public record CrewActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("crew_action"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(CrewActionPacket::encode, CrewActionPacket::decode); + + public enum Action { + /** Raise or lower the landing gear. Pilot only. */ + LANDING_GEAR, + /** Fold or unfold hatches, wings or swept wings. Pilot only. */ + FOLD, + /** Tilt the nozzles for vertical flight. Pilot only. */ + VTOL, + /** Eject from seat zero or one. */ + EJECT, + /** Move to the next free crew seat. */ + SWITCH_SEAT, + /** Load nearby cargo onto the racks, or drive onto a carrier's rack. Pilot only. */ + RACK_LOAD, + /** Drop the last cargo, or leave the rack this vehicle rides on. Pilot only. */ + RACK_UNLOAD, + /** Select the next weapon this seat may use. */ + NEXT_WEAPON, + /** Select the previous weapon this seat may use. */ + PREVIOUS_WEAPON, + /** Cycle the fire mode of the selected weapon. */ + WEAPON_MODE, + /** Release flares. Pilot and first crew seat. */ + FLARE, + /** Open the vehicle screen: fuel and parachute slots, reloading, the way to the cargo chest. */ + VEHICLE_SCREEN, + /** Hand the controls to the autopilot and man the weapons, or take them back. Pilot only. */ + GUNNER_MODE, + /** Turn night vision on for the sender, in vehicles with {@code EnableNightVision}. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF, + /** Let the troops down: ropes from a hover, a parachute drop, or out on the ground. Pilot only. */ + DROP_CREW, + /** Slide down a rope or jump with a parachute. Crew other than the pilot. */ + JUMP_OUT, + /** Turn the search light on or off. Pilot and first crew seat. */ + SEARCH_LIGHT + } + + public static void encode(CrewActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static CrewActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown crew action " + ordinal); + } + return new CrewActionPacket(entityId, actions[ordinal]); + } + + public static void handle(CrewActionPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + boolean pilot = vehicle.getControllingPassenger() == sender; + switch (msg.action) { + case LANDING_GEAR -> { + if (pilot) { + vehicle.toggleLandingGear(); + } + } + case FOLD -> { + if (pilot) { + vehicle.toggleFold(); + } + } + case VTOL -> { + if (pilot && vehicle instanceof PlaneEntity plane) { + plane.toggleVtol(); + } + } + case GUNNER_MODE -> { + if (pilot) { + vehicle.setGunnerMode(!vehicle.isGunnerMode()); + } + } + case EJECT -> vehicle.eject(sender); + case SWITCH_SEAT -> vehicle.switchSeat(sender); + case RACK_LOAD -> { + if (pilot) { + if (!vehicle.boardNearbyRack()) { + vehicle.loadNearbyCargo(); + } + } + } + case RACK_UNLOAD -> { + if (pilot) { + if (vehicle.isPassenger()) { + vehicle.leaveRack(); + } else { + vehicle.unloadCargo(); + } + } + } + case NEXT_WEAPON, PREVIOUS_WEAPON -> { + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons != null && !vehicle.isDestroyed()) { + int next = weapons.next(seat, msg.action == Action.NEXT_WEAPON ? 1 : -1); + if (next >= 0) { + weapons.switchTo(sender, seat, next); + } + } + } + case WEAPON_MODE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.switchMode(vehicle.seatOf(sender)); + } + } + case FLARE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null && vehicle.seatOf(sender) <= 1) { + weapons.flares.use(); + } + } + case VEHICLE_SCREEN -> vehicle.openScreen(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> vehicle.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + case DROP_CREW -> { + if (pilot && !vehicle.dropCrew()) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + case SEARCH_LIGHT -> vehicle.toggleSearchLight(sender); + case JUMP_OUT -> { + if (!pilot && !vehicle.jumpOut(sender)) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 672e2074f8cdc38f45242fc6d307b8acea92fc1c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:01 +0800 Subject: [PATCH 0528/1461] Add network/DraftingTablePacket.java to the 26.1 shared sources --- .../mcheli/network/DraftingTablePacket.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java new file mode 100644 index 00000000..46294aa3 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; + +/** + * The Create button of the drafting table, {@code MCH_DraftingTableCreatePacket}: make one of the + * recipe the player's recipe book knows by this display id. + */ +public record DraftingTablePacket(RecipeDisplayId recipe) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("drafting_table"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(DraftingTablePacket::encode, DraftingTablePacket::decode); + + public static void encode(DraftingTablePacket msg, FriendlyByteBuf buf) { + RecipeDisplayId.STREAM_CODEC.encode(buf, msg.recipe); + } + + public static DraftingTablePacket decode(FriendlyByteBuf buf) { + return new DraftingTablePacket(RecipeDisplayId.STREAM_CODEC.decode(buf)); + } + + public static void handle(DraftingTablePacket msg, ServerPlayer sender) { + if (sender != null && sender.containerMenu instanceof DraftingTableMenu menu && menu.stillValid(sender)) { + menu.craft(sender, msg.recipe); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 57d802fd580cd7b54b8e73c0c390491e3d7d7810 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:02 +0800 Subject: [PATCH 0529/1461] Add network/EquipmentActionPacket.java to the 26.1 shared sources --- .../mcheli/network/EquipmentActionPacket.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java new file mode 100644 index 00000000..76090f2e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A request from a player using a piece of equipment they sit at, such as the GLTD. Like + * {@link CrewActionPacket} the client only asks; the equipment checks that the sender is the one + * using it. + */ +public record EquipmentActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("equipment_action"); + public static final StreamCodec STREAM_CODEC = + StreamCodec.ofMember(EquipmentActionPacket::encode, EquipmentActionPacket::decode); + + public enum Action { + /** Mark the spot under the sight and call in the A-10. */ + CALL_STRIKE, + /** Turn night vision on for the sender. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF + } + + public static void encode(EquipmentActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static EquipmentActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown equipment action " + ordinal); + } + return new EquipmentActionPacket(entityId, actions[ordinal]); + } + + public static void handle(EquipmentActionPacket msg, ServerPlayer sender) { + if (sender == null || sender.getVehicle() == null || sender.getVehicle().getId() != msg.entityId) { + return; + } + if (sender.getVehicle() instanceof GltdEntity gltd) { + switch (msg.action) { + case CALL_STRIKE -> gltd.callStrike(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> gltd.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 934ee8e1e46730d495632490834b1bcc7d05c253 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:02 +0800 Subject: [PATCH 0530/1461] Add network/ExplosionEffectPacket.java to the 26.1 shared sources --- .../mcheli/network/ExplosionEffectPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java new file mode 100644 index 00000000..279b1511 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.ExplosionEffect; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to nearby players: draw an MCHeli explosion here. The original's + * {@code MCH_PacketEffectExplosion}, plus a flag for the bigger fireball of a crashing vehicle. + */ +public record ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater, boolean crash) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("explosion_effect"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(ExplosionEffectPacket::encode, ExplosionEffectPacket::decode); + + public ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater) { + this(x, y, z, size, inWater, false); + } + + public static void encode(ExplosionEffectPacket msg, FriendlyByteBuf buf) { + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.size); + buf.writeBoolean(msg.inWater); + buf.writeBoolean(msg.crash); + } + + public static ExplosionEffectPacket decode(FriendlyByteBuf buf) { + return new ExplosionEffectPacket(buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readBoolean(), + buf.readBoolean()); + } + + public static void handle(ExplosionEffectPacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player == null || !Float.isFinite(msg.size)) { + return; + } + float size = Math.min(msg.size, 50.0F); + if (msg.crash) { + ExplosionEffect.crash(player.level(), msg.x, msg.y, msg.z, Math.min(size, ExplosionEffect.MAX_CRASH_SIZE)); + } else { + ExplosionEffect.play(player.level(), msg.x, msg.y, msg.z, size, msg.inWater); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 13c0e57fdfb51479e7a3654cdf20ed9ae24c60be Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:03 +0800 Subject: [PATCH 0531/1461] Add network/FireWeaponPacket.java to the 26.1 shared sources --- .../mcheli/network/FireWeaponPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java new file mode 100644 index 00000000..0056dcc0 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.ServerFiring; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Client to server: the crew member's selected weapon fired on their side, so fire it for real. + * The client already checked readiness, ammunition and lock; the server checks again with its own + * timers and only trusts the two option values, a locked entity and a fire mode. + * + * @param weapon the weapon the client fired, ignored unless it is still the seat's selection + */ +public record FireWeaponPacket(int entityId, int weapon, int option1, int option2) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("fire_weapon"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(FireWeaponPacket::encode, FireWeaponPacket::decode); + + public static void encode(FireWeaponPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.option1); + buf.writeVarInt(msg.option2); + } + + public static FireWeaponPacket decode(FriendlyByteBuf buf) { + return new FireWeaponPacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(FireWeaponPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons == null || weapons.selected(seat) != msg.weapon || weapons.operatorSeat(msg.weapon) != seat) { + return; + } + Shot shot = new Shot(vehicle, sender); + shot.option1 = msg.option1; + shot.option2 = msg.option2; + weapons.use(sender, seat, shot, ServerFiring.INSTANCE); + } + + @Override + public Type type() { + return TYPE; + } +} From de8c56e97b75d105c4715767afcc3ca1f48afca3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:03 +0800 Subject: [PATCH 0532/1461] Add network/LauncherPacket.java to the 26.1 shared sources --- .../mcheli/network/LauncherPacket.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java new file mode 100644 index 00000000..d3f204c7 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java @@ -0,0 +1,119 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +/** + * The player with a launcher in hand, {@code MCH_PacketLightWeaponPlayerControl}: fire at the + * locked target, or finish a reload. The client locks and times the reload; the server checks + * the launcher, the target and the missile in the inventory. + * + * @param target the locked entity, for {@link Action#FIRE} + * @param mode the Javelin's attack mode: 0 direct, 1 top attack + */ +public record LauncherPacket(Action action, int target, int mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("launcher"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LauncherPacket::encode, LauncherPacket::decode); + + /** Locks further than this are refused, with some slack over the lock range for lag. */ + private static final double MAX_TARGET_DISTANCE = 260.0; + + public enum Action { + FIRE, + RELOAD + } + + public static void encode(LauncherPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeVarInt(msg.target); + buf.writeVarInt(msg.mode); + } + + public static LauncherPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown launcher action " + ordinal); + } + return new LauncherPacket(actions[ordinal], buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(LauncherPacket msg, ServerPlayer sender) { + ItemStack stack = sender == null ? ItemStack.EMPTY : sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof LauncherItem launcher) || !LauncherItem.canUse(sender)) { + return; + } + switch (msg.action) { + case FIRE -> fire(sender, stack, launcher, msg.target, msg.mode); + case RELOAD -> reload(sender, stack, launcher); + } + } + + private static void fire(ServerPlayer player, ItemStack stack, LauncherItem launcher, int targetId, int mode) { + WeaponDefinition def = MCHeli.content().weapon(launcher.weapon()); + ServerLevel level = player.level(); + Entity target = level.getEntity(targetId); + if (def == null || !LauncherItem.isLoaded(stack) || target == null || target.isRemoved() + || target.distanceToSqr(player) > MAX_TARGET_DISTANCE * MAX_TARGET_DISTANCE) { + return; + } + ProjectileEntity missile = def.type == WeaponType.AT_MISSILE + ? ModEntities.AT_MISSILE.get().create(level, EntitySpawnReason.TRIGGERED) + : ModEntities.AA_MISSILE.get().create(level, EntitySpawnReason.TRIGGERED); + if (missile == null) { + return; + } + float yaw = player.getYRot(); + float pitch = player.getXRot(); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + Vec3 at = player.getEyePosition().add(dir); + missile.launch(at.x, at.y, at.z, dir.x, dir.y, dir.z, yaw, pitch, def.acceleration); + missile.setWeapon(launcher.weapon()); + missile.setParameters(def, player, player); + missile.setTarget(target); + if (missile instanceof ATMissileEntity atMissile) { + atMissile.guidanceType = Math.clamp(mode, 0, Math.max(def.modeNum - 1, 0)); + } + level.addFreshEntity(missile); + if (def.sound != null) { + ModSounds.playAt(player, def.sound, def.soundVolume, def.soundPitch); + } + LauncherItem.setLoaded(stack, false); + } + + private static void reload(ServerPlayer player, ItemStack stack, LauncherItem launcher) { + if (LauncherItem.isLoaded(stack)) { + return; + } + if (!player.getAbilities().instabuild) { + int slot = player.getInventory().findSlotMatchingItem(new ItemStack(launcher.missile())); + if (slot < 0) { + return; + } + player.getInventory().removeItem(slot, 1); + } + LauncherItem.setLoaded(stack, true); + } + + @Override + public Type type() { + return TYPE; + } +} From 2686cf041fca1ba062804eeae2a44a2dd92805ec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:03 +0800 Subject: [PATCH 0533/1461] Add network/LockWarningPacket.java to the 26.1 shared sources --- .../mcheli/network/LockWarningPacket.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java new file mode 100644 index 00000000..97d2cbf4 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java @@ -0,0 +1,44 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +/** + * Client to server: this player is locking on to an entity. If it is a vehicle with flares, or a + * crew member of one, its crew hears the warning. The original's {@code MCH_PacketNotifyLock}. + */ +public record LockWarningPacket(int targetId) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("lock_warning"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LockWarningPacket::encode, LockWarningPacket::decode); + + public static void encode(LockWarningPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.targetId); + } + + public static LockWarningPacket decode(FriendlyByteBuf buf) { + return new LockWarningPacket(buf.readVarInt()); + } + + public static void handle(LockWarningPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity)) { + return; + } + Entity target = sender.level().getEntity(msg.targetId); + if (target != null && target.distanceToSqr(sender) < 400.0 * 400.0) { + AircraftEntity vehicle = target instanceof AircraftEntity a ? a : target.getVehicle() instanceof AircraftEntity a ? a : null; + if (vehicle != null) { + vehicle.warnLocked(); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 8e0ca5e66402e44f59b633bd4ee001dddc9d38b9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:04 +0800 Subject: [PATCH 0534/1461] Add network/MarkerPacket.java to the 26.1 shared sources --- .../mcheli/network/MarkerPacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java new file mode 100644 index 00000000..1de0bfc3 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to client: the team spotted these entities for {@code ticks}, or marked {@code point}, + * {@code MCH_PacketNotifySpotedEntity} and {@code MCH_PacketNotifyMarkPoint}. Only the spotter and + * their teammates get it. + */ +public record MarkerPacket(int[] entities, int ticks, BlockPos point) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("marker"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(MarkerPacket::encode, MarkerPacket::decode); + /** Spotting sends no point; this stands in for none. */ + private static final BlockPos NO_POINT = new BlockPos(0, Integer.MIN_VALUE, 0); + private static final int MAX_ENTITIES = 256; + + public static MarkerPacket spotted(int[] entities, int ticks) { + return new MarkerPacket(entities, ticks, NO_POINT); + } + + public static MarkerPacket point(BlockPos point) { + return new MarkerPacket(new int[0], 0, point); + } + + public static void encode(MarkerPacket msg, FriendlyByteBuf buf) { + buf.writeVarIntArray(msg.entities); + buf.writeVarInt(msg.ticks); + buf.writeBlockPos(msg.point); + } + + public static MarkerPacket decode(FriendlyByteBuf buf) { + return new MarkerPacket(buf.readVarIntArray(MAX_ENTITIES), buf.readVarInt(), buf.readBlockPos()); + } + + public static void handle(MarkerPacket msg) { + if (msg.entities.length > 0) { + Markers.spotted(msg.entities, msg.ticks); + } + if (!msg.point.equals(NO_POINT)) { + Markers.point(msg.point); + } + } + + @Override + public Type type() { + return TYPE; + } +} From cb9a0e2017e004b5ce2337541982335507c8ce6a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:04 +0800 Subject: [PATCH 0535/1461] Add network/ModNetwork.java to the 26.1 shared sources --- .../mcheli/network/ModNetwork.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java new file mode 100644 index 00000000..26065705 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java @@ -0,0 +1,90 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Our packets. Each one is a custom payload with its own id and the same encoding on every loader, + * so a client on one loader can talk to a server on another. The loader module registers the + * lists below with its own networking API; handlers always run on the main thread. + */ +public final class ModNetwork { + /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ + public static final int PROTOCOL_VERSION = 11; + + /** Client to server. The handler gets the sending player. */ + public static final List> SERVERBOUND = List.of( + new Serverbound<>(PilotStatePacket.TYPE, PilotStatePacket.STREAM_CODEC, PilotStatePacket::handle), + new Serverbound<>(CrewActionPacket.TYPE, CrewActionPacket.STREAM_CODEC, CrewActionPacket::handle), + new Serverbound<>(EquipmentActionPacket.TYPE, EquipmentActionPacket.STREAM_CODEC, EquipmentActionPacket::handle), + new Serverbound<>(SpotPacket.TYPE, SpotPacket.STREAM_CODEC, SpotPacket::handle), + new Serverbound<>(LauncherPacket.TYPE, LauncherPacket.STREAM_CODEC, LauncherPacket::handle), + new Serverbound<>(DraftingTablePacket.TYPE, DraftingTablePacket.STREAM_CODEC, DraftingTablePacket::handle), + new Serverbound<>(TeamPacket.TYPE, TeamPacket.STREAM_CODEC, TeamPacket::handle), + new Serverbound<>(TestFlightPacket.TYPE, TestFlightPacket.STREAM_CODEC, TestFlightPacket::handle), + new Serverbound<>(FireWeaponPacket.TYPE, FireWeaponPacket.STREAM_CODEC, FireWeaponPacket::handle), + new Serverbound<>(BoardVehiclePacket.TYPE, BoardVehiclePacket.STREAM_CODEC, BoardVehiclePacket::handle), + new Serverbound<>(LockWarningPacket.TYPE, LockWarningPacket.STREAM_CODEC, LockWarningPacket::handle), + new Serverbound<>(PackRequestPacket.TYPE, PackRequestPacket.STREAM_CODEC, PackRequestPacket::handle), + new Serverbound<>(PackSubmitPacket.TYPE, PackSubmitPacket.STREAM_CODEC, PackSubmitPacket::handle)); + + /** Server to client. Handlers touch client classes, so they only ever run on a client. */ + public static final List> CLIENTBOUND = List.of( + new Clientbound<>(WeaponStatePacket.TYPE, WeaponStatePacket.STREAM_CODEC, WeaponStatePacket::handle), + new Clientbound<>(ExplosionEffectPacket.TYPE, ExplosionEffectPacket.STREAM_CODEC, ExplosionEffectPacket::handle), + new Clientbound<>(PackListPacket.TYPE, PackListPacket.STREAM_CODEC, PackListPacket::handle), + new Clientbound<>(PackDataPacket.TYPE, PackDataPacket.STREAM_CODEC, PackDataPacket::handle), + new Clientbound<>(PackSoundPacket.TYPE, PackSoundPacket.STREAM_CODEC, PackSoundPacket::handle), + new Clientbound<>(MarkerPacket.TYPE, MarkerPacket.STREAM_CODEC, MarkerPacket::handle)); + + private ModNetwork() { + } + + static CustomPacketPayload.Type type(String name) { + return new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, name)); + } + + public static void sendToServer(CustomPacketPayload message) { + Platform.get().sendToServer(message); + } + + public static void sendToPlayer(ServerPlayer player, CustomPacketPayload message) { + Platform.get().sendToPlayer(player, message); + } + + public static void sendToAll(MinecraftServer server, CustomPacketPayload message) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + sendToPlayer(player, message); + } + } + + /** Sends to every player in {@code level} within {@code range} blocks of the point. */ + public static void sendNear(ServerLevel level, double x, double y, double z, double range, CustomPacketPayload message) { + for (ServerPlayer player : level.players()) { + if (player.distanceToSqr(x, y, z) < range * range) { + sendToPlayer(player, message); + } + } + } + + public record Serverbound(CustomPacketPayload.Type type, + StreamCodec codec, + BiConsumer handler) { + } + + public record Clientbound(CustomPacketPayload.Type type, + StreamCodec codec, + Consumer handler) { + } +} From 154a7e74d5f5009af50d2d7a89014a48aba40d5c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:05 +0800 Subject: [PATCH 0536/1461] Add network/PackDataPacket.java to the 26.1 shared sources --- .../mcheli/network/PackDataPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java new file mode 100644 index 00000000..89c1644d --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: one chunk of a pack's zip. Chunks of a pack arrive in order, so + * {@code offset} only lets the client check that nothing went missing. + * + * @param total length of the whole zip, as in the {@link PackListPacket} + */ +public record PackDataPacket(String hash, int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_data"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackDataPacket::encode, PackDataPacket::decode); + + /** Well under the 1 MiB limit of a clientbound custom payload. */ + public static final int CHUNK_SIZE = 128 * 1024; + + public static void encode(PackDataPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.hash, 64); + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackDataPacket decode(FriendlyByteBuf buf) { + return new PackDataPacket(buf.readUtf(64), buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackDataPacket msg) { + ClientAccess.packs().onPackData(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 2258381a8588a315535d26027e688956dd4babc2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:05 +0800 Subject: [PATCH 0537/1461] Add network/PackListPacket.java to the 26.1 shared sources --- .../mcheli/network/PackListPacket.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java new file mode 100644 index 00000000..ad7f0c85 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +import java.util.ArrayList; +import java.util.List; + +/** + * Server to player: the content packs the server uses, in load order. Sent on join and after + * {@code /mcheli reload}. The client requests the packs it has not cached ({@link PackRequestPacket}) + * and then rebuilds its content from exactly this list, so both sides fly the same definitions. + * The imported 1.0.3 content is never listed: every player imports their own copy. + */ +public record PackListPacket(List packs) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_list"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackListPacket::encode, PackListPacket::decode); + + public static final int MAX_PACKS = 256; + + /** + * @param size length of the pack's zip in bytes, which is what {@link PackDataPacket} sends + */ + public record Entry(String id, String version, String hash, int size) { + } + + public static void encode(PackListPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.packs.size()); + for (Entry entry : msg.packs) { + buf.writeUtf(entry.id, 64); + buf.writeUtf(entry.version, 32); + buf.writeUtf(entry.hash, 64); + buf.writeVarInt(entry.size); + } + } + + public static PackListPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > MAX_PACKS) { + throw new DecoderException("Too many content packs: " + count); + } + List packs = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + packs.add(new Entry(buf.readUtf(64), buf.readUtf(32), buf.readUtf(64), buf.readVarInt())); + } + return new PackListPacket(List.copyOf(packs)); + } + + public static void handle(PackListPacket msg) { + ClientAccess.packs().onPackList(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 2f93be068e4572954a5f200ad4ca6e9751a3fa67 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:06 +0800 Subject: [PATCH 0538/1461] Add network/PackRequestPacket.java to the 26.1 shared sources --- .../mcheli/network/PackRequestPacket.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java new file mode 100644 index 00000000..50f10f74 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.network; + +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Player to server: send me these packs, by content hash. The server only answers for hashes in + * the list it sent, so a client cannot ask for anything else. + */ +public record PackRequestPacket(List hashes) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_request"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackRequestPacket::encode, PackRequestPacket::decode); + + public static void encode(PackRequestPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.hashes.size()); + for (String hash : msg.hashes) { + buf.writeUtf(hash, 64); + } + } + + public static PackRequestPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > PackListPacket.MAX_PACKS) { + throw new DecoderException("Too many content packs requested: " + count); + } + List hashes = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + hashes.add(buf.readUtf(64)); + } + return new PackRequestPacket(List.copyOf(hashes)); + } + + public static void handle(PackRequestPacket msg, ServerPlayer sender) { + if (sender != null) { + PackSync.request(sender, msg.hashes); + } + } + + @Override + public Type type() { + return TYPE; + } +} From a0f868c1384abb2f9a857e8d875ab65c7657c694 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:06 +0800 Subject: [PATCH 0539/1461] Add network/PackSoundPacket.java to the 26.1 shared sources --- .../mcheli/network/PackSoundPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java new file mode 100644 index 00000000..8bac5034 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: play a sound a content pack brought, once, at a point. The game's own sound + * packet ends at the sound manager, which only knows the sounds in {@code sounds.json}, and a pack + * has none: it would be silent. See {@code ModSounds}. + */ +public record PackSoundPacket(String sound, double x, double y, double z, float volume, float pitch) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_sound"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSoundPacket::encode, PackSoundPacket::decode); + + private static final int MAX_NAME = 256; + + public static void encode(PackSoundPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.sound, MAX_NAME); + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.volume); + buf.writeFloat(msg.pitch); + } + + public static PackSoundPacket decode(FriendlyByteBuf buf) { + return new PackSoundPacket(buf.readUtf(MAX_NAME), buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readFloat()); + } + + public static void handle(PackSoundPacket msg) { + ClientAccess.packs().onPackSound(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 2adc92f32d077663c968aa39a30e347c5c92b813 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:06 +0800 Subject: [PATCH 0540/1461] Add network/PackSubmissions.java to the 26.1 shared sources --- .../mcheli/network/PackSubmissions.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java new file mode 100644 index 00000000..b25eba0a --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java @@ -0,0 +1,185 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Util; +import org.jspecify.annotations.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Players offering a server the vehicles they built, for its operators to approve. + *

+ *  Submit in the workshop   ── PackSubmitPacket(chunk) ──▶  until the whole zip is here
+ *  checked off the main thread, kept in mcheli/pending, operators told
+ *  /mcheli approve <pack>    moves it to mcheli/packs and reloads, so every player gets it
+ * 
+ * Nothing a client sends is trusted. Submissions are off until the server's owner turns them on, one + * zip can be no larger than the server allows for a pack, a player sends one at a time with a pause + * in between, chunks out of order end the transfer, and a few transfers run at once at most. The + * bytes stay in memory only while they arrive; {@link PendingPacks} checks them as it would any pack. + * + *

Everything here runs on the server thread except the check, which reads the whole pack. + */ +public final class PackSubmissions { + private static final int MAX_UPLOADS = 4; + private static final long COOLDOWN_MILLIS = 30_000L; + + private static final Map UPLOADS = new HashMap<>(); + private static final Map LAST_SUBMISSION = new HashMap<>(); + + private PackSubmissions() { + } + + static void receive(ServerPlayer player, PackSubmitPacket packet) { + UUID id = player.getUUID(); + if (packet.offset() == 0) { + UPLOADS.remove(id); + Component refusal = refusal(player, packet.total()); + if (refusal != null) { + player.sendSystemMessage(refusal); + return; + } + UPLOADS.put(id, new ByteArrayOutputStream()); + } + ByteArrayOutputStream upload = UPLOADS.get(id); + // No upload means it was refused above and the rest of its chunks are still on their way. + if (upload == null) { + return; + } + long end = (long) upload.size() + packet.data().length; + if (packet.offset() != upload.size() || packet.total() > ServerConfig.maxPackBytes() || end > packet.total()) { + UPLOADS.remove(id); + player.sendSystemMessage(Component.translatable("message.mcheli.submit.failed", "chunks arrived out of order")); + return; + } + upload.write(packet.data(), 0, packet.data().length); + if (end == packet.total()) { + UPLOADS.remove(id); + LAST_SUBMISSION.put(id, System.currentTimeMillis()); + check(player, upload.toByteArray()); + } + } + + private static @Nullable Component refusal(ServerPlayer player, int total) { + if (!ServerConfig.allowSubmissions()) { + return Component.translatable("message.mcheli.submit.off"); + } + if (total <= 0 || total > ServerConfig.maxPackBytes()) { + return Component.translatable("message.mcheli.submit.too_large", ServerConfig.maxPackBytes() >> 20); + } + if (UPLOADS.size() >= MAX_UPLOADS) { + return Component.translatable("message.mcheli.submit.busy"); + } + long now = System.currentTimeMillis(); + LAST_SUBMISSION.values().removeIf(time -> now - time >= COOLDOWN_MILLIS); + return LAST_SUBMISSION.containsKey(player.getUUID()) ? Component.translatable("message.mcheli.submit.busy") : null; + } + + private static void check(ServerPlayer player, byte[] zip) { + MinecraftServer server = player.level().getServer(); + UUID id = player.getUUID(); + String name = player.getScoreboardName(); + CompletableFuture.supplyAsync(() -> { + try { + return MCHeli.packs().pending().store(zip, name, id); + } catch (IOException | PackException | RuntimeException e) { + throw new java.util.concurrent.CompletionException(e); + } + }, Util.nonCriticalIoPool()).whenComplete((submission, failure) -> server.execute(() -> { + if (failure != null) { + Throwable cause = failure.getCause() != null ? failure.getCause() : failure; + MCHeli.LOGGER.info("{} sent a pack that was refused: {}", name, cause.toString()); + tell(server, id, Component.translatable("message.mcheli.submit.failed", cause.getMessage() != null ? cause.getMessage() : cause.toString())); + } else { + arrived(server, submission); + } + })); + } + + private static void arrived(MinecraftServer server, PendingPacks.Submission submission) { + MCHeli.LOGGER.info("{} submitted content pack {} {} ({})", submission.submitter(), submission.id(), + submission.manifest().version(), ContentPack.shortHash(submission.hash())); + // Even with approval off, a pack taking over one that is in use waits for a person. + boolean replaces = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (!ServerConfig.requireApproval() && !replaces) { + try { + install(server, submission); + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not install submitted content pack {}", submission.id(), e); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.failed", e.getMessage())); + } + return; + } + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.waiting", submission.id())); + Component note = Component.translatable("message.mcheli.submit.to_operators", submission.submitter(), submission.id(), submission.manifest().version()); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (PackSync.seesProblems(player)) { + player.sendSystemMessage(note); + } + } + } + + /** + * Puts a waiting pack in use: into {@code mcheli/packs}, then a reload that sends every player + * the new list. + * + * @return whether the pack is in use now; the server's allow list or size limits may leave it out + */ + public static boolean install(MinecraftServer server, PendingPacks.Submission submission) throws IOException, PackException { + MCHeli.packs().pending().install(submission, MCHeli.packs().folder()); + ContentReload.reload(server); + MCHeli.LOGGER.info("Content pack {} from {} approved", submission.id(), submission.submitter()); + boolean live = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (live) { + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.approved", submission.id())); + } + return live; + } + + /** Throws a waiting pack away and tells the player who sent it, if they are here. */ + public static void reject(MinecraftServer server, PendingPacks.Submission submission) throws IOException { + MCHeli.packs().pending().remove(submission); + MCHeli.LOGGER.info("Content pack {} from {} rejected", submission.id(), submission.submitter()); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.rejected", submission.id())); + } + + private static void tell(MinecraftServer server, UUID player, Component message) { + ServerPlayer online = server.getPlayerList().getPlayer(player); + if (online != null) { + online.sendSystemMessage(message); + } + } + + /** An operator joining hears what is waiting for them. */ + static void onLogin(ServerPlayer player) { + if (!PackSync.seesProblems(player)) { + return; + } + try { + List waiting = MCHeli.packs().pending().list(); + if (!waiting.isEmpty()) { + player.sendSystemMessage(Component.translatable("message.mcheli.submit.waiting_for_you", waiting.size())); + } + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not list the submitted content packs", e); + } + } + + static void onLogout(ServerPlayer player) { + UPLOADS.remove(player.getUUID()); + } +} From df5d94066f45ceca968ac2a770d1c4f804846cea Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:07 +0800 Subject: [PATCH 0541/1461] Add network/PackSubmitPacket.java to the 26.1 shared sources --- .../mcheli/network/PackSubmitPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java new file mode 100644 index 00000000..d142e700 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Player to server: one chunk of a pack the player made, offered to the server's operators. See + * {@link PackSubmissions}, which treats all of this as untrusted. + * + * @param offset where this chunk goes; 0 starts a new submission + * @param total length of the whole zip + */ +public record PackSubmitPacket(int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_submit"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSubmitPacket::encode, PackSubmitPacket::decode); + + /** Under the 32 KiB limit of a serverbound custom payload, which is far lower than the other way. */ + public static final int CHUNK_SIZE = 30 * 1024; + + public static void encode(PackSubmitPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackSubmitPacket decode(FriendlyByteBuf buf) { + return new PackSubmitPacket(buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackSubmitPacket msg, ServerPlayer sender) { + PackSubmissions.receive(sender, msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 66a6cd6eb174444b6138d4b1528d6d722583d4ea Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:07 +0800 Subject: [PATCH 0542/1461] Add network/PackSync.java to the 26.1 shared sources --- .../jnhprojects/mcheli/network/PackSync.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java new file mode 100644 index 00000000..b3e53269 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * The server half of the content pack sync. + *

+ *  player joins          ── PackListPacket(id, version, hash, size per pack) ──▶ client
+ *                        ◀── PackRequestPacket(hashes it has not cached) ────────
+ *  every tick            ── PackDataPacket(chunk) ──▶  until each pack is sent
+ * 
+ * Each player gets {@link #CHUNKS_PER_TICK} chunks a tick (about 5 MB a second), one pack after + * the other, so a download never crowds out the game's own packets. A pack's bytes are read when + * its turn comes and dropped once sent. Everything here runs on the server thread. + */ +public final class PackSync { + private static final int CHUNKS_PER_TICK = 2; + + private static final Map> TRANSFERS = new HashMap<>(); + + private PackSync() { + } + + /** Sends a joining player the pack list, and operators any pack problems. */ + public static void onLogin(ServerPlayer player) { + ModNetwork.sendToPlayer(player, list()); + if (seesProblems(player)) { + MCHeli.packs().reportProblems(player::sendSystemMessage); + } + PackSubmissions.onLogin(player); + } + + public static void onLogout(ServerPlayer player) { + TRANSFERS.remove(player.getUUID()); + PackSubmissions.onLogout(player); + } + + /** The packs the server uses now, as the client should load them. */ + public static PackListPacket list() { + List entries = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + try { + entries.add(new PackListPacket.Entry(pack.id(), pack.manifest().version(), pack.hash(), (int) pack.archiveSize())); + } catch (IOException e) { + MCHeli.LOGGER.error("Content pack {} cannot be sent to players", pack, e); + } + } + return new PackListPacket(List.copyOf(entries)); + } + + /** After a reload: drops transfers in progress and sends every player the new list. */ + public static void sendListToAll(MinecraftServer server) { + TRANSFERS.clear(); + ModNetwork.sendToAll(server, list()); + } + + /** Operators, and the owner of a single player or LAN world, are told what is wrong with the packs. */ + public static boolean seesProblems(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + return server.isSingleplayerOwner(player.nameAndId()) + || Commands.hasPermission(Commands.LEVEL_GAMEMASTERS).test(player.createCommandSourceStack()); + } + + static void request(ServerPlayer player, List hashes) { + Deque queue = TRANSFERS.computeIfAbsent(player.getUUID(), id -> new ArrayDeque<>()); + for (String hash : hashes) { + ContentPack pack = find(hash); + if (pack == null) { + MCHeli.LOGGER.debug("{} asked for content pack {}, which the server does not use", player.getScoreboardName(), hash); + continue; + } + if (queue.stream().noneMatch(t -> t.pack == pack)) { + queue.add(new Transfer(pack)); + } + } + } + + private static @Nullable ContentPack find(String hash) { + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.hash().equals(hash)) { + return pack; + } + } + return null; + } + + public static void pump(MinecraftServer server) { + Iterator>> it = TRANSFERS.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey()); + Deque queue = entry.getValue(); + if (player == null || queue.isEmpty()) { + it.remove(); + continue; + } + for (int sent = 0; sent < CHUNKS_PER_TICK && !queue.isEmpty(); sent++) { + Transfer transfer = queue.peek(); + PackDataPacket chunk = transfer.next(); + if (chunk == null) { + queue.poll(); + continue; + } + ModNetwork.sendToPlayer(player, chunk); + if (transfer.done()) { + queue.poll(); + } + } + } + } + + /** One pack on its way to one player. */ + private static final class Transfer { + private final ContentPack pack; + private byte @Nullable [] data; + private int offset; + + Transfer(ContentPack pack) { + this.pack = pack; + } + + /** The next chunk, or {@code null} when the pack could not be read. */ + @Nullable PackDataPacket next() { + if (data == null) { + try { + data = pack.archive(); + } catch (IOException e) { + MCHeli.LOGGER.error("Could not read content pack {} to send it", pack, e); + return null; + } + } + int end = Math.min(offset + PackDataPacket.CHUNK_SIZE, data.length); + PackDataPacket chunk = new PackDataPacket(pack.hash(), offset, data.length, Arrays.copyOfRange(data, offset, end)); + offset = end; + return chunk; + } + + boolean done() { + return data != null && offset >= data.length; + } + } +} From 3858e1909e1b874a5e3e5cf65d2cd4c3283a72ad Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:08 +0800 Subject: [PATCH 0543/1461] Add network/PilotStatePacket.java to the 26.1 shared sources --- .../mcheli/network/PilotStatePacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java new file mode 100644 index 00000000..6426f060 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Sent by the pilot every tick. Position, yaw and pitch already travel through the vanilla + * vehicle move packet; this carries the parts vanilla does not know about. + * + * @param keys the pilot's key state as packed by {@link AircraftEntity.ControlInput#write()}, so + * other players see wheels steer and tracks turn + */ +public record PilotStatePacket(int entityId, float throttle, float reverse, float roll, boolean hovering, byte keys) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pilot_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PilotStatePacket::encode, PilotStatePacket::decode); + + public static void encode(PilotStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeFloat(msg.throttle); + buf.writeFloat(msg.reverse); + buf.writeFloat(msg.roll); + buf.writeBoolean(msg.hovering); + buf.writeByte(msg.keys); + } + + public static PilotStatePacket decode(FriendlyByteBuf buf) { + return new PilotStatePacket(buf.readVarInt(), buf.readFloat(), buf.readFloat(), buf.readFloat(), buf.readBoolean(), buf.readByte()); + } + + public static void handle(PilotStatePacket msg, ServerPlayer sender) { + if (sender == null) { + return; + } + // Only the player actually flying this aircraft may drive it. + if (sender.getVehicle() instanceof AircraftEntity aircraft + && aircraft.getId() == msg.entityId + && aircraft.getControllingPassenger() == sender + && Float.isFinite(msg.throttle) + && Float.isFinite(msg.reverse) + && Float.isFinite(msg.roll)) { + aircraft.acceptPilotState(msg.throttle, msg.reverse, msg.roll, msg.hovering, msg.keys); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 3b1051f5b1aa01469e59a08f8680e7e56ec80ea1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:08 +0800 Subject: [PATCH 0544/1461] Add network/SpotPacket.java to the 26.1 shared sources --- .../mcheli/network/SpotPacket.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java new file mode 100644 index 00000000..1ee72286 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.weapon.Spotting; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; + +/** + * The attack key through a rangefinder, {@code MCH_PacketIndSpotEntity}: spot players and + * vehicles, spot mobs, or mark a point for the team. Spotting uses a charge; marking is free. + */ +public record SpotPacket(Mode mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("spot"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(SpotPacket::encode, SpotPacket::decode); + + /** The rangefinder's modes, in the order its mode key steps through them. */ + public enum Mode { + PLAYERS_AND_VEHICLES(Spotting.PLAYERS | Spotting.GROUND_VEHICLES | Spotting.HELICOPTERS | Spotting.PLANES), + MOBS(Spotting.OTHERS | Spotting.MONSTERS), + MARK_POINT(0); + + final int filter; + + Mode(int filter) { + this.filter = filter; + } + } + + public static void encode(SpotPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.mode.ordinal()); + } + + public static SpotPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Mode[] modes = Mode.values(); + if (ordinal < 0 || ordinal >= modes.length) { + throw new IllegalArgumentException("Unknown spot mode " + ordinal); + } + return new SpotPacket(modes[ordinal]); + } + + public static void handle(SpotPacket msg, ServerPlayer sender) { + if (sender == null || !RangefinderItem.canUse(sender)) { + return; + } + ItemStack stack = sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + stack = sender.getItemInHand(InteractionHand.OFF_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + return; + } + } + boolean done; + if (msg.mode == Mode.MARK_POINT) { + done = Spotting.markPoint(sender); + } else if (RangefinderItem.charges(stack) > 0) { + if (!sender.getAbilities().instabuild) { + // An empty rangefinder stays: it only needs redstone, unlike a worn out tool. + stack.setDamageValue(stack.getDamageValue() + 1); + } + done = Spotting.spot(sender, msg.mode.filter); + } else { + done = false; + } + ModSounds.playAt(sender, done ? "pi" : "ng", 1.0F, 1.0F); + } + + @Override + public Type type() { + return TYPE; + } +} From 870dd691b439784e84140a717ff72b7fc1372551 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:09 +0800 Subject: [PATCH 0545/1461] Add network/TeamPacket.java to the 26.1 shared sources --- .../mcheli/network/TeamPacket.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java new file mode 100644 index 00000000..14b2db42 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A button of the team manager, {@code MCH_PacketIndMultiplayCommand}. Operators only; the + * server checks again. + * + * @param name the new team's name, for {@link Action#CREATE_TEAM} + * @param color the new team's colour, as a vanilla team colour id + */ +public record TeamPacket(Action action, String name, int color, boolean friendlyFire) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("team"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TeamPacket::encode, TeamPacket::decode); + + public enum Action { + PVP, + SHUFFLE, + CREATE_TEAM, + JUMP_TO_SPAWN, + DESTROY_ALL + } + + public TeamPacket(Action action) { + this(action, "", 0, false); + } + + public static void encode(TeamPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeUtf(msg.name, 16); + buf.writeVarInt(msg.color); + buf.writeBoolean(msg.friendlyFire); + } + + public static TeamPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown team action " + ordinal); + } + return new TeamPacket(actions[ordinal], buf.readUtf(16), buf.readVarInt(), buf.readBoolean()); + } + + public static void handle(TeamPacket msg, ServerPlayer sender) { + if (sender == null || !TeamManager.mayManage(sender)) { + return; + } + switch (msg.action) { + case PVP -> TeamManager.togglePvp(sender); + case SHUFFLE -> TeamManager.shuffle(sender); + case CREATE_TEAM -> TeamManager.create(sender, msg.name, msg.color, msg.friendlyFire); + case JUMP_TO_SPAWN -> TeamManager.jumpToSpawn(sender); + case DESTROY_ALL -> TeamManager.destroyAll(sender); + } + } + + @Override + public Type type() { + return TYPE; + } +} From bd6abe77a3fb8edb454c7fb259ecd8b5d178c28c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:09 +0800 Subject: [PATCH 0546/1461] Add network/TestFlightPacket.java to the 26.1 shared sources --- .../mcheli/network/TestFlightPacket.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java new file mode 100644 index 00000000..db12a390 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.workshop.TestFlight; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** The workshop's Test flight button: fly the vehicle just saved. The server checks who is asking. */ +public record TestFlightPacket(String vehicle) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("test_flight"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TestFlightPacket::encode, TestFlightPacket::decode); + private static final int MAX_NAME = 64; + + public static void encode(TestFlightPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.vehicle, MAX_NAME); + } + + public static TestFlightPacket decode(FriendlyByteBuf buf) { + return new TestFlightPacket(buf.readUtf(MAX_NAME)); + } + + public static void handle(TestFlightPacket msg, ServerPlayer sender) { + if (sender != null) { + TestFlight.start(sender, msg.vehicle); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 07869d1e2738874adeeaca7c6cf0b478f3b27e44 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:09 +0800 Subject: [PATCH 0547/1461] Add network/WeaponStatePacket.java to the 26.1 shared sources --- .../mcheli/network/WeaponStatePacket.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java new file mode 100644 index 00000000..292f643e --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to crew: the magazine and reserve of one weapon after it fired, reloaded or was + * resupplied. The original's {@code MCH_PacketNotifyAmmoNum}. + */ +public record WeaponStatePacket(int entityId, int weapon, int ammo, int rest) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("weapon_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(WeaponStatePacket::encode, WeaponStatePacket::decode); + + public static void encode(WeaponStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.ammo); + buf.writeVarInt(msg.rest); + } + + public static WeaponStatePacket decode(FriendlyByteBuf buf) { + return new WeaponStatePacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(WeaponStatePacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player != null && player.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.applyAmmo(msg.weapon, msg.ammo, msg.rest); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From ee96260c5705582ca0baaeaed704756b49786a58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:10 +0800 Subject: [PATCH 0548/1461] Add particle/GlowParticleOptions.java to the 26.1 shared sources --- .../mcheli/particle/GlowParticleOptions.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java new file mode 100644 index 00000000..63a3577f --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A soft ball of light blended on top of the scene: the flash of an explosion, a burning wreck, an + * ember. Spawned on the client only, but the codecs let {@code /particle mcheli:glow} show it too. + * + * @param color RGB of the light + * @param size radius in blocks at its largest + * @param lifetime ticks until it has faded out + * @param flicker whether it wavers like a fire rather than fading smoothly like a flash + */ +public record GlowParticleOptions(int color, float size, int lifetime, boolean flicker) implements ParticleOptions { + /** White hot at the centre of a blast. */ + public static final int FLASH = 0xFFE2B0; + /** Burning fuel and the fire inside the smoke. */ + public static final int FIRE = 0xFF8A2E; + /** Embers and the deeper red at the edge of a fireball. */ + public static final int EMBER = 0xFF5A1E; + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.RGB_COLOR_CODEC.fieldOf("color").forGetter(GlowParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(GlowParticleOptions::size), + ExtraCodecs.POSITIVE_INT.fieldOf("lifetime").forGetter(GlowParticleOptions::lifetime), + Codec.BOOL.optionalFieldOf("flicker", false).forGetter(GlowParticleOptions::flicker)) + .apply(i, GlowParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, GlowParticleOptions::color, + ByteBufCodecs.FLOAT, GlowParticleOptions::size, + ByteBufCodecs.VAR_INT, GlowParticleOptions::lifetime, + ByteBufCodecs.BOOL, GlowParticleOptions::flicker, + GlowParticleOptions::new); + + public static GlowParticleOptions flash(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, false); + } + + public static GlowParticleOptions fire(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, true); + } + + @Override + public ParticleType getType() { + return ModParticles.GLOW.get(); + } +} From cfee62c784070d957621f0178319c206fdb4a749 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:10 +0800 Subject: [PATCH 0549/1461] Add particle/SmokeParticleOptions.java to the 26.1 shared sources --- .../mcheli/particle/SmokeParticleOptions.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java new file mode 100644 index 00000000..29a0b0ef --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java @@ -0,0 +1,42 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A puff of coloured smoke that swells and pales as it drifts, {@code MCH_EntityParticleSmoke} + * with {@code toWhite} set. The smoke grenades use it to mark a spot. + * + * @param color RGB the puff starts with + * @param size the original's particle size; a puff ends up about {@code size / 5} blocks across + * @param lifetime ticks the puff lasts, or 0 for the grenade smoke's three to five seconds + */ +public record SmokeParticleOptions(int color, float size, int lifetime) implements ParticleOptions { + public SmokeParticleOptions(int color, float size) { + this(color, size, 0); + } + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.RGB_COLOR_CODEC.fieldOf("color").forGetter(SmokeParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(SmokeParticleOptions::size), + ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("lifetime", 0).forGetter(SmokeParticleOptions::lifetime)) + .apply(i, SmokeParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, SmokeParticleOptions::color, + ByteBufCodecs.FLOAT, SmokeParticleOptions::size, + ByteBufCodecs.VAR_INT, SmokeParticleOptions::lifetime, + SmokeParticleOptions::new); + + @Override + public ParticleType getType() { + return ModParticles.SMOKE.get(); + } +} From 3963b556d30886a6f18cedf89c76106b3b27fa42 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:11 +0800 Subject: [PATCH 0550/1461] Ask the loader for block entity types, whose constructor is private on 26.1 --- .../jnhprojects/mcheli/platform/Platform.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Platform.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Platform.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Platform.java new file mode 100644 index 00000000..dbf58f03 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Platform.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.function.BiFunction; + +/** + * What the shared code needs from the mod loader. Each loader module installs its implementation + * first thing during mod construction, before anything else of ours runs. + * + *

Kept small on purpose: registration and events go the other way, with the loader module + * calling into the shared code (see {@code Registration} and the {@code *Hooks} classes). + */ +public interface Platform { + + static Platform get() { + Platform platform = Holder.instance; + if (platform == null) { + throw new IllegalStateException("No mod loader platform installed yet"); + } + return platform; + } + + static void install(Platform platform) { + if (Holder.instance != null) { + throw new IllegalStateException("Platform already installed: " + Holder.instance.loaderName()); + } + Holder.instance = Objects.requireNonNull(platform); + } + + /** {@code forge}, {@code neoforge} or {@code fabric}, for logs and {@code /mcheli status}. */ + String loaderName(); + + /** The game directory, which holds the {@code mcheli} folder. */ + Path gameDir(); + + /** The config directory, which holds the client config. */ + Path configDir(); + + /** Our version as the loader read it from the mod metadata. */ + String modVersion(); + + /** Whether this is a physical client, as opposed to a dedicated server. */ + boolean isClient(); + + void sendToServer(CustomPacketPayload payload); + + void sendToPlayer(ServerPlayer player, CustomPacketPayload payload); + + /** + * A builder for one of our creative tabs, placed after {@code after}. Vanilla's builder is not + * public on every loader and only Forge and NeoForge can order tabs. + */ + CreativeModeTab.Builder creativeTab(ResourceKey after); + + /** A block entity type; vanilla's constructor is private on 26.1. */ + BlockEntityType blockEntityType(BiFunction factory, Block block); + + final class Holder { + private static volatile Platform instance; + + private Holder() { + } + } +} From 931889f2641e52f7c2790882ff6c73d4df289891 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:11 +0800 Subject: [PATCH 0551/1461] Add platform/Registered.java to the 26.1 shared sources --- .../mcheli/platform/Registered.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Registered.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Registered.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Registered.java new file mode 100644 index 00000000..3d1b9085 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Registered.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.resources.Identifier; + +import java.util.function.Supplier; + +/** + * A registry entry the shared code declares and the loader module registers, like Forge's + * {@code RegistryObject}. {@link #get()} works once the loader has registered it. + */ +public final class Registered implements Supplier { + private final Identifier id; + private volatile T value; + + Registered(Identifier id) { + this.id = id; + } + + public Identifier id() { + return id; + } + + @Override + public T get() { + T v = value; + if (v == null) { + throw new IllegalStateException("Registry entry " + id + " is not registered yet"); + } + return v; + } + + void bind(T value) { + this.value = value; + } +} From 780f87fdb1e49810de3567a3736b03f758f36b6b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:12 +0800 Subject: [PATCH 0552/1461] Add platform/Registration.java to the 26.1 shared sources --- .../mcheli/platform/Registration.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Registration.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Registration.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Registration.java new file mode 100644 index 00000000..030a07dd --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/platform/Registration.java @@ -0,0 +1,80 @@ +package com.jnhprojects.mcheli.platform; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.Registry; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Every vanilla registry entry the mod adds. The shared registry classes declare their entries + * here during mod construction; the loader module then registers them its own way: Forge and + * NeoForge from their register event, Fabric straight away. + */ +public final class Registration { + private static final List> PENDING = new ArrayList<>(); + private static final Set>> REGISTRIES = new LinkedHashSet<>(); + + private Registration() { + } + + /** + * Declares an entry. + * + * @param factory makes the value; it gets the entry's key for {@code Item.Properties#setId} + * and the like + */ + public static synchronized Registered register(ResourceKey> registry, String name, + Function, T> factory) { + Identifier id = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, name); + Registered entry = new Registered<>(id); + PENDING.add(new Pending<>(registry, entry, factory)); + REGISTRIES.add(registry); + return entry; + } + + /** The registries with entries of ours, in the order they were first used. */ + public static synchronized List>> registries() { + return List.copyOf(REGISTRIES); + } + + /** + * A key from {@link #registries()} with its element type filled in, for the loader's generic + * register calls. The type is whatever the caller needs; entries are only ever handed to the + * registry they were declared for. + */ + @SuppressWarnings("unchecked") + public static ResourceKey> typed(ResourceKey> registry) { + return (ResourceKey>) registry; + } + + /** + * Creates our entries for one registry, in declaration order, and hands each to the loader. + * Called once per registry. + */ + public static synchronized void registerAll(ResourceKey> registry, BiConsumer sink) { + for (Pending pending : PENDING) { + if (pending.registry.equals(registry)) { + @SuppressWarnings("unchecked") + Pending ours = (Pending) pending; + ours.create(sink); + } + } + } + + private record Pending(ResourceKey> registry, Registered entry, + Function, T> factory) { + @SuppressWarnings("unchecked") + void create(BiConsumer sink) { + T value = factory.apply(ResourceKey.create((ResourceKey>) registry, entry.id())); + entry.bind(value); + sink.accept(entry.id(), value); + } + } +} From 8e7dd7a045af20b4a7b3485ac8bcf705874b4c8e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:12 +0800 Subject: [PATCH 0553/1461] Get the drafting table's block entity type from the platform on 26.1 --- .../mcheli/registry/ModBlockEntities.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java new file mode 100644 index 00000000..00d40aed --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java @@ -0,0 +1,21 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; + +/** Our block entities: the drafting table's, which draws its model. */ +public final class ModBlockEntities { + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.BLOCK_ENTITY_TYPE, + "drafting_table", key -> Platform.get().blockEntityType(DraftingTableBlockEntity::new, ModBlocks.DRAFTING_TABLE.get())); + + private ModBlockEntities() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From f931fc21c36a4ecb7a1df697e5c006f340417588 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:13 +0800 Subject: [PATCH 0554/1461] Add registry/ModBlocks.java to the 26.1 shared sources --- .../mcheli/registry/ModBlocks.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java new file mode 100644 index 00000000..19995a92 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockBehaviour; + +/** Our blocks: the drafting table, lit or not. */ +public final class ModBlocks { + public static final Registered DRAFTING_TABLE = Registration.register(Registries.BLOCK, "drafting_table", + key -> new DraftingTableBlock(BlockBehaviour.Properties.of().setId(key) + .strength(0.2F) + .sound(SoundType.METAL) + .noOcclusion() + .lightLevel(state -> state.getValue(DraftingTableBlock.LIT) ? 15 : 0))); + + private ModBlocks() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 16547e3b97d694f7be44448fd1e87dbae70bc6c2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:13 +0800 Subject: [PATCH 0555/1461] Add registry/ModCreativeTabs.java to the 26.1 shared sources --- .../mcheli/registry/ModCreativeTabs.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java new file mode 100644 index 00000000..870aa8b8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java @@ -0,0 +1,72 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** + * One tab per vehicle kind, listing a stack for each loaded definition of that kind, and one for + * the equipment and throwables. Those lists are read from the content registry whenever a tab is + * built, so reloaded content shows up. + */ +public final class ModCreativeTabs { + public static final Registered HELICOPTERS = register("helicopters", "helicopter", "ah-64", + CreativeModeTabs.SPAWN_EGGS); + public static final Registered PLANES = register("planes", "plane", "f-35b", key(HELICOPTERS)); + public static final Registered TANKS = register("tanks", "tank", "m1a2", key(PLANES)); + public static final Registered VEHICLES = register("vehicles", "vehicle", "mk15", key(TANKS)); + public static final Registered EQUIPMENT = Registration.register(Registries.CREATIVE_MODE_TAB, "equipment", + key -> Platform.get().creativeTab(key(VEHICLES)) + .title(Component.translatable("itemGroup.mcheli.equipment")) + .icon(() -> new ItemStack(ModItems.WRENCH.get())) + .displayItems((params, output) -> { + ModItems.EQUIPMENT.forEach(item -> output.accept(item.get())); + MCHeli.content().throwables().forEach(def -> output.accept(ThrowableItem.stackOf(def))); + }) + .build()); + + private ModCreativeTabs() { + } + + private static ResourceKey key(Registered tab) { + return ResourceKey.create(Registries.CREATIVE_MODE_TAB, tab.id()); + } + + private static Registered register(String name, String kind, String icon, ResourceKey after) { + return Registration.register(Registries.CREATIVE_MODE_TAB, name, key -> Platform.get().creativeTab(after) + .title(Component.translatable("itemGroup.mcheli." + name)) + .icon(() -> { + AircraftDefinition def = MCHeli.content().any(icon); + return def != null ? AircraftItem.stackOf(def) : new ItemStack(ModItems.forKind(kind)); + }) + .displayItems((params, output) -> { + for (AircraftDefinition def : MCHeli.content().all()) { + if (def.kind().equals(kind)) { + output.accept(AircraftItem.stackOf(def)); + } + } + }) + .build()); + } + + /** Our tabs, for rebuilding their contents after a content reload. */ + public static List all() { + return List.of(HELICOPTERS.get(), PLANES.get(), TANKS.get(), VEHICLES.get(), EQUIPMENT.get()); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 910f60a845008d5d66f9a937bae9cc37734464d8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:14 +0800 Subject: [PATCH 0556/1461] Add registry/ModDataComponents.java to the 26.1 shared sources --- .../mcheli/registry/ModDataComponents.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java new file mode 100644 index 00000000..377a5783 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.Codec; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.codec.ByteBufCodecs; + +public final class ModDataComponents { + /** + * The definition a vehicle item places, such as {@code ah-64}. Saved with the stack and sent + * to clients; the item itself only says which kind of vehicle it is. + */ + public static final Registered> VEHICLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "vehicle", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + /** The definition a throwable item throws, such as {@code m18_red}. */ + public static final Registered> THROWABLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "throwable", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + private ModDataComponents() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 08167ff8e11ef155b51345c049c1f116b85fb418 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:14 +0800 Subject: [PATCH 0557/1461] Add registry/ModEntities.java to the 26.1 shared sources --- .../mcheli/registry/ModEntities.java | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java new file mode 100644 index 00000000..b6686ccd --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java @@ -0,0 +1,170 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.entity.ContainerEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.RappelEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; + +/** + * One entity type per vehicle kind. The concrete vehicle is stored on the entity, the same approach + * the original took, so adding content never changes the entity registry. + */ +public final class ModEntities { + public static final Registered> HELICOPTER = register("helicopter", HelicopterEntity::new); + public static final Registered> PLANE = register("plane", PlaneEntity::new); + public static final Registered> TANK = register("tank", TankEntity::new); + public static final Registered> VEHICLE = register("vehicle", GroundVehicleEntity::new); + + public static final Registered> BULLET = projectile("bullet", BulletEntity::new); + public static final Registered> ROCKET = projectile("rocket", RocketEntity::new); + public static final Registered> BOMB = projectile("bomb", BombEntity::new); + public static final Registered> AA_MISSILE = projectile("aa_missile", AAMissileEntity::new); + public static final Registered> AS_MISSILE = projectile("as_missile", ASMissileEntity::new); + public static final Registered> AT_MISSILE = projectile("at_missile", ATMissileEntity::new); + public static final Registered> TV_MISSILE = projectile("tv_missile", TvMissileEntity::new); + public static final Registered> TORPEDO = projectile("torpedo", TorpedoEntity::new); + public static final Registered> MARKER_ROCKET = projectile("marker_rocket", MarkerRocketEntity::new); + public static final Registered> DISPENSED_ITEM = projectile("dispensed_item", DispensedItemEntity::new); + public static final Registered> FLARE = Registration.register(Registries.ENTITY_TYPE, "flare", key -> EntityType.Builder + .of(FlareEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(2) + .fireImmune() + .noSave() + .build(key)); + public static final Registered> A10 = Registration.register(Registries.ENTITY_TYPE, "a10", key -> EntityType.Builder + .of(A10Entity::new, MobCategory.MISC) + .sized(5.0F, 3.0F) + .clientTrackingRange(32) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + + public static final Registered> PARACHUTE = Registration.register(Registries.ENTITY_TYPE, "parachute", key -> EntityType.Builder + .of(ParachuteEntity::new, MobCategory.MISC) + .sized(1.5F, 0.6F) + .clientTrackingRange(10) + .updateInterval(1) + .build(key)); + + public static final Registered> THROWABLE = Registration.register(Registries.ENTITY_TYPE, "throwable", key -> EntityType.Builder + .of(ThrowableEntity::new, MobCategory.MISC) + .sized(0.25F, 0.25F) + .clientTrackingRange(8) + .updateInterval(2) + .noSave() + .build(key)); + + public static final Registered> GLTD = Registration.register(Registries.ENTITY_TYPE, "gltd", key -> EntityType.Builder + .of(GltdEntity::new, MobCategory.MISC) + .sized(0.5F, 0.5F) + .clientTrackingRange(10) + .updateInterval(3) + .build(key)); + + public static final Registered> CONTAINER = Registration.register(Registries.ENTITY_TYPE, "container", key -> EntityType.Builder + .of(ContainerEntity::new, MobCategory.MISC) + .sized(2.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key)); + + public static final Registered> CHAIN = Registration.register(Registries.ENTITY_TYPE, "chain", key -> EntityType.Builder + .of(ChainEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(2) + .fireImmune() + .build(key)); + + public static final Registered> RAPPEL = Registration.register(Registries.ENTITY_TYPE, "rappel", key -> EntityType.Builder + .of(RappelEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + + public static final Registered> UAV_STATION = Registration.register(Registries.ENTITY_TYPE, "uav_station", key -> EntityType.Builder + .of(UavStationEntity::new, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key)); + + private ModEntities() { + } + + /** Projectiles are a metre cube like the original's, and never saved. */ + private static Registered> projectile(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + } + + private static Registered> register(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .build(key)); + } + + /** The entity type that hosts a definition of this kind. */ + public static EntityType typeFor(AircraftDefinition def) { + if (def instanceof HelicopterDefinition) { + return HELICOPTER.get(); + } + if (def instanceof PlaneDefinition) { + return PLANE.get(); + } + if (def instanceof TankDefinition) { + return TANK.get(); + } + return VEHICLE.get(); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 82c6838f5aa7aa89be500cef1473dbda8eff9a0e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:14 +0800 Subject: [PATCH 0558/1461] Add registry/ModItems.java to the 26.1 shared sources --- .../jnhprojects/mcheli/registry/ModItems.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java new file mode 100644 index 00000000..e7415dfd --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java @@ -0,0 +1,102 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.item.LegacyAircraftItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.item.PlacedEntityItem; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.item.UavStationItem; +import com.jnhprojects.mcheli.item.WrenchItem; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; + +import java.util.List; +import java.util.Map; + +/** + * One item per vehicle kind; the stack says which vehicle (see {@link AircraftItem}). The 1.0.0 + * items, one per vehicle, are kept for the content present at startup so old worlds keep them. The + * equipment (fuel can, wrench, parachute and so on) has one item each, and the throwables share one. + */ +public final class ModItems { + public static final Registered HELICOPTER = kind("helicopter"); + public static final Registered PLANE = kind("plane"); + public static final Registered TANK = kind("tank"); + public static final Registered VEHICLE = kind("vehicle"); + + public static final Registered FUEL = Registration.register(Registries.ITEM, "fuel", key -> new FuelItem(new Item.Properties().setId(key))); + public static final Registered WRENCH = Registration.register(Registries.ITEM, "wrench", key -> new WrenchItem(new Item.Properties().setId(key))); + public static final Registered PARACHUTE = Registration.register(Registries.ITEM, "parachute", key -> new ParachuteItem(new Item.Properties().setId(key))); + public static final Registered GLTD = Registration.register(Registries.ITEM, "gltd", + key -> new PlacedEntityItem(ModEntities.GLTD, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered RANGEFINDER = Registration.register(Registries.ITEM, "rangefinder", key -> new RangefinderItem(new Item.Properties().setId(key))); + public static final Registered CONTAINER = Registration.register(Registries.ITEM, "container", + key -> new PlacedEntityItem(ModEntities.CONTAINER, new Item.Properties().setId(key))); + public static final Registered CHAIN = Registration.register(Registries.ITEM, "chain", key -> new ChainItem(new Item.Properties().setId(key))); + public static final Registered STINGER_MISSILE = Registration.register(Registries.ITEM, "fim92_bullet", key -> new Item(new Item.Properties().setId(key).stacksTo(16))); + public static final Registered STINGER = Registration.register(Registries.ITEM, "fim92", + key -> new LauncherItem("fim92", () -> ModItems.STINGER_MISSILE.get(), new Item.Properties().setId(key))); + public static final Registered JAVELIN_MISSILE = Registration.register(Registries.ITEM, "fgm148_bullet", key -> new Item(new Item.Properties().setId(key).stacksTo(16))); + public static final Registered JAVELIN = Registration.register(Registries.ITEM, "fgm148", + key -> new LauncherItem("fgm148", () -> ModItems.JAVELIN_MISSILE.get(), new Item.Properties().setId(key))); + public static final Registered UAV_STATION = Registration.register(Registries.ITEM, "uav_station", + key -> new UavStationItem(UavStationEntity.STATION, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered UAV_CONTROLLER = Registration.register(Registries.ITEM, "uav_station2", + key -> new UavStationItem(UavStationEntity.PORTABLE, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered DRAFTING_TABLE = Registration.register(Registries.ITEM, "drafting_table", + key -> new BlockItem(ModBlocks.DRAFTING_TABLE.get(), new Item.Properties().setId(key).useBlockDescriptionPrefix())); + /** Every throwable; the stack says which (see {@link ThrowableItem}). */ + public static final Registered THROWABLE = Registration.register(Registries.ITEM, "throwable", key -> new ThrowableItem(new Item.Properties().setId(key))); + + /** The equipment tab, in the order it lists them. */ + public static final List> EQUIPMENT = List.of(FUEL, WRENCH, PARACHUTE, GLTD, RANGEFINDER, CONTAINER, CHAIN, + STINGER, STINGER_MISSILE, JAVELIN, JAVELIN_MISSILE, UAV_STATION, UAV_CONTROLLER, DRAFTING_TABLE); + + private static final Map> BY_KIND = + Map.of("helicopter", HELICOPTER, "plane", PLANE, "tank", TANK, "vehicle", VEHICLE); + + private ModItems() { + } + + private static Registered kind(String kind) { + return Registration.register(Registries.ITEM, kind, key -> new AircraftItem(kind, new Item.Properties().setId(key).stacksTo(1))); + } + + /** + * Declares the entries, with a 1.0.0 item for each original vehicle in {@code content}; the + * loader module registers them. + */ + public static void init(ContentRegistry content) { + for (AircraftDefinition def : content.all()) { + String name = def.name(); + // Only the imported content existed in 1.0.0; pack vehicles never had their own item. + if (!def.pack().equals(ContentSource.ORIGINAL) || BY_KIND.containsKey(name) || EQUIPMENT.stream().anyMatch(e -> e.id().getPath().equals(name))) { + // 1.0.0 could not have had such an item either: it would have clashed the same way. + continue; + } + String kind = def.kind(); + Registration.register(Registries.ITEM, name, key -> new LegacyAircraftItem(kind, name, + new Item.Properties().setId(key).stacksTo(1))); + } + } + + /** The generic item of a vehicle kind ({@code helicopter}, {@code plane}, {@code tank} or {@code vehicle}). */ + public static Item forKind(String kind) { + Registered item = BY_KIND.get(kind); + if (item == null) { + throw new IllegalArgumentException("Unknown vehicle kind " + kind); + } + return item.get(); + } +} From 898f0f4a4ade4d81710331e9fb9b9cd4d681d6d1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:15 +0800 Subject: [PATCH 0559/1461] Add registry/ModMenus.java to the 26.1 shared sources --- .../jnhprojects/mcheli/registry/ModMenus.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java new file mode 100644 index 00000000..0dac0052 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; + +/** Our screens with slots. The cargo chest uses vanilla's chest menus and needs no entry here. */ +public final class ModMenus { + public static final Registered> VEHICLE = Registration.register(Registries.MENU, "vehicle", + key -> new MenuType<>(VehicleMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.MENU, "drafting_table", + key -> new MenuType<>(DraftingTableMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> UAV_STATION = Registration.register(Registries.MENU, "uav_station", + key -> new MenuType<>(UavStationMenu::new, FeatureFlags.VANILLA_SET)); + + private ModMenus() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 6afbe750ad0db7878fb6fee2b93cde8cdd3365a2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:15 +0800 Subject: [PATCH 0560/1461] Add registry/ModParticles.java to the 26.1 shared sources --- .../mcheli/registry/ModParticles.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java new file mode 100644 index 00000000..4ed53acd --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java @@ -0,0 +1,48 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; + +public final class ModParticles { + /** Ignores the particle limit: an explosion that loses its flash to the cap looks broken. */ + public static final Registered> GLOW = Registration.register(Registries.PARTICLE_TYPE, "glow", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return GlowParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return GlowParticleOptions.STREAM_CODEC; + } + }); + + /** Smoke grenades: a marker that vanishes under the particle limit is no marker. */ + public static final Registered> SMOKE = Registration.register(Registries.PARTICLE_TYPE, "smoke", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return SmokeParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return SmokeParticleOptions.STREAM_CODEC; + } + }); + + private ModParticles() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 42cb8b63366fbcaf5249e558ed88167a9dec41e7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:16 +0800 Subject: [PATCH 0561/1461] Add team/Markers.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/team/Markers.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/team/Markers.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/team/Markers.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/team/Markers.java new file mode 100644 index 00000000..4d869373 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/team/Markers.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.team; + +import net.minecraft.core.BlockPos; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * What this client's team has spotted, {@code MCH_GuiTargetMarker}: the spotted entities, each for + * as long as it was spotted, and the last marked point. Filled by {@code MarkerPacket} on the + * client's main thread and drawn by the HUD; a dedicated server never touches it. + */ +public final class Markers { + /** A marked point stays up this long, unless another replaces it. */ + public static final int POINT_TICKS = 60 * 20; + + private static final Map SPOTTED = new HashMap<>(); + private static @Nullable BlockPos point; + private static int pointTicks; + + private Markers() { + } + + public static void spotted(int[] entities, int ticks) { + for (int id : entities) { + SPOTTED.merge(id, ticks, Math::max); + } + } + + public static void point(BlockPos pos) { + point = pos; + pointTicks = POINT_TICKS; + } + + /** Entity ids and ticks left. */ + public static Map spotted() { + return SPOTTED; + } + + public static @Nullable BlockPos point() { + return point; + } + + public static void tick() { + SPOTTED.replaceAll((id, ticks) -> ticks - 1); + SPOTTED.values().removeIf(ticks -> ticks <= 0); + if (pointTicks > 0 && --pointTicks == 0) { + point = null; + } + } + + public static void clear() { + SPOTTED.clear(); + point = null; + pointTicks = 0; + } +} From 65ff8f88e7f5301f56f9f87dc4202473b4038033 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:16 +0800 Subject: [PATCH 0562/1461] Offer the ChatFormatting colours for teams on 26.1 --- .../jnhprojects/mcheli/team/TeamManager.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java new file mode 100644 index 00000000..db56d298 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java @@ -0,0 +1,116 @@ +package com.jnhprojects.mcheli.team; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.Permissions; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.gamerules.GameRules; +import net.minecraft.world.level.portal.TeleportTransition; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * The team manager's actions, {@code MCH_Multiplay} and {@code MCH_MultiplayPacketHandler}: for + * operators running a match. Teams are the vanilla scoreboard teams, so {@code /team} and the + * player list see the same ones. + */ +public final class TeamManager { + /** The colours a team can have. */ + public static final List COLORS = Arrays.stream(ChatFormatting.values()).filter(ChatFormatting::isColor).toList(); + + private TeamManager() { + } + + /** Operators only, as the original checked the scoreboard command's permission. */ + public static boolean mayManage(Player player) { + return player.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER); + } + + /** Switches the {@code pvp} game rule in every dimension and says which way it went. */ + public static void togglePvp(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + boolean pvp = !player.level().getGameRules().get(GameRules.PVP); + for (ServerLevel level : server.getAllLevels()) { + level.getGameRules().set(GameRules.PVP, pvp, server); + } + broadcast(server, Component.translatable(pvp ? "message.mcheli.team.pvp_on" : "message.mcheli.team.pvp_off")); + } + + /** {@code shuffleTeam}: every player online goes into one of the teams in turn, in random order. */ + public static void shuffle(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + Scoreboard scoreboard = server.getScoreboard(); + List teams = new ArrayList<>(scoreboard.getPlayerTeams()); + if (teams.isEmpty()) { + player.sendOverlayMessage(Component.translatable("message.mcheli.team.no_teams")); + return; + } + List players = new ArrayList<>(server.getPlayerList().getPlayers()); + Collections.shuffle(players); + Collections.shuffle(teams); + for (int i = 0; i < players.size(); i++) { + scoreboard.addPlayerToTeam(players.get(i).getScoreboardName(), teams.get(i % teams.size())); + } + broadcast(server, Component.translatable("message.mcheli.team.shuffled", teams.size())); + } + + /** The original's create team screen: a name, a colour and friendly fire. */ + public static void create(ServerPlayer player, String name, int color, boolean friendlyFire) { + Scoreboard scoreboard = player.level().getServer().getScoreboard(); + String trimmed = name.trim(); + if (trimmed.isEmpty() || trimmed.length() > 16 || trimmed.contains(" ") || scoreboard.getPlayerTeam(trimmed) != null) { + player.sendOverlayMessage(Component.translatable("message.mcheli.team.bad_name", trimmed)); + return; + } + PlayerTeam team = scoreboard.addPlayerTeam(trimmed); + team.setColor(COLORS.get(Math.floorMod(color, COLORS.size()))); + team.setAllowFriendlyFire(friendlyFire); + player.sendOverlayMessage(Component.translatable("message.mcheli.team.created", trimmed)); + } + + /** {@code jumpSpawnPoint}: every player back to where they would respawn. */ + public static void jumpToSpawn(ServerPlayer player) { + for (ServerPlayer target : List.copyOf(player.level().getServer().getPlayerList().getPlayers())) { + target.stopRiding(); + target.teleport(target.findRespawnPositionAndUseSpawnBlock(false, TeleportTransition.DO_NOTHING)); + } + } + + /** {@code destoryAllAircraft}: every loaded vehicle is gone, its crew left standing. */ + public static void destroyAll(ServerPlayer player) { + int count = 0; + for (ServerLevel level : player.level().getServer().getAllLevels()) { + for (AircraftEntity vehicle : level.getEntities(EntityTypeTest.forClass(AircraftEntity.class), e -> true)) { + vehicle.ejectPassengers(); + vehicle.discard(); + count++; + } + } + player.sendOverlayMessage(Component.translatable("message.mcheli.team.destroyed", count)); + } + + /** {@code entity} and everyone on its team. */ + public static List teammates(ServerPlayer entity) { + List out = new ArrayList<>(); + for (ServerPlayer player : entity.level().getServer().getPlayerList().getPlayers()) { + if (player == entity || player.isAlliedTo(entity)) { + out.add(player); + } + } + return out; + } + + private static void broadcast(MinecraftServer server, Component message) { + server.getPlayerList().broadcastSystemMessage(message, false); + } +} From 996a30e373a4d1e91a0d702973535f66ef0efe14 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:17 +0800 Subject: [PATCH 0563/1461] Add weapon/CasStrike.java to the 26.1 shared sources --- .../jnhprojects/mcheli/weapon/CasStrike.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java new file mode 100644 index 00000000..e78278a8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java @@ -0,0 +1,125 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A close air support run, {@code MCH_WeaponCAS}: three A-10 passes over a spot, 45 ticks apart, + * the second and third offset to either side. Vehicles with a CAS weapon drive theirs from their + * {@link WeaponSet}; the GLTD keeps one of these. + */ +public final class CasStrike { + /** Ticks from the call to the first pass: the time it takes the A-10 to arrive. */ + public static final int FIRST_PASS = 50; + private static final int BETWEEN_PASSES = 45; + private static final int PASSES = 3; + private static final RandomSource RANDOM = RandomSource.create(); + + private final String weapon; + private double x; + private double y; + private double z; + private int direction; + private @Nullable Entity shooter; + private @Nullable Entity user; + private int passes = PASSES; + private int ticksToNext; + + /** @param weapon the CAS weapon definition the A-10s fire, such as {@code a10gau8} */ + public CasStrike(String weapon) { + this.weapon = weapon; + } + + /** Whether passes are still to come. */ + public boolean isRunning() { + return passes < PASSES; + } + + /** + * Calls the strike on {@code at}. The A-10 comes in from one side of the caller's view, + * picked at random, never straight from behind or towards them. + */ + public void start(Vec3 at, float callerYaw, @Nullable Entity shooter, @Nullable Entity user) { + x = at.x; + y = at.y; + z = at.z; + direction = Math.floorMod(approach(callerYaw), 4); + this.shooter = shooter; + this.user = user; + passes = 0; + ticksToNext = FIRST_PASS; + } + + /** The original's approach: the quarter the caller faces, turned one way or the other. */ + static int approach(float callerYaw) { + int facing = (int) Mth.positiveModulo(callerYaw + 45.0F, 360.0F) / 90; + return facing + (RANDOM.nextBoolean() ? -1 : 1); + } + + public void tick(ServerLevel level, WeaponDefinition def) { + if (passes >= PASSES || --ticksToNext > 0) { + return; + } + Vec3 offset = passOffset(passes, direction); + spawnA10(level, weapon, def, direction, x + offset.x, y + 20.0, z + offset.z, shooter, user); + passes++; + ticksToNext = BETWEEN_PASSES; + } + + /** The first pass is dead on; the second strays to one side of the track, the third to the other. */ + static Vec3 passOffset(int pass, int direction) { + if (pass == 0) { + return Vec3.ZERO; + } + double sign = pass == 1 ? 1.0 : -1.0; + if (direction % 2 == 0) { + return new Vec3(RANDOM.nextDouble() * 10.0 * sign, 0.0, (RANDOM.nextDouble() - 0.5) * 10.0); + } + return new Vec3((RANDOM.nextDouble() - 0.5) * 10.0, 0.0, RANDOM.nextDouble() * 10.0 * sign); + } + + /** One A-10, starting 90 blocks short of the spot and flying over it at three blocks a tick. */ + static void spawnA10(ServerLevel level, String weapon, WeaponDefinition def, int direction, double x, double y, double z, + @Nullable Entity shooter, @Nullable Entity user) { + double mx = 0.0; + double mz = 0.0; + switch (direction) { + case 0 -> { + mz = 3.0; + z -= 90.0; + } + case 1 -> { + mx = -3.0; + x += 90.0; + } + case 2 -> { + mz = -3.0; + z += 90.0; + } + default -> { + mx = 3.0; + x -= 90.0; + } + } + A10Entity a10 = ModEntities.A10.get().create(level, EntitySpawnReason.TRIGGERED); + if (a10 == null) { + return; + } + a10.setPos(x, y, z); + a10.setup(weapon, direction, mx, mz, shooter, user); + a10.power = def.power; + a10.explosionPower = def.explosion; + a10.acceleration = def.acceleration; + level.addFreshEntity(a10); + ModSounds.playAt(level, x, y, z, "a-10_snd", 150.0F, 1.0F); + } +} From 6199d4e0094ffd0ea8b268860ba11d4c850131ff Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:17 +0800 Subject: [PATCH 0564/1461] Add weapon/Combat.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/weapon/Combat.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java new file mode 100644 index 00000000..204fe93f --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +/** Damage rules shared by bullets and explosions. */ +public final class Combat { + private Combat() { + } + + /** The {@code DamageFactor} kind a target belongs to, or {@code null} for anything else. */ + public static @Nullable String kind(Entity entity) { + return switch (entity) { + case Player p -> "player"; + case HelicopterEntity h -> "helicopter"; + case PlaneEntity p -> "plane"; + case TankEntity t -> "tank"; + case GroundVehicleEntity v -> "vehicle"; + default -> null; + }; + } + + public static float factor(@Nullable WeaponDefinition def, Entity target) { + String kind = kind(target); + return def != null && kind != null ? def.damageFactor(kind) : 1.0F; + } + + /** + * Deals weapon damage. The original scaled every living target's invulnerability window by + * {@code HurtResistantTime}, zero by default, so each round of a burst lands. + */ + public static boolean hurt(Entity target, DamageSource source, float damage, @Nullable WeaponDefinition def) { + if (!(target.level() instanceof ServerLevel level)) { + return false; + } + if (target instanceof LivingEntity living) { + living.invulnerableTime = 0; + } + return target.hurtServer(level, source, damage * factor(def, target)); + } + + /** The convention tag every loader fills, so modded panes count too. */ + private static final TagKey GLASS_PANES = TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("c", "glass_panes")); + + /** + * {@code BulletBreakableBlocks} with its default list: panes, grass, flowers, vines, wheat, + * sugar cane and lily pads, which a round shoots through. + */ + public static boolean isBulletBreakable(BlockState state) { + return state.is(GLASS_PANES) + || state.is(BlockTags.FLOWERS) + || state.is(Blocks.SHORT_GRASS) || state.is(Blocks.TALL_GRASS) + || state.is(Blocks.FERN) || state.is(Blocks.LARGE_FERN) + || state.is(Blocks.VINE) || state.is(Blocks.WHEAT) + || state.is(Blocks.SUGAR_CANE) || state.is(Blocks.LILY_PAD); + } +} From f02509688e9ff4ba00ae286c446f3edf0f57d144 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:17 +0800 Subject: [PATCH 0565/1461] Add weapon/ExplosionEffect.java to the 26.1 shared sources --- .../mcheli/weapon/ExplosionEffect.java | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java new file mode 100644 index 00000000..4f4b0dc8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java @@ -0,0 +1,232 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.HashSet; +import java.util.Set; + +/** + * The look of an MCHeli explosion, ported from {@code MCH_Explosion.effectExplosion} and + * {@code effectExplosionInWater}: a fireball, ground dust thrown up from the crater, and columns + * and rings of smoke that hang around for seconds. It runs on each client that receives the + * explosion; the burning debris arcs are real flares spawned by the server. + */ +public final class ExplosionEffect { + /** The largest crash fireball drawn; beyond this the particle count gets out of hand. */ + public static final float MAX_CRASH_SIZE = 12.0F; + /** The widest explosion flash, in blocks; past this it washes out the whole screen. */ + private static final float MAX_FLASH_RADIUS = 30.0F; + + private ExplosionEffect() { + } + + /** + * Not in the original: a crashing vehicle goes up in a far bigger show than its fuel explosion + * alone makes. The same shell of smoke as a missile hit in mid-air, a cluster of fireballs + * around it, a plume of smoke that keeps climbing for seconds and burning fuel thrown about. + * The fire glows for several seconds afterwards, which at night lights up the smoke above it. + */ + public static void crash(Level level, double x, double y, double z, float size) { + play(level, x, y, z, size, false); + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, Math.min(size * 3.0F, MAX_FLASH_RADIUS), 20), + true, x, y + size * 0.3, z, 0.0, 0.0, 0.0); + int fireballs = Math.max(3, (int) (size / 2.0F)); + for (int i = 0; i < fireballs; i++) { + double r = size * 0.5 * random.nextDouble(); + double a = random.nextDouble() * Math.PI * 2.0; + double fx = x + Math.cos(a) * r; + double fy = y + random.nextDouble() * size * 0.4; + double fz = z + Math.sin(a) * r; + level.addAlwaysVisibleParticle(ParticleTypes.EXPLOSION_EMITTER, true, fx, fy, fz, 1.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size * (0.4F + random.nextFloat() * 0.3F), + 80 + random.nextInt(60)), true, fx, fy, fz, 0.0, 0.01, 0.0); + } + for (int i = 0; i < size * 4; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + x + (random.nextDouble() - 0.5) * size * 0.6, y + random.nextDouble() * 2.0, z + (random.nextDouble() - 0.5) * size * 0.6, + (random.nextDouble() - 0.5) * 0.02, 0.05 + random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.02); + } + for (int i = 0; i < size * 10; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = (0.2 + random.nextDouble() * 0.4) * size / 6.0; + double up = random.nextDouble() * speed; + level.addAlwaysVisibleParticle(random.nextInt(4) == 0 ? ParticleTypes.LAVA : ParticleTypes.FLAME, true, x, y + 0.5, z, + Math.cos(a) * speed, up, Math.sin(a) * speed); + if (i % 4 == 0) { + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.5F + random.nextFloat() * 0.4F, + 20 + random.nextInt(20)), true, x, y + 0.5, z, Math.cos(a) * speed, up, Math.sin(a) * speed); + } + } + } + + /** + * Not in the original: the light of the blast itself, a white hot flash inside a wider red + * glow, and a few embers thrown out. Faint by day; at night it lights up the smoke. + */ + private static void flash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + float radius = Math.min(2.0F + size * 1.5F, MAX_FLASH_RADIUS); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, radius, 8 + (int) size), true, x, y, z, 0.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.EMBER, Math.min(radius * 1.6F, MAX_FLASH_RADIUS), + 14 + (int) (size * 2.0F)), true, x, y, z, 0.0, 0.0, 0.0); + int embers = Math.min((int) (size * 2.0F), 24); + for (int i = 0; i < embers; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = 0.2 + random.nextDouble() * 0.3 * Math.min(size, 4.0F); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 15 + random.nextInt(15)), true, x, y, z, Math.cos(a) * speed, 0.1 + random.nextDouble() * speed, Math.sin(a) * speed); + } + } + + public static void play(Level level, double x, double y, double z, float size, boolean inWater) { + if (size <= 0.0F) { + return; + } + if (inWater) { + splash(level, x, y, z, size); + return; + } + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(size >= 2.0F ? ParticleTypes.EXPLOSION_EMITTER : ParticleTypes.EXPLOSION, true, x, y, z, 1.0, 0.0, 0.0); + flash(level, x, y, z, size); + + int count = 0; + int puffEvery = (int) Math.max(size, 4.0F); + for (BlockPos pos : affectedBlocks(level, x, y, z, size)) { + count++; + double bx = pos.getX() + random.nextFloat(); + double by = pos.getY() + random.nextFloat(); + double bz = pos.getZ() + random.nextFloat(); + double mx = bx - x; + double my = by - y; + double mz = bz - z; + double d = Math.sqrt(mx * mx + my * my + mz * mz); + if (d == 0.0) { + continue; + } + double push = 0.5 / (d / size + 0.1) * (random.nextFloat() * random.nextFloat() + 0.3F) * 0.5; + mx = mx / d * push; + my = my / d * push; + mz = mz / d * push; + double px = (bx + x) / 2.0; + double py = (by + y) / 2.0; + double pz = (bz + z) / 2.0; + double angle = Math.toRadians(random.nextInt(360)); + + if (count % 4 == 0) { + // Dirt and stone thrown up from the ground under the blast. + BlockState ground = level.getBlockState(BlockPos.containing(px + 0.5, py - 0.5, pz + 0.5)); + if (!ground.isAir()) { + float force = Math.min(size / 3.0F, 2.0F) * (0.5F + random.nextFloat() * 0.5F); + BlockParticleOption dust = new BlockParticleOption(ParticleTypes.BLOCK, ground); + for (int i = 0; i < 3; i++) { + level.addAlwaysVisibleParticle(dust, true, px, py + 1.0, pz, + Math.sin(angle) * force * 0.5, 0.5 + my / 5.0 * force, Math.cos(angle) * force * 0.5); + } + } + } + if (size <= 1.0F || count % puffEvery == 0) { + // Smoke either shoots up as a column or rolls out along the ground. + if (random.nextBoolean()) { + my *= 3.0; + mx *= 0.1; + mz *= 0.1; + } else { + my *= 0.2; + mx *= 3.0; + mz *= 3.0; + } + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, px, py, pz, mx * 0.3, my * 0.3, mz * 0.3); + for (int i = 0; i < 2; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, px, py, pz, mx, my, mz); + } + if (random.nextInt(3) == 0) { + level.addAlwaysVisibleParticle(ParticleTypes.FLAME, true, px, py, pz, mx * 0.5, my * 0.5, mz * 0.5); + } + } + } + } + + /** + * The blocks the original's effect ray marched through: sixteen by sixteen rays from the + * centre, each running until block resistance used up its strength. + */ + @SuppressWarnings("deprecation") // The plain block resistance is what the original used here. + private static Set affectedBlocks(Level level, double x, double y, double z, float size) { + Set out = new HashSet<>(); + RandomSource random = level.getRandom(); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + for (int k = 0; k < 16; k++) { + if (i != 0 && i != 15 && j != 0 && j != 15 && k != 0 && k != 15) { + continue; + } + double dx = i / 15.0F * 2.0F - 1.0F; + double dy = j / 15.0F * 2.0F - 1.0F; + double dz = k / 15.0F * 2.0F - 1.0F; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + dx /= len; + dy /= len; + dz /= len; + float strength = size * (0.7F + random.nextFloat() * 0.6F); + double cx = x; + double cy = y; + double cz = z; + for (float step = 0.3F; strength > 0.0F; strength -= step * 0.75F) { + cursor.set(Mth.floor(cx), Mth.floor(cy), Mth.floor(cz)); + BlockState state = level.getBlockState(cursor); + if (!state.isAir()) { + strength -= (state.getBlock().getExplosionResistance() + 0.3F) * step; + } + if (strength > 0.0F) { + out.add(cursor.immutable()); + } + cx += dx * step; + cy += dy * step; + cz += dz * step; + } + } + } + } + return out; + } + + /** {@code effectExplosionInWater}: a dome of spray from the water around the charge. */ + private static void splash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + int range = Math.max((int) (size + 0.5), 1); + int ex = (int) (x + 0.5); + int ey = (int) (y + 0.5); + int ez = (int) (z + 0.5); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int dy = -range; dy <= range; dy++) { + for (int dx = -range; dx <= range; dx++) { + for (int dz = -range; dz <= range; dz++) { + if (dx * dx + dy * dy + dz * dz >= range * range) { + continue; + } + cursor.set(ex + dx, ey + dy, ez + dz); + if (!level.getFluidState(cursor).is(FluidTags.WATER)) { + continue; + } + for (int n = random.nextInt(2); n > 0; n--) { + double up = 1.0 - Math.sqrt(dx * dx + dz * dz) / range + random.nextFloat() * 0.4 * range * 0.4; + level.addAlwaysVisibleParticle(ParticleTypes.SPLASH, true, cursor.getX(), cursor.getY() + 1.0, cursor.getZ(), + (double) dx / range * (random.nextFloat() - 0.2), up, (double) dz / range * (random.nextFloat() - 0.2)); + level.addAlwaysVisibleParticle(ParticleTypes.BUBBLE, true, cursor.getX(), cursor.getY(), cursor.getZ(), 0.0, up * 0.5, 0.0); + } + } + } + } + } +} From 0f3a24acf031fd5a75f42e7aca198fa7c17680a2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:18 +0800 Subject: [PATCH 0566/1461] Add weapon/Explosions.java to the 26.1 shared sources --- .../jnhprojects/mcheli/weapon/Explosions.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java new file mode 100644 index 00000000..f7fe3e89 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java @@ -0,0 +1,217 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.network.ExplosionEffectPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerExplosion; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Port of {@code MCH_Explosion}. + * + *

The original explosion had two sizes: one for blocks and one, doubled, for entities, with its + * own damage curve and the weapon's per-target {@code DamageFactor}. Entity damage is computed + * here as it was; the blocks, particles and sound go through a vanilla explosion of the block size, + * so mob griefing rules, protection mods and the client effects all behave as players expect. + */ +public final class Explosions { + /** Players this far away still see the smoke of an explosion. */ + private static final double EFFECT_RANGE = 256.0; + /** Share of the blast knockback an empty vehicle takes; they are far heavier than a mob. */ + private static final double VEHICLE_PUSH = 0.25; + + private Explosions() { + } + + /** What an explosion request needs besides its position and size. */ + public record Params(@Nullable Entity exploder, @Nullable Entity cause, float size, float blockSize, + boolean flaming, boolean destroyBlocks, int fireSeconds, boolean inWater, + @Nullable WeaponDefinition weapon) { + } + + /** + * @return whether any entity took damage, which the shooter hears as a hit confirmation + */ + public static boolean explode(ServerLevel level, double x, double y, double z, Params p) { + Vec3 center = new Vec3(x, y, z); + boolean hit = damageEntities(level, center, p); + + boolean breakBlocks = p.destroyBlocks && p.blockSize > 0.0F && ServerConfig.explosionsBreakBlocks(); + float radius = breakBlocks ? p.blockSize : Math.max(p.size, 0.5F); + level.explode(p.exploder, explosionSource(level, p), new Calculator(p.inWater, level), + x, y, z, radius, p.flaming && breakBlocks && ServerConfig.explosionsStartFires(), + breakBlocks ? Level.ExplosionInteraction.MOB : Level.ExplosionInteraction.NONE); + ModNetwork.sendNear(level, x, y, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y, z, p.size, p.inWater)); + if (!p.inWater) { + throwDebris(level, x, y, z, p.size); + } + return hit; + } + + /** + * The look of a vehicle going up when it crashes: no damage or blocks of its own (the crash + * explosion does those), but the {@link ExplosionEffect#crash big fireball} on every nearby + * client and twice the burning debris. Drawn a little above the impact so its smoke spreads + * into the air the way a missile hit's does, rather than straight into the ground. + */ + public static void crashFireball(ServerLevel level, double x, double y, double z, float size) { + float visual = Math.clamp(size * 2.0F, 6.0F, ExplosionEffect.MAX_CRASH_SIZE); + double lift = visual * 0.25; + ModNetwork.sendNear(level, x, y + lift, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y + lift, z, visual, false, true)); + throwDebris(level, x, y, z, visual, 24); + } + + private static void throwDebris(ServerLevel level, double x, double y, double z, float size) { + throwDebris(level, x, y, z, size, 12); + } + + /** + * Big blasts throw burning debris that arcs away trailing smoke, which the original made of + * flares. One piece per point of size, up to {@code maxPieces}. + */ + private static void throwDebris(ServerLevel level, double x, double y, double z, float size, int maxPieces) { + if (size < 4.0F) { + return; + } + int pieces = Math.min((int) size * maxPieces / 12, maxPieces); + for (int i = 0; i < pieces; i++) { + double angle = Math.toRadians(level.getRandom().nextInt(360)); + double speed = Math.min(size / 12.0F, 0.6) * (0.5F + level.getRandom().nextFloat() * 0.5F); + FlareEntity debris = ModEntities.FLARE.get().create(level, EntitySpawnReason.TRIGGERED); + if (debris != null) { + double spread = size / 2.0; + debris.launch(x + (level.getRandom().nextDouble() - 0.5) * spread, y + 2.0, z + (level.getRandom().nextDouble() - 0.5) * spread, + Math.sin(angle) * speed, (1.0 + level.getRandom().nextFloat() * 0.2) * speed, Math.cos(angle) * speed, 2.0F, 0, false); + level.addFreshEntity(debris); + } + } + } + + private static DamageSource explosionSource(ServerLevel level, Params p) { + return level.damageSources().explosion(p.exploder, p.cause); + } + + /** {@code doExplosionA} for entities: twice the block size, the 1.7.10 damage curve and knockback. */ + private static boolean damageEntities(ServerLevel level, Vec3 center, Params p) { + float size = p.size * 2.0F; + if (size <= 0.0F) { + return false; + } + AABB area = new AABB(center, center).inflate(size + 1.0); + DamageSource source = explosionSource(level, p); + boolean hit = false; + for (Entity entity : level.getEntities(p.exploder, area)) { + double d7 = Math.sqrt(entity.distanceToSqr(center)) / size; + if (d7 > 1.0) { + continue; + } + double dx = entity.getX() - center.x; + double dy = entity.getEyeY() - center.y; + double dz = entity.getZ() - center.z; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist == 0.0) { + continue; + } + dx /= dist; + dy /= dist; + dz /= dist; + double exposure = ServerExplosion.getSeenPercent(center, entity); + double impact = (1.0 - d7) * exposure; + float damage = (int) ((impact * impact + impact) / 2.0 * 8.0 * size + 1.0); + if (damage > 0.0F && !(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) + && !(entity instanceof FallingBlockEntity)) { + // Our own rounds caught in the blast do not count as hitting something. + if (!(entity instanceof ProjectileEntity bullet) || bullet.user() != p.cause) { + hit = true; + } + } + Combat.hurt(entity, source, damage, p.weapon); + if (entity instanceof AircraftEntity vehicle) { + // Not in the original: a vehicle someone is driving is simulated on their client, + // so a push here only snaps it back a step. An empty one still rocks a little. + if (vehicle.getControllingPassenger() == null) { + entity.push(dx * impact * VEHICLE_PUSH * 0.4, dy * impact * VEHICLE_PUSH * 0.1, dz * impact * VEHICLE_PUSH * 0.4); + } + } else if (!(entity instanceof ProjectileEntity)) { + entity.push(dx * impact * 0.4, dy * impact * 0.1, dz * impact * 0.4); + } + if (damage > 0.0F && p.fireSeconds > 0) { + double fire = 1.0 - dist / size; + if (fire > 0.0) { + entity.igniteForSeconds((float) (fire * p.fireSeconds)); + } + } + } + return hit; + } + + /** + * Vanilla explosion settings for the block pass: no entity damage or knockback (done above), + * and under water the blocks crumble the way the original's water charges made them. + */ + private static final class Calculator extends ExplosionDamageCalculator { + private final boolean inWater; + private final Level level; + + Calculator(boolean inWater, Level level) { + this.inWater = inWater; + this.level = level; + } + + // Vanilla's own calculator reads the block's resistance the same deprecated way. + @SuppressWarnings("deprecation") + @Override + public Optional getBlockExplosionResistance(Explosion explosion, BlockGetter getter, BlockPos pos, + BlockState block, FluidState fluid) { + if (!inWater) { + return super.getBlockExplosionResistance(explosion, getter, pos, block, fluid); + } + if (block.isAir()) { + return Optional.empty(); + } + float resistance = block.getBlock().getExplosionResistance(); + return Optional.of(resistance * (level.getRandom().nextFloat() * 0.2F + 0.2F)); + } + + @Override + public boolean shouldDamageEntity(Explosion explosion, Entity entity) { + return false; + } + + @Override + public float getKnockbackMultiplier(Entity entity) { + return 0.0F; + } + } + + /** {@code playExplosionSound}: the bang of a round that has no explosive charge. */ + public static void playSound(Level level, double x, double y, double z) { + float pitch = (1.0F + (level.getRandom().nextFloat() - level.getRandom().nextFloat()) * 0.2F) * 0.7F; + level.playSound(null, x, y, z, SoundEvents.GENERIC_EXPLODE.value(), SoundSource.BLOCKS, 4.0F, pitch); + } +} From 71781b92bf00c004e0d8d04c736251f58f8a2b18 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:18 +0800 Subject: [PATCH 0567/1461] Add weapon/FlareDispenser.java to the 26.1 shared sources --- .../mcheli/weapon/FlareDispenser.java | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java new file mode 100644 index 00000000..12ddc658 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java @@ -0,0 +1,195 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.level.block.LevelEvent; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and smoke dischargers, ported from {@code MCH_Flare} and the flare half of + * {@code MCH_MissileDetector}. While a salvo burns, every missile homing on the vehicle within 60 + * blocks is decoyed. + * + *

The ten {@code FlareType} values pick a pattern: 1 random scatter, 2 and 3 to the sides and + * rear, 4 and 5 trailing, 10 a ring of smoke grenades. Unknown types fall back to type 1. + */ +public final class FlareDispenser { + private static final int DECOY_RANGE = 60; + + /** {@code num, interval, tickWait, tickEnable, numFlareMax} per type. */ + private static final int[][] TYPES = new int[11][]; + + static { + TYPES[1] = new int[]{1, 3, 200, 100, 16}; + TYPES[2] = new int[]{3, 5, 300, 200, 16}; + TYPES[3] = new int[]{2, 3, 200, 100, 16}; + TYPES[4] = new int[]{1, 3, 200, 100, 16}; + TYPES[5] = new int[]{2, 3, 200, 100, 16}; + TYPES[10] = new int[]{8, 1, 250, 60, 1}; + for (int i : new int[]{0, 6, 7, 8, 9}) { + TYPES[i] = TYPES[1]; + } + } + + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final RandomSource random = RandomSource.create(); + private int tick; + private int fired; + private int type; + private int typeIndex; + + FlareDispenser(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + } + + public boolean isFitted() { + return !def.flareTypes.isEmpty(); + } + + /** Waiting out the reload of the last salvo. */ + public boolean isPreparing() { + return tick != 0; + } + + /** The salvo is still burning, so incoming missiles are being decoyed. */ + public boolean isActive() { + int[] p = params(type); + return tick != 0 && tick > p[2] - p[3]; + } + + public int ticksLeft() { + return tick; + } + + public int cooldown() { + return params(type)[2]; + } + + public int currentType() { + return def.flareTypes.isEmpty() ? 0 : def.flareTypes.get(typeIndex % def.flareTypes.size()); + } + + public void nextType() { + if (!def.flareTypes.isEmpty()) { + typeIndex = (typeIndex + 1) % def.flareTypes.size(); + } + } + + /** {@code useFlare}: starts a salvo of the current type if the last one has finished. */ + public boolean use() { + if (!isFitted() || tick != 0 || vehicle.isDestroyed()) { + return false; + } + type = currentType(); + fired = 0; + // The client waited 50 ticks longer before showing the dispenser as ready again. + tick = params(type)[2] + (vehicle.level().isClientSide() ? 50 : 0); + return true; + } + + private static int[] params(int type) { + return type >= 0 && type < TYPES.length && TYPES[type] != null ? TYPES[type] : TYPES[1]; + } + + void tick() { + if (type <= 0 || vehicle.isRemoved()) { + return; + } + if (tick > 0) { + tick--; + } + int[] p = params(type); + if (!vehicle.level().isClientSide()) { + if (tick > 0 && tick % p[1] == 0 && fired < p[4]) { + Vec3 at = vehicle.toWorld(def.flarePosition); + spawnSalvo(new Vec3(at.x + vehicle.xo - vehicle.getX(), at.y + vehicle.yo - vehicle.getY(), at.z + vehicle.zo - vehicle.getZ())); + } + if (isActive()) { + decoyMissiles(); + } + } + } + + private void spawnSalvo(Vec3 at) { + fired++; + int[] p = params(type); + Vec3 m = vehicle.getDeltaMovement(); + double x = at.x - m.x * 2.0; + double y = at.y - m.y * 2.0 - 1.0; + double z = at.z - m.z * 2.0; + vehicle.level().levelEvent(LevelEvent.SOUND_DISPENSER_PROJECTILE_LAUNCH, net.minecraft.core.BlockPos.containing(x, y, z), 0); + for (int i = 0; i < p[0]; i++) { + double tx = 0.0; + double ty = m.y; + double tz = 0.0; + int fuse = 0; + double r = vehicle.getYRot(); + double fy = y; + switch (type) { + case 1 -> { + tx = Mth.sin(random.nextFloat() * 360.0F); + tz = Mth.cos(random.nextFloat() * 360.0F); + } + case 2, 3 -> { + if (i == 0) { + r += 90.0; + } else if (i == 1) { + r -= 90.0; + } else if (i == 2) { + r += 180.0; + } + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 0.6; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 0.6; + } + case 4 -> { + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 1.3; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 1.3; + } + case 5 -> { + r = Math.toRadians(r); + tx = (-Math.sin(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + tz = (Math.cos(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + } + default -> { + } + } + tx += m.x; + ty += m.y / 2.0; + tz += m.z; + if (type == 10) { + r += 360.0 / p[0] / 2.0 + i * (360.0 / p[0]); + r = Math.toRadians(r); + tx = -Math.sin(r) * 2.0; + tz = Math.cos(r) * 2.0; + ty = 0.7; + fy += 2.0; + fuse = 10; + } + FlareEntity flare = ModEntities.FLARE.get().create(vehicle.level(), EntitySpawnReason.TRIGGERED); + if (flare != null) { + flare.launch(x, fy, z, tx * 0.5, ty * 0.5, tz * 0.5, 6.0F, fuse, type == 4); + vehicle.level().addFreshEntity(flare); + } + } + } + + /** {@code destroyMissile}: missiles homing on this vehicle or its crew lose their target. */ + private void decoyMissiles() { + for (ProjectileEntity missile : vehicle.level().getEntitiesOfClass(ProjectileEntity.class, + vehicle.getBoundingBox().inflate(DECOY_RANGE), p -> p.target() != null)) { + if (missile.target() == vehicle || vehicle.hasPassenger(missile.target())) { + missile.decoy(); + } + } + } +} From 0e10ce10b1866eaa2fb53800431975bac49d0cce Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:19 +0800 Subject: [PATCH 0568/1461] Add weapon/Guidance.java to the 26.1 shared sources --- .../jnhprojects/mcheli/weapon/Guidance.java | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java new file mode 100644 index 00000000..b8142e92 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java @@ -0,0 +1,220 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Predicate; + +/** + * Lock-on for seeking missiles, ported from {@code MCH_WeaponGuidanceSystem}. It runs on the + * client of the crew member aiming; the locked entity travels to the server with the fire request. + * + *

A target is acquired inside a cone around the aim, then the lock count climbs while it stays + * in the cone. Stealth shrinks both the range and the cone and stretches the time to lock. + */ +public final class Guidance { + public final boolean canLockInAir; + public final boolean canLockOnGround; + public final boolean canLockInWater = false; + public final boolean ridableOnly; + public final double lockRange = 200.0; + public final int lockAngle = 5; + private final int lockCountMax; + private final @Nullable Predicate filter; + + private @Nullable Entity target; + private @Nullable Entity lastLocked; + private int lockCount; + private int prevLockCount; + private int continueLockCount; + private int soundCount; + + public Guidance(boolean air, boolean ground, boolean ridableOnly, int lockTime, @Nullable Predicate filter) { + this.canLockInAir = air; + this.canLockOnGround = ground; + this.ridableOnly = ridableOnly; + this.lockCountMax = Math.max(lockTime, 1); + this.filter = filter; + } + + public int lockCount() { + return lockCount; + } + + public int lockCountMax() { + float stealth = stealth(target); + return (int) (lockCountMax + lockCountMax * stealth); + } + + public @Nullable Entity target() { + return lockCount > 0 && target != null && !target.isRemoved() ? target : null; + } + + public @Nullable Entity lastLocked() { + return lastLocked; + } + + public boolean isLocked() { + return lockCount == lockCountMax() && lastLocked != null; + } + + /** Called every client tick: a lock decays unless {@link #lock} kept it alive this tick. */ + public void tick() { + if (lockCount != prevLockCount) { + prevLockCount = lockCount; + } else { + lockCount = 0; + prevLockCount = 0; + } + } + + /** + * Advances the lock for one tick of aiming from {@code user}'s view. + * + * @param eye where the view is seen from. The original used the player's position, which on a + * 1.7.10 client was eye height; gunner sights and launcher cameras sit higher still, + * and a 5 degree cone measured from the wrong point misses targets close by. + * @return true once the lock is complete; {@link #lastLocked()} is the target to send + */ + public boolean lock(Entity user, Vec3 eye) { + if (lockCount == 0) { + acquire(user, eye); + return false; + } + if (target == null || target.isRemoved() || !inLockableState(target)) { + clear(); + return false; + } + double range = lockRange * (1.0F - stealth(target)); + if (user.distanceToSqr(target) >= range * range) { + clear(); + return false; + } + soundCount = (soundCount + 1) % 15; + if (inCone(eye, user.getYRot(), user.getXRot(), target, lockAngle)) { + if (lockCount < lockCountMax()) { + lockCount++; + } + } else if (continueLockCount > 0) { + continueLockCount--; + if (continueLockCount <= 0 && lockCount > 0) { + lockCount--; + } + } else { + continueLockCount = 0; + lockCount--; + } + if (lockCount >= lockCountMax()) { + if (continueLockCount <= 0) { + continueLockCount = Math.min(lockCountMax() / 3, 20); + } + lastLocked = target; + prevLockCount = lockCount - 1; + return true; + } + return false; + } + + /** True on the tick the target should hear that it is being tracked. */ + public boolean shouldNotifyTarget() { + return lockCount > 0 && soundCount == 1; + } + + private void acquire(Entity user, Vec3 eye) { + Entity best = null; + double bestDistance = lockRange * lockRange * 2.0; + for (Entity entity : user.level().getEntities(user, user.getBoundingBox().inflate(lockRange), this::canLock)) { + double d = entity.distanceToSqr(user); + float stealth = 1.0F - stealth(entity); + double range = lockRange * stealth; + float angle = lockAngle * (stealth / 2.0F + 0.5F); + if (d < range * range && d < bestDistance && inCone(eye, user.getYRot(), user.getXRot(), entity, angle)) { + Vec3 to = new Vec3(entity.getX(), entity.getY() + entity.getBbHeight() / 2.0F, entity.getZ()); + HitResult hit = user.level().clip(new ClipContext(eye, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, user)); + if (hit.getType() == HitResult.Type.MISS) { + best = entity; + bestDistance = d; + } + } + } + target = best; + if (best != null) { + lockCount++; + } + } + + public void clear() { + target = null; + lockCount = 0; + continueLockCount = 0; + soundCount = 0; + } + + private boolean canLock(Entity entity) { + if (ridableOnly && entity instanceof Player && entity.getVehicle() == null) { + return false; + } + if (!(entity instanceof LivingEntity) && !(entity instanceof AircraftEntity)) { + return false; + } + if (filter != null && !filter.test(entity)) { + return false; + } + return inLockableState(entity); + } + + private boolean inLockableState(Entity entity) { + if (!canLockInWater && entity.isInWater()) { + return false; + } + boolean onGround = isOnGround(entity); + return onGround ? canLockOnGround : canLockInAir; + } + + /** On the ground, or less than twelve blocks above something solid. */ + public static boolean isOnGround(Entity entity) { + if (entity.onGround()) { + return true; + } + BlockPos.MutableBlockPos pos = BlockPos.containing(entity.getX() + 0.5, entity.getY() + 0.5, entity.getZ() + 0.5).mutable(); + for (int i = 0; i < 12; i++) { + if (!entity.level().getBlockState(pos).isAir() && !entity.level().getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + pos.move(0, -1, 0); + } + return false; + } + + public static float stealth(@Nullable Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + return vehicle.stealth(); + } + return entity != null && entity.getVehicle() instanceof AircraftEntity vehicle ? vehicle.stealth() : 0.0F; + } + + /** {@code inLockRange}: whether {@code target} is within {@code angle} degrees of the aim. */ + public static boolean inCone(Entity from, float yaw, float pitch, Entity target, float angle) { + return inCone(from.position(), yaw, pitch, target, angle); + } + + public static boolean inCone(Vec3 from, float yaw, float pitch, Entity target, float angle) { + double dx = target.getX() - from.x; + double dy = target.getY() + target.getBbHeight() / 2.0F - from.y; + double dz = target.getZ() - from.z; + float targetYaw = (float) Math.toDegrees(Math.atan2(dz, dx)); + float diffYaw = Mth.positiveModulo(targetYaw - yaw - 90.0F, 360.0F); + float targetPitch = -(float) Math.toDegrees(Math.atan2(dy, Math.sqrt(dx * dx + dz * dz))); + float diffPitch = targetPitch - pitch; + return (diffYaw < angle || diffYaw > 360.0F - angle) && Math.abs(diffPitch) < angle; + } +} From d172c6079c119dd7edf505b1cb0631a9cea12d45 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:19 +0800 Subject: [PATCH 0569/1461] Add weapon/LegacyItems.java to the 26.1 shared sources --- .../mcheli/weapon/LegacyItems.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java new file mode 100644 index 00000000..f763f73f --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import org.jspecify.annotations.Nullable; + +/** + * Maps the 1.7.10 item names and damage values in the content files to today's items. Names + * that still exist resolve directly; the few that were renamed or flattened are listed here. + */ +public final class LegacyItems { + private static final String[] DYES = { + "ink_sac", "red_dye", "green_dye", "cocoa_beans", "lapis_lazuli", "purple_dye", "cyan_dye", "light_gray_dye", + "gray_dye", "pink_dye", "lime_dye", "yellow_dye", "light_blue_dye", "magenta_dye", "orange_dye", "bone_meal"}; + + private LegacyItems() { + } + + public static @Nullable Item item(String name, int damage) { + if (name.isEmpty()) { + return null; + } + String id = switch (name) { + case "dye" -> DYES[Math.floorMod(damage, DYES.length)]; + case "boat" -> "oak_boat"; + case "reeds" -> "sugar_cane"; + case "wool" -> "white_wool"; + case "log" -> "oak_log"; + case "planks" -> "oak_planks"; + default -> name.startsWith("minecraft:") ? name.substring("minecraft:".length()) : name; + }; + Identifier key = Identifier.tryParse(id.contains(":") ? id : "minecraft:" + id); + if (key == null) { + return null; + } + Item item = BuiltInRegistries.ITEM.getValue(key); + return item == Items.AIR ? null : item; + } +} From 842b349a1073c0773b11fcb211d5489fcdab7448 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:20 +0800 Subject: [PATCH 0570/1461] Add weapon/ModDamageTypes.java to the 26.1 shared sources --- .../mcheli/weapon/ModDamageTypes.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java new file mode 100644 index 00000000..b4007926 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * Damage types defined in {@code data/mcheli/damage_type}. Bullets and missiles use their own type + * so death messages name the gun instead of calling it a thrown snowball, which is what the + * original's {@code causeThrownDamage} amounted to. + */ +public final class ModDamageTypes { + public static final ResourceKey BULLET = + ResourceKey.create(Registries.DAMAGE_TYPE, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "bullet")); + + private ModDamageTypes() { + } + + public static DamageSource bullet(Level level, Entity projectile, @Nullable Entity owner) { + return new DamageSource(level.registryAccess().lookupOrThrow(Registries.DAMAGE_TYPE).getOrThrow(BULLET), projectile, owner); + } +} From 4e55c098c8ac8fd3ad9f77f5eca08ee8742f6386 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:20 +0800 Subject: [PATCH 0571/1461] Add weapon/Resupply.java to the 26.1 shared sources --- .../jnhprojects/mcheli/weapon/Resupply.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java new file mode 100644 index 00000000..533f5fba --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** + * Resupplying a weapon from a crew member's own items, the {@code supplyAmmo} path of the + * original's vehicle screen. One supply costs the weapon's {@code Item} lines and adds + * {@code SuppliedNum} rounds; it only works while the vehicle stands on something. + */ +public final class Resupply { + private Resupply() { + } + + /** {@code canPlayerSupplyAmmo}: whether {@link #supply} would do anything right now. */ + public static boolean canSupply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.set(weapon) : null; + if (set == null || vehicle.isDestroyed() || player.getVehicle() != vehicle || !vehicle.canSupply()) { + return false; + } + if (set.ammo() + set.restAmmo() >= set.maxAmmo()) { + return false; + } + return player.getAbilities().instabuild || canPay(player.getInventory(), set.definition()); + } + + /** Pays for and adds one supply of {@code weapon}. Server only. */ + public static boolean supply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + if (!canSupply(player, vehicle, weapon)) { + return false; + } + WeaponSet set = vehicle.weapons().set(weapon); + if (!player.getAbilities().instabuild) { + pay(player.getInventory(), set.definition()); + } + set.supply(); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + return true; + } + + private static boolean canPay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null || inventory.countItem(item) < cost.count()) { + return false; + } + } + return true; + } + + private static void pay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + int left = cost.count(); + for (int i = 0; i < inventory.getContainerSize() && left > 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.is(item)) { + continue; + } + if (item == Items.WATER_BUCKET || item == Items.LAVA_BUCKET) { + // Buckets give their bucket back, one at a time. + inventory.setItem(i, new ItemStack(Items.BUCKET)); + left--; + } else { + int take = Math.min(left, stack.getCount()); + stack.shrink(take); + left -= take; + } + } + } + inventory.setChanged(); + } +} From 8ef30eed8c84ed25c6c8aff8c2f5e0e53edc76e2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:20 +0800 Subject: [PATCH 0572/1461] Add weapon/ServerFiring.java to the 26.1 shared sources --- .../mcheli/weapon/ServerFiring.java | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java new file mode 100644 index 00000000..3f867744 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java @@ -0,0 +1,418 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * What each weapon type does when the server fires it: the {@code shot} methods of the + * {@code MCH_Weapon*} classes. Every method returns whether a round was used. + */ +public final class ServerFiring implements WeaponSet.Trigger { + public static final ServerFiring INSTANCE = new ServerFiring(); + private static final RandomSource RANDOM = RandomSource.create(); + + private ServerFiring() { + } + + @Override + public boolean fire(WeaponSet set, int mount, Shot shot) { + if (!(shot.shooter.level() instanceof ServerLevel level)) { + return false; + } + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_1, MACHINE_GUN_2 -> gun(level, set, shot); + case ROCKET -> rocket(level, set, shot); + case BOMB -> bomb(level, set, shot); + case AA_MISSILE -> aaMissile(level, set, mount, shot); + case AS_MISSILE -> asMissile(level, set, shot); + case AT_MISSILE -> atMissile(level, set, mount, shot); + case TV_MISSILE -> tvMissile(level, set, mount, shot); + case TORPEDO -> torpedo(level, set, shot); + case CAS -> cas(level, set, shot); + case MARKER_ROCKET -> markerRocket(level, set, shot); + case DISPENSER -> dispenser(level, set, shot); + case TARGETING_POD -> targetingPod(level, set, shot); + case DUMMY, SMOKE -> false; + case null -> false; + }; + } + + private static @Nullable T spawn(ServerLevel level, EntityType type, WeaponSet set, Shot shot, + Vec3 dir, float yaw, float pitch, double acceleration) { + T e = type.create(level, EntitySpawnReason.TRIGGERED); + if (e == null) { + return null; + } + e.launch(shot.x, shot.y, shot.z, dir.x, dir.y, dir.z, yaw, pitch, acceleration); + e.setWeapon(set.name()); + e.setParameters(set.definition(), shot.shooter, shot.user); + return e; + } + + private static Vec3 forward(float yaw, float pitch) { + return Vec3.directionFromRotation(pitch, yaw); + } + + private static void sound(WeaponSet set, Entity at) { + sound(set, at, soundName(set)); + } + + private static String soundName(WeaponSet set) { + return set.ownSound != null ? set.ownSound.sound() : set.definition().sound; + } + + private static void sound(WeaponSet set, Entity at, String name) { + if (set.soundAllowed) { + WeaponDefinition w = set.definition(); + float rnd = w.soundPitchRandom; + float pitch = set.ownSound != null && set.ownSound.pitch() != null ? set.ownSound.pitch() : w.soundPitch; + ModSounds.playAt(at, name, w.soundVolume, pitch * (1.0F - rnd) + RANDOM.nextFloat() * rnd); + } + } + + // Guns, rockets, bombs + + private static boolean gun(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + BulletEntity e = spawn(level, ModEntities.BULLET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, w.acceleration); + if (e == null) { + return false; + } + if (w.type == WeaponType.MACHINE_GUN_2 && w.modeNum >= 2) { + // Mode 0 fires ball ammunition, which only bangs; mode 1 is high explosive. + e.explosionPower = shot.option1 == 0 ? -w.explosion : w.explosion; + } + Vec3 m = e.getDeltaMovement(); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean rocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + RocketEntity e = spawn(level, ModEntities.ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + if (shot.option1 == 0 && set.definition().modeNum > 1) { + e.piercing = 0; + } + level.addFreshEntity(e); + return true; + } + + private static boolean bomb(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + if (w.destruct) { + // A drone's self destruct charge. + if (shot.shooter instanceof HelicopterEntity drone && drone.definition() != null && drone.definition().uav) { + Explosions.explode(level, drone.getX(), drone.getY(), drone.getZ(), + new Explosions.Params(null, shot.user, w.explosion, w.explosionBlock, w.flaming, true, 0, false, null)); + sound(set, shot.shooter); + drone.ejectPassengers(); + drone.discard(); + } + return true; + } + sound(set, shot.shooter); + Vec3 motion = shot.shooter.getDeltaMovement(); + BombEntity e = spawn(level, ModEntities.BOMB.get(), set, shot, motion, shot.shooter.getYRot(), 0.0F, w.acceleration); + if (e == null) { + return false; + } + e.setDeltaMovement(motion); + level.addFreshEntity(e); + return true; + } + + // Missiles + + private static float fixedYaw(WeaponSet set, int mount) { + return set.mounts().get(mount).yaw(); + } + + private static float fixedPitch(WeaponSet set, int mount) { + return set.mounts().get(mount).pitch(); + } + + private static boolean aaMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + sound(set, shot.shooter); + float yaw = shot.shooter.getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + AAMissileEntity e = spawn(level, ModEntities.AA_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + level.addFreshEntity(e); + return true; + } + + /** Ray from the shooter along the aimer's view, as the original's AS missile and CAS sights. */ + private static @Nullable BlockHitResult sight(ServerLevel level, Shot shot, double eyeHeight, double range) { + Entity aimer = shot.aimer(); + Vec3 from = shot.shooter.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(forward(aimer.getYRot(), aimer.getXRot()).scale(range)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + return hit.getType() == HitResult.Type.BLOCK ? hit : null; + } + + private static boolean nextToWater(ServerLevel level, BlockPos pos) { + if (level.getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + for (Direction d : Direction.values()) { + if (level.getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + private static boolean asMissile(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 1.62, 250.0); + if (hit == null || nextToWater(level, hit.getBlockPos())) { + return false; + } + Entity aimer = shot.aimer(); + ASMissileEntity e = spawn(level, ModEntities.AS_MISSILE.get(), set, shot, forward(aimer.getYRot(), aimer.getXRot()), + aimer.getYRot(), aimer.getXRot(), set.definition().acceleration); + if (e == null) { + return false; + } + e.targetX = hit.getLocation().x; + e.targetY = hit.getLocation().y; + e.targetZ = hit.getLocation().z; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean atMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + ATMissileEntity e = spawn(level, ModEntities.AT_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + e.guidanceType = shot.option2; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean tvMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.aimer().getXRot() + fixedPitch(set, mount); + boolean tvGuided = shot.option1 == 0; + float acceleration = set.definition().acceleration * (tvGuided ? 1.0F : 1.5F); + TvMissileEntity e = spawn(level, ModEntities.TV_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, acceleration); + if (e == null) { + return false; + } + level.addFreshEntity(e); + if (shot.shooter instanceof AircraftEntity vehicle) { + vehicle.setTvMissile(e); + } + sound(set, shot.shooter); + return true; + } + + private static boolean torpedo(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Vec3 vehicleMotion = shot.shooter.getDeltaMovement(); + float yaw = shot.yaw; + float pitch = shot.pitch; + Vec3 target = null; + if (w.guidedTorpedo) { + Entity aimer = shot.aimer(); + yaw = aimer.getYRot(); + pitch = aimer.getXRot(); + Vec3 from = aimer.position(); + Vec3 to = from.add(forward(yaw, pitch).scale(150.0)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + if (hit.getType() != HitResult.Type.BLOCK || !nextToWater(level, hit.getBlockPos())) { + return false; + } + target = hit.getLocation(); + } + Vec3 m = forward(yaw, pitch).scale(w.acceleration).add(vehicleMotion); + double speed = m.length(); + TorpedoEntity e = spawn(level, ModEntities.TORPEDO.get(), set, shot, m, yaw, 0.0F, speed); + if (e == null) { + return false; + } + e.setDeltaMovement(m); + e.accelerationInWater = w.accelerationInWater; + if (target != null) { + e.targetX = target.x; + e.targetY = target.y; + e.targetZ = target.z; + } + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + // Support weapons + + private static boolean markerRocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + MarkerRocketEntity e = spawn(level, ModEntities.MARKER_ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + e.setStatus(1); + level.addFreshEntity(e); + return true; + } + + private static boolean dispenser(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + DispensedItemEntity e = spawn(level, ModEntities.DISPENSED_ITEM.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + Vec3 m = shot.shooter.getDeltaMovement().add(e.getDeltaMovement().scale(0.5)); + e.setDeltaMovement(m); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + return true; + } + + /** + * Spots everything of the weapon's {@code Target} kinds inside its cone, which then glows for + * {@code MarkTime}. Marking a block for the team needs the team manager of a later phase. + */ + private static boolean targetingPod(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Entity aimer = shot.aimer(); + if ((w.target & WeaponDefinition.TARGET_BLOCK) != 0) { + // {@code MCH_Multiplay.markPoint}: the block under the crew's view, 300 blocks out, goes to the team. + boolean marked = aimer instanceof ServerPlayer player && Spotting.markPoint(player, new Vec3(shot.x, shot.y, shot.z), 300.0); + sound(set, aimer, marked ? soundName(set) : "ng"); + return true; + } + List spotted = new ArrayList<>(); + double range = Math.max(w.length, 1.0F); + for (LivingEntity entity : level.getEntitiesOfClass(LivingEntity.class, aimer.getBoundingBox().inflate(range))) { + if (entity == aimer || shot.shooter.hasPassenger(entity) || aimer.distanceToSqr(entity) > range * range + || !matchesTarget(entity, w.target) || !Guidance.inCone(aimer, aimer.getYRot(), aimer.getXRot(), entity, w.angle)) { + continue; + } + spotted.add(entity.getId()); + } + if (aimer instanceof ServerPlayer player) { + Spotting.tellTeam(player, spotted, Math.max(w.markTime, 20)); + } + sound(set, shot.shooter, !spotted.isEmpty() ? soundName(set) : "ng"); + return true; + } + + private static boolean matchesTarget(LivingEntity entity, int bits) { + Entity vehicle = entity.getVehicle(); + if (vehicle instanceof PlaneEntity) { + return (bits & WeaponDefinition.TARGET_PLANES) != 0; + } + if (vehicle instanceof HelicopterEntity) { + return (bits & WeaponDefinition.TARGET_HELICOPTERS) != 0; + } + if (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity) { + return (bits & WeaponDefinition.TARGET_GROUND_VEHICLES) != 0; + } + if (entity instanceof Player) { + return (bits & WeaponDefinition.TARGET_PLAYERS) != 0; + } + if (entity instanceof Enemy) { + return (bits & WeaponDefinition.TARGET_MONSTERS) != 0; + } + return (bits & WeaponDefinition.TARGET_OTHERS) != 0; + } + + // Close air support + + private static boolean cas(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 2.0, 150.0); + if (hit == null) { + return false; + } + Vec3 at = hit.getLocation(); + set.casX = at.x; + set.casY = at.y; + set.casZ = at.z; + set.casDirection = Math.floorMod(CasStrike.approach(shot.aimer().getYRot()), 4); + set.casUser = shot.user; + set.casShooter = shot.shooter; + set.casStartTick = CasStrike.FIRST_PASS; + set.casCount = 0; + if (shot.user instanceof ServerPlayer player) { + ModSounds.playTo(player, set.definition().sound, 1.0F, 1.0F); + } + return true; + } + + /** {@code MCH_WeaponCAS.update}: three passes, 45 ticks apart, the later ones offset to the sides. */ + static void tick(AircraftEntity vehicle, WeaponSet set) { + if (set.casCount >= 3 || set.countWait == 0 || set.ticksSinceShot(0) != set.casStartTick + || !(vehicle.level() instanceof ServerLevel level)) { + return; + } + Vec3 offset = CasStrike.passOffset(set.casCount, set.casDirection); + CasStrike.spawnA10(level, set.name(), set.definition(), set.casDirection, set.casX + offset.x, set.casY + 20.0, + set.casZ + offset.z, set.casShooter, set.casUser); + set.casStartTick = set.ticksSinceShot(0) + 45; + set.casCount++; + } +} From ff6f2512528db871f9f9bed9e57ac918b97cfbec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:21 +0800 Subject: [PATCH 0573/1461] Add weapon/Shot.java to the 26.1 shared sources --- .../com/jnhprojects/mcheli/weapon/Shot.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java new file mode 100644 index 00000000..6a9f53cf --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java @@ -0,0 +1,40 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +/** + * One trigger pull, the port of {@code MCH_WeaponParam}. The weapon set fills in the aim and the + * muzzle position; the client fills in the two option values (a locked target, a fire mode) and + * the server receives them with the fire request. + */ +public final class Shot { + /** The vehicle firing, or the player for hand held weapons. */ + public final Entity shooter; + /** Who pulled the trigger. Damage and kills are credited to this entity. */ + public final @Nullable Entity user; + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + public float roll; + public int option1; + public int option2; + public boolean infiniteAmmo; + /** Set by the weapon set when the magazine ran dry with this shot. */ + public boolean reload; + + public Shot(Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + this.x = shooter.getX(); + this.y = shooter.getY(); + this.z = shooter.getZ(); + } + + /** The entity whose view steers guided weapons: the user if there is one, else the shooter. */ + public Entity aimer() { + return user != null ? user : shooter; + } +} From fade3bcf327cc00789d23df94cb288638031f8b9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:21 +0800 Subject: [PATCH 0574/1461] Add weapon/Spotting.java to the 26.1 shared sources --- .../jnhprojects/mcheli/weapon/Spotting.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java new file mode 100644 index 00000000..b9331154 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.network.MarkerPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +/** + * Spotting with the rangefinder, {@code MCH_Multiplay.spotEntity} and {@code markPoint}. + * + *

Spotted targets and marked points go to the spotter and their team only, as screen markers + * the HUD draws ({@code MCH_GuiTargetMarker}); nobody else sees them. + */ +public final class Spotting { + // The original's target filter bits. + public static final int OTHERS = 1; + public static final int MONSTERS = 2; + public static final int PLAYERS = 4; + public static final int GROUND_VEHICLES = 8; + public static final int HELICOPTERS = 16; + public static final int PLANES = 32; + + /** {@code RangeFinderSpotDist}. */ + public static final double RANGE = 400.0; + /** {@code RangeFinderSpotTime} in ticks, for players and vehicles. */ + private static final int PLAYER_TIME = 15 * 20; + /** Mobs stay marked for a minute. */ + private static final int MOB_TIME = 60 * 20; + /** Half the width of the cone a spot covers, in degrees: the original's angle times two. */ + private static final double CONE = 40.0; + + private Spotting() { + } + + static boolean matches(int filter, Entity entity) { + int bit; + if (entity instanceof PlaneEntity) { + bit = PLANES; + } else if (entity instanceof HelicopterEntity) { + bit = HELICOPTERS; + } else if (entity instanceof AircraftEntity) { + bit = GROUND_VEHICLES; + } else if (entity instanceof Player) { + bit = PLAYERS; + } else if (entity instanceof Enemy) { + bit = MONSTERS; + } else if (entity instanceof LivingEntity) { + bit = OTHERS; + } else { + return false; + } + return (filter & bit) != 0; + } + + /** + * Marks what {@code spotter} sees within {@link #RANGE} and the cone around their view: never + * a teammate, and only targets in plain sight. + * + * @return whether anything was spotted + */ + public static boolean spot(ServerPlayer spotter, int filter) { + ServerLevel level = spotter.level(); + Vec3 eye = spotter.getEyePosition(); + Vec3 look = spotter.getLookAngle(); + int ticks = (filter & ~(OTHERS | MONSTERS)) == 0 ? MOB_TIME : PLAYER_TIME; + List spotted = new ArrayList<>(); + for (Entity entity : level.getEntities(spotter, spotter.getBoundingBox().inflate(RANGE), e -> matches(filter, e))) { + if (isFriendly(spotter, entity)) { + continue; + } + Vec3 to = entity.getBoundingBox().getCenter().subtract(eye); + double dist = to.length(); + if (dist < 1.0 || dist > RANGE || Math.toDegrees(Math.acos(to.scale(1.0 / dist).dot(look))) > CONE) { + continue; + } + if (level.clip(new ClipContext(eye, entity.getBoundingBox().getCenter(), ClipContext.Block.COLLIDER, + ClipContext.Fluid.NONE, spotter)).getType() == HitResult.Type.BLOCK) { + continue; + } + spotted.add(entity.getId()); + } + tellTeam(spotter, spotted, ticks); + return !spotted.isEmpty(); + } + + /** Sends the spotted entities to {@code spotter} and their team, {@code MCH_PacketNotifySpotedEntity}. */ + public static void tellTeam(ServerPlayer spotter, List entities, int ticks) { + if (entities.isEmpty()) { + return; + } + MarkerPacket packet = MarkerPacket.spotted(entities.stream().mapToInt(Integer::intValue).toArray(), ticks); + for (ServerPlayer player : TeamManager.teammates(spotter)) { + ModNetwork.sendToPlayer(player, packet); + } + } + + /** Teammates, and a vehicle crewed by one, are never spotted. */ + private static boolean isFriendly(ServerPlayer spotter, Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + for (Entity crew : vehicle.getPassengers()) { + if (crew.isAlliedTo(spotter)) { + return true; + } + } + return false; + } + return entity.isAlliedTo(spotter); + } + + /** + * Tells the spotter and their team where they are looking, as block coordinates. + * + * @return whether the view ended on a block within range + */ + public static boolean markPoint(ServerPlayer spotter) { + return markPoint(spotter, spotter.getEyePosition(), RANGE); + } + + /** + * As above, looking from {@code from} along the spotter's view: a targeting pod marks from + * the vehicle, out to {@code range}. + */ + public static boolean markPoint(ServerPlayer spotter, Vec3 from, double range) { + ServerLevel level = spotter.level(); + BlockHitResult hit = level.clip(new ClipContext(from, from.add(spotter.getLookAngle().scale(range)), + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, spotter)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + BlockPos pos = hit.getBlockPos(); + MarkerPacket marker = MarkerPacket.point(pos); + Component message = Component.translatable("message.mcheli.mark_point", spotter.getDisplayName(), pos.getX(), pos.getY(), pos.getZ()); + for (ServerPlayer player : level.getServer().getPlayerList().getPlayers()) { + if (player == spotter || player.isAlliedTo(spotter)) { + player.sendSystemMessage(message); + ModNetwork.sendToPlayer(player, marker); + } + } + return true; + } +} From 95d4532af32a39c05f983e6bc00f7f800d56ac4a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:22 +0800 Subject: [PATCH 0575/1461] Add weapon/VehicleWeapons.java to the 26.1 shared sources --- .../mcheli/weapon/VehicleWeapons.java | 597 ++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java new file mode 100644 index 00000000..85dee9f8 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java @@ -0,0 +1,597 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.weapon.MuzzleFlash; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.WeaponStatePacket; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The armament of one vehicle: its weapon sets, which one each seat has selected, the flares, and + * the effects every client derives from the synced firing state. Ported from the weapon handling + * in {@code MCH_EntityAircraft} ({@code createWeapon}, {@code switchWeapon}, + * {@code useCurrentWeapon}, {@code updateWeapons} and {@code updateWeaponsRotation}). + */ +public final class VehicleWeapons { + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final boolean clientSide; + private final List sets = new ArrayList<>(); + private final int[] selected; + private final @Nullable Guidance[] guidance; + private final RandomSource random = RandomSource.create(); + + /** One bit per mount, set while that mount shows muzzle effects. Synced by the vehicle. */ + private int useStat; + private int prevUseStat; + + private final float[] bayAngle; + private final float[] prevBayAngle; + + private final int[] sentAmmo; + private final int[] sentRest; + + public final FlareDispenser flares; + + // Recoil that kicks the whole hull, from Recoil in the weapon file. + private int recoilCount; + private float recoilValue; + private float recoilYaw; + + public VehicleWeapons(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + this.clientSide = vehicle.level().isClientSide(); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + if (weapon != null && !group.mounts().isEmpty()) { + WeaponSet set = new WeaponSet(weapon, group.mounts(), clientSide); + set.ownSound = def.weaponSounds.get(group.weaponType()); + sets.add(set); + } + } + selected = new int[def.seatCount()]; + Arrays.fill(selected, -1); + guidance = new Guidance[sets.size()]; + for (int i = 0; i < sets.size(); i++) { + WeaponDefinition w = sets.get(i).definition(); + if (w.type == WeaponType.AA_MISSILE || w.type == WeaponType.AT_MISSILE) { + boolean aa = w.type == WeaponType.AA_MISSILE; + guidance[i] = new Guidance(aa, !aa, w.ridableOnly, w.lockTime, this::canLock); + } + } + bayAngle = new float[def.weaponBays.size()]; + prevBayAngle = new float[def.weaponBays.size()]; + sentAmmo = new int[sets.size()]; + sentRest = new int[sets.size()]; + Arrays.fill(sentAmmo, -1); + flares = new FlareDispenser(vehicle, def); + } + + public List sets() { + return sets; + } + + public @Nullable WeaponSet set(int id) { + return id >= 0 && id < sets.size() ? sets.get(id) : null; + } + + public int indexOf(String weapon) { + for (int i = 0; i < sets.size(); i++) { + if (sets.get(i).name().equalsIgnoreCase(weapon)) { + return i; + } + } + return -1; + } + + public @Nullable WeaponSet byName(String weapon) { + return set(indexOf(weapon)); + } + + public @Nullable Guidance guidance(int id) { + return id >= 0 && id < guidance.length ? guidance[id] : null; + } + + /** A vehicle does not lock its own crew or cargo. */ + private boolean canLock(Entity entity) { + return entity != vehicle && !vehicle.hasPassenger(entity); + } + + // Selection + + public int selected(int seat) { + return seat >= 0 && seat < selected.length ? selected[seat] : -1; + } + + public @Nullable WeaponSet current(int seat) { + return set(selected(seat)); + } + + /** The seat that operates a weapon: its own seat, or the pilot while that seat has no player. */ + public int operatorSeat(int id) { + WeaponSet set = set(id); + if (set == null) { + return -1; + } + WeaponMount mount = set.firstMount(); + if (mount.usableByPilot() && !(vehicle.occupant(mount.seatIndex()) instanceof Player)) { + return 0; + } + return mount.seatIndex(); + } + + /** {@code getNextWeaponID}: the next weapon {@code seat} may use, stepping forwards or back. */ + public int next(int seat, int step) { + if (sets.isEmpty() || seat < 0) { + return -1; + } + int id = selected(seat); + for (int i = 0; i < sets.size(); i++) { + id = step >= 0 ? (id + 1) % sets.size() : id > 0 ? id - 1 : sets.size() - 1; + WeaponMount mount = sets.get(id).firstMount(); + int owner = mount.seatIndex(); + if (owner == seat || seat == 0 && mount.usableByPilot() && !(vehicle.occupant(owner) instanceof Player)) { + return id; + } + } + return -1; + } + + /** {@code initCurrentWeapon}: a player took a seat. Server only. */ + public void onSeated(Entity entity, int seat) { + if (seat < 0 || seat >= selected.length) { + return; + } + selected[seat] = -1; + if (entity instanceof Player) { + switchTo(entity, seat, next(seat, 1)); + } else { + vehicle.publishWeaponSelection(); + } + } + + public void onLeft(int seat) { + if (seat >= 0 && seat < selected.length) { + selected[seat] = -1; + vehicle.publishWeaponSelection(); + } + } + + /** {@code switchWeapon}. Server only; the selection reaches clients through synced data. */ + public void switchTo(Entity user, int seat, int id) { + if (seat < 0 || seat >= selected.length || sets.isEmpty()) { + return; + } + WeaponSet before = current(seat); + if (before != null) { + before.reload(); + } + selected[seat] = Math.min(id, sets.size() - 1); + WeaponSet now = current(seat); + if (now != null) { + now.onSwitchWeapon(vehicle.hasInfiniteAmmo(user)); + } + vehicle.publishWeaponSelection(); + } + + public String encodeSelection() { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < selected.length; i++) { + if (i > 0) { + b.append(','); + } + b.append(selected[i]); + } + return b.toString(); + } + + /** Applies the server's selection on a client, running the switch delay where it changed. */ + public void decodeSelection(String encoded, @Nullable Entity localPlayer) { + String[] parts = encoded.isEmpty() ? new String[0] : encoded.split(","); + for (int i = 0; i < selected.length; i++) { + int id; + try { + id = i < parts.length ? Integer.parseInt(parts[i]) : -1; + } catch (NumberFormatException e) { + id = -1; + } + if (selected[i] != id) { + selected[i] = id; + WeaponSet set = set(id); + Entity occupant = vehicle.occupant(i); + if (set != null && occupant != null && occupant == localPlayer) { + set.onSwitchWeapon(vehicle.hasInfiniteAmmo(occupant)); + Guidance g = guidance(id); + if (g != null) { + g.clear(); + } + } + } + } + } + + // Firing + + /** + * {@code useCurrentWeapon}: fires the selected weapon of {@code seat} if it is ready. The + * trigger decides what the shot does on this side. + */ + public boolean use(Entity user, int seat, Shot shot, WeaponSet.Trigger trigger) { + int id = selected(seat); + WeaponSet set = set(id); + if (set == null || !set.canUse() || vehicle.isDestroyed()) { + return false; + } + shot.infiniteAmmo = vehicle.hasInfiniteAmmo(user); + int mount = set.currentMount(); + // As the original did, face the vehicle where the user looks for the length of the shot. + boolean followUser = vehicle.aimsWithUser(set); + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + if (followUser) { + vehicle.setYRot(user.getYRot()); + vehicle.setXRot(user.getXRot()); + } + boolean fired; + try { + fired = set.use(shot, (s, m, sh) -> { + Vec3 muzzle = muzzleOffset(s, m, 1.0F); + sh.x += muzzle.x; + sh.y += muzzle.y; + sh.z += muzzle.z; + return trigger.fire(s, m, sh); + }); + } finally { + if (followUser) { + vehicle.setYRot(hullYaw); + vehicle.setXRot(hullPitch); + } + } + if (!fired) { + return false; + } + String group = set.definition().group; + if (!group.isEmpty()) { + for (WeaponSet other : sets) { + if (other != set && group.equals(other.definition().group)) { + other.waitForGroup(shot.reload); + } + } + } + if (!clientSide) { + int bit = bitOf(id, mount); + if (bit < 32) { + useStat |= 1 << bit; + } + } + return true; + } + + /** World offset of a mount from the vehicle origin; turret mounts turn with the turret. */ + public Vec3 muzzleOffset(WeaponSet set, int mount, float partialTicks) { + WeaponMount m = set.mounts().get(mount); + Vector3 local = m.turret() ? vehicle.onTurret(def, m.position(), partialTicks) : m.position(); + Vector3f v = vehicle.toWorldOffset(local); + return new Vec3(v.x, v.y, v.z); + } + + private int bitOf(int id, int mount) { + int shift = 0; + for (int i = 0; i < id; i++) { + shift += sets.get(i).mounts().size(); + } + return shift + mount; + } + + /** Whether a mount is cooling down after a shot; missile parts are hidden meanwhile. */ + public boolean isMountUsed(int id, int mount) { + int bit = bitOf(id, mount); + return bit < 32 && (useStat & 1 << bit) != 0; + } + + public int useStat() { + return useStat; + } + + public void setUseStat(int stat) { + useStat = stat; + } + + public void switchMode(int seat) { + WeaponSet set = current(seat); + if (set != null) { + set.switchMode(); + } + } + + // Ticking + + /** {@code updateWeapons} and {@code updateWeaponsRotation}, once per tick on both sides. */ + public void tick() { + if (sets.isEmpty()) { + flares.tick(); + return; + } + int previous = prevUseStat; + if (!clientSide) { + useStat |= usedStat(); + vehicle.publishWeaponUse(useStat); + } else { + useStat = vehicle.syncedWeaponUse(); + } + prevUseStat = useStat; + + float yaw = Mth.wrapDegrees(vehicle.getYRot()); + float pitch = Mth.wrapDegrees(vehicle.getXRot()); + int bit = 0; + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + boolean longDelay = set.isLongDelay(); + boolean anyUsed = false; + for (int mount = 0; mount < set.mounts().size(); mount++) { + boolean wasUsed = bit < 32 && (previous & 1 << bit) != 0; + boolean used = bit < 32 && (useStat & 1 << bit) != 0; + if (longDelay && wasUsed && used) { + used = false; + } + anyUsed |= used; + if (!wasUsed && used && set.definition().recoil > 0.0F) { + recoilCount = 30; + recoilValue = set.definition().recoil; + recoilYaw = set.rotationYaw; + } + if (clientSide && used) { + muzzleEffects(set, mount, yaw); + } + set.tickRecoil(mount, used); + bit++; + } + set.tick(anyUsed); + if (!clientSide) { + ServerFiring.tick(vehicle, set); + } + updateRotation(set); + } + if (!clientSide) { + useStat = 0; + syncAmmo(); + } else { + for (Guidance g : guidance) { + if (g != null) { + g.tick(); + } + } + } + if (recoilCount > 0) { + recoilCount--; + } + tickBays(); + if (clientSide) { + smoke(); + } + flares.tick(); + } + + private int usedStat() { + int stat = 0; + int bit = 0; + for (WeaponSet set : sets) { + for (int mount = 0; mount < set.mounts().size() && bit < 32; mount++) { + stat |= set.isUsed(mount) ? 1 << bit : 0; + bit++; + } + } + return stat; + } + + /** Turret aim of one set, following whoever operates it, within the mount's traverse limits. */ + private void updateRotation(WeaponSet set) { + if (vehicle.isDestroyed()) { + return; + } + WeaponMount mount = set.firstMount(); + int seat = mount.seatIndex(); + Entity operator = vehicle.occupant(seat); + if (mount.usableByPilot() && !(operator instanceof Player)) { + operator = vehicle.occupant(0); + } + float hullYaw = Mth.wrapDegrees(vehicle.getYRot()); + float hullPitch = Mth.wrapDegrees(vehicle.getXRot()); + float turretYaw = vehicle.turretYaw(); + if (operator instanceof Player) { + if ((int) mount.minYaw() != 0 || (int) mount.maxYaw() != 0) { + float ty = mount.turret() ? turretYaw : 0.0F; + float ey = Mth.wrapDegrees(operator.getYRot() - hullYaw - mount.defaultYaw() - ty); + if (Math.abs((int) mount.minYaw()) < 360 && Math.abs((int) mount.maxYaw()) < 360) { + float targetYaw = Mth.clamp(ey, mount.minYaw(), mount.maxYaw()); + float wy = set.rotationYaw - mount.defaultYaw() - ty; + if (targetYaw < wy) { + wy = wy - targetYaw > 15.0F ? wy - 15.0F : targetYaw; + } else if (targetYaw > wy) { + wy = targetYaw - wy > 15.0F ? wy + 15.0F : targetYaw; + } + set.rotationYaw = wy + mount.defaultYaw() + ty; + } else { + set.rotationYaw = ey + ty; + } + } + float ep = Mth.wrapDegrees(operator.getXRot() - hullPitch); + set.rotationPitch = Mth.clamp(ep, mount.minPitch(), mount.maxPitch()); + set.rotationTurretYaw = 0.0F; + } else { + set.rotationTurretYaw = turretYaw; + } + } + + /** {@code updateRecoil}: the hull kick of a big gun, applied on the simulating side. */ + public float @Nullable [] recoilKick() { + if (recoilCount < 12) { + return null; + } + float rel = (float) Math.toRadians(recoilYaw - vehicle.roll()); + float kick = Mth.cos((float) Math.toRadians(recoilCount * 6)) * recoilValue; + return new float[]{kick * Mth.cos(rel), kick * Mth.sin(rel)}; + } + + /** {@code updateWeaponBay}: bays swing open while a seated crew member has their weapon selected. */ + private void tickBays() { + for (int i = 0; i < bayAngle.length; i++) { + WeaponBayPart bay = def.weaponBays.get(i); + boolean open = false; + for (String weapon : bay.weapons()) { + int id = indexOf(weapon); + for (int seat = 0; seat < selected.length && id >= 0; seat++) { + if (selected[seat] == id && vehicle.occupant(seat) != null) { + open = true; + } + } + } + prevBayAngle[i] = bayAngle[i]; + bayAngle[i] = Mth.clamp(bayAngle[i] + (open ? 3.0F : -3.0F), 0.0F, 90.0F); + } + } + + public float bayAngle(int bay, float partialTicks) { + return bay < bayAngle.length ? Mth.lerp(partialTicks, prevBayAngle[bay], bayAngle[bay]) : 0.0F; + } + + // Effects, client only + + /** {@code spawnParticleMuzzleFlash} plus the spent case of guns with {@code SetCartridge}. */ + private void muzzleEffects(WeaponSet set, int mount, float hullYaw) { + Level level = vehicle.level(); + WeaponDefinition w = set.definition(); + Vec3 offset = muzzleOffset(set, mount, 1.0F); + double px = vehicle.getX() + offset.x; + double py = vehicle.getY() + offset.y; + double pz = vehicle.getZ() + offset.z; + Vec3 dir = Vec3.directionFromRotation(vehicle.getXRot() + set.rotationPitch, hullYaw + set.rotationYaw); + for (MuzzleFlash mf : w.muzzleSmokes) { + double x = px + dir.x * mf.distance(); + double y = py + dir.y * mf.distance(); + double z = pz + dir.z * mf.distance(); + for (int i = 0; i < mf.count(); i++) { + double r = mf.range(); + level.addParticle(mf.size() >= 2.0F ? ParticleTypes.LARGE_SMOKE : ParticleTypes.SMOKE, + x + (random.nextDouble() - 0.5) * r, y + (random.nextDouble() - 0.5) * r, z + (random.nextDouble() - 0.5) * r, + (random.nextDouble() - 0.5) * 0.05, 0.02, (random.nextDouble() - 0.5) * 0.05); + } + } + for (MuzzleFlash mf : w.muzzleFlashes) { + int rgb = mf.color().argb() & 0xFFFFFF; + level.addParticle(new DustParticleOptions(rgb, Mth.clamp(mf.size() * 4.0F, 0.5F, 4.0F)), + px + dir.x * mf.distance(), py + dir.y * mf.distance(), pz + dir.z * mf.distance(), 0.0, 0.0, 0.0); + } + if (w.cartridge != null) { + Vec3 motion = vehicle.getDeltaMovement(); + Vec3 side = Vec3.directionFromRotation(0.0F, hullYaw + set.rotationYaw + w.cartridge.yaw() + 90.0F) + .scale(0.1 + w.cartridge.acceleration()); + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, Items.GOLD_NUGGET), + px, py, pz, motion.x + side.x, motion.y + 0.1, motion.z + side.z); + } + } + + /** {@code onUpdate_ParticleSmoke}: smoke generators trail while the pilot has them selected. */ + private void smoke() { + WeaponSet set = current(0); + if (set == null || set.definition().type != WeaponType.SMOKE || vehicle.visibleThrottle() <= 0.1F) { + return; + } + WeaponDefinition w = set.definition(); + Vec3 back = Vec3.directionFromRotation(vehicle.getXRot(), vehicle.getYRot()).reverse(); + int rgb = w.color.argb() & 0xFFFFFF; + for (int mount = 0; mount < set.mounts().size(); mount++) { + if (random.nextFloat() > vehicle.visibleThrottle() * 1.5F) { + continue; + } + Vec3 offset = muzzleOffset(set, mount, 1.0F); + for (int i = 0; i < w.smokeNum; i++) { + vehicle.level().addParticle(new DustParticleOptions(rgb, Mth.clamp(w.smokeSize * 2.0F, 0.5F, 4.0F)), + vehicle.getX() + offset.x + back.x, vehicle.getY() + offset.y + back.y, vehicle.getZ() + offset.z + back.z, + back.x * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.y * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.z * w.acceleration + (random.nextDouble() - 0.5) * 0.2); + } + } + } + + // Ammunition + + /** Sends magazine and reserve counts to the crew whenever they change. Server only. */ + private void syncAmmo() { + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + if (set.ammo() != sentAmmo[id] || set.restAmmo() != sentRest[id]) { + sentAmmo[id] = set.ammo(); + sentRest[id] = set.restAmmo(); + WeaponStatePacket packet = new WeaponStatePacket(vehicle.getId(), id, set.ammo(), set.restAmmo()); + for (Entity passenger : vehicle.getPassengers()) { + if (passenger instanceof ServerPlayer player) { + ModNetwork.sendToPlayer(player, packet); + } + } + } + } + } + + /** Forces a full resend, for a player who just boarded. */ + public void resendAmmo() { + Arrays.fill(sentAmmo, -1); + } + + public void applyAmmo(int id, int ammo, int rest) { + WeaponSet set = set(id); + if (set != null) { + set.setAmmo(ammo); + set.setRestAmmo(rest); + } + } + + /** {@code reloadAllWeapon}: tops up every magazine from its reserve. */ + public void reloadAll() { + for (WeaponSet set : sets) { + set.reloadMagazine(); + } + } + + public int[] saveAmmo() { + int[] out = new int[sets.size() * 2]; + for (int i = 0; i < sets.size(); i++) { + out[i * 2] = sets.get(i).ammo(); + out[i * 2 + 1] = sets.get(i).restAmmo(); + } + return out; + } + + public void loadAmmo(int[] saved) { + for (int i = 0; i < sets.size() && i * 2 + 1 < saved.length; i++) { + sets.get(i).setAmmo(Math.min(saved[i * 2], sets.get(i).magazineSize())); + sets.get(i).setRestAmmo(saved[i * 2 + 1]); + } + } +} From 23bbf13572f2d6bf9809ec50a5658f247a48ec2e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:22 +0800 Subject: [PATCH 0576/1461] Add weapon/WeaponSet.java to the 26.1 shared sources --- .../jnhprojects/mcheli/weapon/WeaponSet.java | 505 ++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java new file mode 100644 index 00000000..3f539030 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java @@ -0,0 +1,505 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * One selectable weapon of a vehicle: every mount that fires the same weapon, with the magazine, + * reload, heat and recoil they share. A port of {@code MCH_WeaponSet} together with the parts of + * {@code MCH_WeaponBase} and {@code MCH_WeaponCreator} that are not specific to a weapon type. + * + *

Both sides keep one. The server's is authoritative for ammunition and decides what is fired; + * the client's runs with the slightly longer intervals the original used there, so the client never + * asks to fire faster than the server accepts. + */ +public final class WeaponSet { + private static final int SWITCH_WAIT = 15; + + /** Fires one mount. Returns whether the shot happened, which consumes a round. */ + @FunctionalInterface + public interface Trigger { + boolean fire(WeaponSet set, int mount, Shot shot); + } + + private final RandomSource random = RandomSource.create(); + private final WeaponDefinition def; + private final List mounts; + private final boolean clientSide; + + public final int interval; + public final int heatCount; + private final int numMode; + + private int currentMount; + private int mode; + private int ammo; + private int restAmmo; + public int currentHeat; + private int cooldownSpeed = 1; + public int countWait; + public int countReloadWait; + private final int[] lastUsedCount; + /** Ticks since each mount last fired, {@code MCH_WeaponBase.tick}. */ + final int[] ticks; + public int soundWait; + /** Whether the shot being fired may play its sound, see {@code SoundDelay}. */ + public boolean soundAllowed = true; + /** The vehicle's own sound for this weapon, from {@code WeaponSound}. */ + public @Nullable WeaponSound ownSound; + + /** Aim relative to the hull, following the crew member who operates the weapon. */ + public float rotationYaw; + public float rotationPitch; + public float prevRotationYaw; + public float prevRotationPitch; + public final float defaultRotationYaw; + /** How far behind the pilot's view a turret weapon lags, for drawing. */ + public float rotationTurretYaw; + + public float barrelSpeed; + public float barrel; + public float prevBarrel; + private final Recoil[] recoil; + + // Close air support run, server only. See ServerFiring. + @Nullable Entity casUser; + @Nullable Entity casShooter; + double casX; + double casY; + double casZ; + int casDirection; + int casStartTick; + int casCount = 3; + + public WeaponSet(WeaponDefinition def, List mounts, boolean clientSide) { + this.def = def; + this.mounts = List.copyOf(mounts); + this.clientSide = clientSide; + this.lastUsedCount = new int[mounts.size()]; + this.ticks = new int[mounts.size()]; + this.recoil = new Recoil[mounts.size()]; + for (int i = 0; i < recoil.length; i++) { + recoil[i] = new Recoil(def.recoilBufCount, def.recoilBufCountSpeed); + } + this.defaultRotationYaw = mounts.isEmpty() ? 0.0F : mounts.getFirst().defaultYaw(); + this.rotationYaw = defaultRotationYaw; + this.prevRotationYaw = defaultRotationYaw; + + int wait = def.delay; + if (def.type == WeaponType.CAS && wait > -250) { + wait = -250; + } + int heat = def.heatCount; + if (def.maxHeatCount > 0 && heat < 2) { + heat = 2; + } + if (clientSide) { + wait = clientInterval(wait); + heat++; + } + this.interval = wait; + this.heatCount = heat; + this.numMode = def.type == WeaponType.MACHINE_GUN_2 && def.explosion == 0 ? 0 : def.modeNum; + } + + /** The original gave the client longer intervals so its requests never outran the server. */ + private static int clientInterval(int wait) { + if (wait < 4) { + return wait + 1; + } else if (wait < 7) { + return wait + 2; + } else if (wait < 10) { + return wait + 3; + } else if (wait < 20) { + return wait + 6; + } + wait += 10; + return wait >= 40 ? -wait : wait; + } + + public WeaponDefinition definition() { + return def; + } + + public String name() { + return def.name(); + } + + public List mounts() { + return mounts; + } + + public WeaponMount firstMount() { + return mounts.getFirst(); + } + + public int currentMount() { + return currentMount; + } + + // Ammunition + + public int ammo() { + return ammo; + } + + public int magazineSize() { + return def.round; + } + + public int restAmmo() { + return restAmmo; + } + + public int maxAmmo() { + return def.maxAmmo; + } + + public void setAmmo(int n) { + ammo = n; + } + + /** Everything not in the magazine, capped so magazine plus reserve never exceed {@code MaxAmmo}. */ + public void setRestAmmo(int n) { + restAmmo = Math.min(n, def.maxAmmo - ammo); + } + + /** {@code supplyRestAllAmmo}: one resupply adds {@code SuppliedNum} rounds. */ + public boolean supply() { + if (restAmmo + ammo < def.maxAmmo) { + setRestAmmo(restAmmo + ammo + def.suppliedNum); + return true; + } + return false; + } + + public boolean isInPreparation() { + return countWait < 0 || countReloadWait > 0; + } + + public boolean isReloading() { + return countReloadWait > 0; + } + + public boolean canUse() { + return countWait == 0; + } + + public boolean isLongDelay() { + return def.delay > 4; + } + + public boolean isOverheated() { + return def.maxHeatCount > 0 && currentHeat >= def.maxHeatCount; + } + + public void reload() { + if (magazineSize() > 0 && ammo < magazineSize() && def.reloadTime > 0) { + countReloadWait = def.reloadTime; + if (clientSide) { + ammo = 0; + } else { + countReloadWait = Math.max(countReloadWait - 20, 1); + } + } + } + + public void reloadMagazine() { + int needed = magazineSize() - ammo; + if (needed > 0) { + needed = Math.min(needed, restAmmo); + ammo += needed; + setRestAmmo(restAmmo - needed); + } + } + + // Modes + + public int mode() { + return def.fixMode > 0 ? def.fixMode : mode; + } + + public boolean switchMode() { + if (def.fixMode > 0) { + return false; + } + int before = mode(); + mode = numMode > 0 ? (mode() + 1) % numMode : 0; + if (before == mode()) { + return false; + } + waitAfterSwitch(SWITCH_WAIT); + return true; + } + + /** {@code getName}: the display name with the mode suffix the weapon class added. */ + public String displayName() { + String suffix = switch (def.type) { + case MACHINE_GUN_2 -> mode() == 0 ? "" : " [HE]"; + case ROCKET -> mode() == 0 ? "" : " [HEIAP]"; + case AT_MISSILE -> mode() == 1 ? " [TA]" : ""; + case TV_MISSILE -> switch (mode()) { + case 0 -> " [TV]"; + case 2 -> " [TA]"; + default -> ""; + }; + case null, default -> ""; + }; + return def.displayName + suffix; + } + + public void onSwitchWeapon(boolean infiniteAmmo) { + waitAfterSwitch(clientSide ? SWITCH_WAIT + 10 : SWITCH_WAIT); + currentMount = 0; + if (infiniteAmmo) { + ammo = magazineSize(); + } + } + + private void waitAfterSwitch(int wait) { + if (countWait >= -wait) { + countWait = countWait > wait ? -countWait : -wait; + } + } + + /** Another weapon of the same {@code Group} fired; this one waits and reloads with it. */ + public void waitForGroup(boolean reload) { + countWait = interval; + countReloadWait = 0; + if (reload && magazineSize() > 0 && def.reloadTime > 0) { + countReloadWait = clientSide ? def.reloadTime : Math.max(def.reloadTime - 20, 1); + } + } + + /** + * {@code getLandInDistance}: how far out, measured flat, a shell fired now would come down, + * stepping its arc a tick at a time as the projectile does; -1 if it would not come down in + * range. Mortars show it on the HUD. + */ + public double landingDistance(Entity shooter) { + if (def.gravity >= 0.0F || mounts.isEmpty()) { + return -1.0; + } + WeaponMount mount = mounts.get(currentMount); + float yaw = Mth.wrapDegrees(shooter.getYRot() + rotationYaw + mount.yaw()); + float pitch = Mth.wrapDegrees(shooter.getXRot() + rotationPitch + mount.pitch()); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + double acc = Math.min(def.acceleration, 4.0F); + double my = dir.y * def.acceleration; + if (my <= 0.0 || acc <= 0.0) { + return -1.0; + } + double mx = dir.x * def.acceleration; + double mz = dir.z * def.acceleration; + double gravity = def.gravity * (def.acceleration / acc); + double steps = my / def.gravity; + if (steps < -12.0) { + double f = steps / -12.0; + mx *= f; + my *= f; + mz *= f; + gravity *= f * f * 0.95; + } + Level level = shooter.level(); + double x = shooter.getX(); + double y = shooter.getY() + 3.0; + double z = shooter.getZ(); + for (int i = 0; i < 50; i++) { + Vec3 from = new Vec3(x, y, z); + Vec3 to = new Vec3(x + mx, y + my, z + mz); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shooter)); + if (hit.getType() == HitResult.Type.BLOCK) { + return Math.hypot(hit.getBlockPos().getX() - shooter.getX(), hit.getBlockPos().getZ() - shooter.getZ()); + } + my += gravity; + x += mx; + y += my; + z += mz; + if (y < shooter.getY()) { + return Math.hypot(x - shooter.getX(), z - shooter.getZ()); + } + } + return -1.0; + } + + // Firing + + /** + * Fires the next mount if the set is ready. The aim is the shooter's rotation plus this set's + * turret aim and the mount's fixed offset, scattered by {@code Accuracy}. + */ + public boolean use(Shot shot, Trigger trigger) { + if (magazineSize() > 0 && ammo <= 0 || isOverheated()) { + return false; + } + WeaponMount mount = mounts.get(currentMount); + soundAllowed = soundWait == 0; + float yaw = shot.shooter.getYRot() + rotationYaw + mount.yaw(); + float pitch = shot.shooter.getXRot() + rotationPitch + mount.pitch(); + if (def.accuracy > 0.0F) { + yaw += (random.nextFloat() - 0.5F) * def.accuracy; + pitch += (random.nextFloat() - 0.5F) * def.accuracy; + } + shot.yaw = Mth.wrapDegrees(yaw); + shot.pitch = Mth.wrapDegrees(pitch); + if (!trigger.fire(this, currentMount, shot)) { + return false; + } + ticks[currentMount] = 0; + + if (def.maxHeatCount > 0) { + cooldownSpeed = 1; + currentHeat += heatCount; + if (currentHeat >= def.maxHeatCount) { + currentHeat += 30; + } + } + if (def.soundDelay > 0 && soundWait == 0) { + soundWait = def.soundDelay; + } + lastUsedCount[currentMount] = Math.abs(interval); + if (isCooldownReloadTime() && def.reloadTime - 10 > lastUsedCount[currentMount]) { + lastUsedCount[currentMount] = def.reloadTime - 10; + } + currentMount = (currentMount + 1) % mounts.size(); + countWait = interval; + countReloadWait = 0; + if (ammo > 0) { + ammo--; + } + if (ammo <= 0) { + if (shot.infiniteAmmo && restAmmo < magazineSize()) { + setRestAmmo(magazineSize()); + } + reload(); + shot.reload = true; + } + return true; + } + + /** Missiles keep their mount empty for the whole reload, which hides the missile part meanwhile. */ + private boolean isCooldownReloadTime() { + return def.type == WeaponType.AA_MISSILE || def.type == WeaponType.AS_MISSILE || def.type == WeaponType.AT_MISSILE; + } + + /** Whether {@code mount} fired recently enough to show muzzle effects or keep a missile hidden. */ + public boolean isUsed(int mount) { + if (mount >= lastUsedCount.length) { + return false; + } + int count = lastUsedCount[mount]; + return interval >= 4 && count > interval / 2 || count >= 4; + } + + public int ticksSinceShot(int mount) { + return ticks[mount]; + } + + // Ticking + + /** {@code MCH_WeaponSet.update}, once per tick on both sides. */ + public void tick(boolean used) { + if (countReloadWait > 0) { + countReloadWait--; + if (countReloadWait == 0) { + reloadMagazine(); + } + } + for (int i = 0; i < lastUsedCount.length; i++) { + if (lastUsedCount[i] > 0) { + if (lastUsedCount[i] != 4 || currentMount == 0 && canUse() && (ammo > 0 || def.maxAmmo <= 0)) { + lastUsedCount[i]--; + } + } + } + if (currentHeat > 0) { + if (currentHeat < def.maxHeatCount) { + cooldownSpeed++; + } + currentHeat = Math.max(currentHeat - (cooldownSpeed / 20 + 1), 0); + } + if (countWait > 0) { + countWait--; + } else if (countWait < 0) { + countWait++; + } + prevRotationYaw = rotationYaw; + prevRotationPitch = rotationPitch; + if (countWait != 0) { + for (int i = 0; i < ticks.length; i++) { + ticks[i]++; + } + } + if (soundWait > 0) { + soundWait--; + } + + if (used && barrelSpeed < 75.0F) { + barrelSpeed = Math.min(barrelSpeed + 25 + random.nextInt(3), 74.0F); + } + prevBarrel = barrel; + barrel += barrelSpeed; + if (barrel >= 360.0F) { + barrel -= 360.0F; + prevBarrel -= 360.0F; + } + if (barrelSpeed > 0.0F) { + barrelSpeed = Math.max(barrelSpeed - 1.48F, 0.0F); + } + } + + /** {@code updateWeapon}: the recoil kick of one mount. */ + public void tickRecoil(int mount, boolean used) { + Recoil r = recoil[mount]; + r.prev = r.value; + if (used && r.count <= 0) { + r.count = r.max; + } + if (r.count > 0) { + if (r.max <= 1) { + r.value = 1.0F; + } else if (r.max == 2) { + r.value = r.count == 2 ? 1.0F : 0.6F; + } else { + if (r.count > r.max / 2) { + r.count -= r.speed; + } + r.value = Mth.sin((float) r.count / r.max * Mth.PI); + } + r.count--; + } else { + r.value = 0.0F; + } + } + + public float recoil(int mount, float partialTicks) { + Recoil r = recoil[Math.min(mount, recoil.length - 1)]; + return Mth.lerp(partialTicks, r.prev, r.value); + } + + private static final class Recoil { + final int max; + final int speed; + int count; + float value; + float prev; + + Recoil(int max, int speed) { + this.max = max; + this.speed = speed; + } + } +} From cdb9c2eae1fcf017c6edb51f09b60c2a76738dd2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:23 +0800 Subject: [PATCH 0577/1461] Add workshop/TestFlight.java to the 26.1 shared sources --- .../mcheli/workshop/TestFlight.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 minecraft-26.1/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java new file mode 100644 index 00000000..f5786b92 --- /dev/null +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java @@ -0,0 +1,62 @@ +package com.jnhprojects.mcheli.workshop; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.phys.Vec3; + +/** + * Flying a vehicle straight from the workshop. The pack was just saved on the machine this server + * runs on, so loading it again is enough. That only holds for the owner of a single player or LAN + * game: a dedicated server does not get a pack until an operator approves it, and then it is no + * longer a test, so the screen does not offer it there and the server refuses it too. + */ +public final class TestFlight { + /** How far up to look for room when the vehicle does not fit where the player stands. */ + private static final int MAX_RISE = 48; + + private TestFlight() { + } + + public static void start(ServerPlayer player, String vehicle) { + MinecraftServer server = player.level().getServer(); + if (!server.isSingleplayerOwner(player.nameAndId())) { + player.sendOverlayMessage(Component.translatable("message.mcheli.workshop.test_flight_owner_only")); + return; + } + if (player.isPassenger()) { + player.sendOverlayMessage(Component.translatable("message.mcheli.workshop.test_flight_riding")); + return; + } + ContentReload.reload(server); + AircraftDefinition def = MCHeli.content().any(vehicle); + if (def == null) { + player.sendSystemMessage(Component.translatable("message.mcheli.workshop.test_flight_no_vehicle", vehicle)); + MCHeli.packs().reportProblems(player::sendSystemMessage); + return; + } + ServerLevel level = player.level(); + AircraftEntity aircraft = ModEntities.typeFor(def).create(level, EntitySpawnReason.COMMAND); + if (aircraft == null) { + return; + } + aircraft.setType(def); + Vec3 start = player.position(); + for (int rise = 0; rise <= MAX_RISE; rise++) { + aircraft.snapTo(start.x, start.y + rise, start.z, player.getYRot(), 0.0F); + if (level.noCollision(aircraft, aircraft.getBoundingBox())) { + break; + } + } + aircraft.startTestFlight(start); + level.addFreshEntity(aircraft); + player.startRiding(aircraft, true, true); + } +} From 096e05c41bed069387e7cd336884df68d7fb5d2a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:23 +0800 Subject: [PATCH 0578/1461] Add assets/mcheli/blockstates/drafting_table.json to the 26.1 shared sources --- .../resources/assets/mcheli/blockstates/drafting_table.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/blockstates/drafting_table.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/blockstates/drafting_table.json b/minecraft-26.1/src/main/resources/assets/mcheli/blockstates/drafting_table.json new file mode 100644 index 00000000..82992af9 --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/blockstates/drafting_table.json @@ -0,0 +1,5 @@ +{ + "multipart": [ + { "apply": { "model": "mcheli:block/drafting_table" } } + ] +} From 4802bbd6e37a8aecd1546252b52e3cebb04cd739 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:23 +0800 Subject: [PATCH 0579/1461] Add assets/mcheli/items/drafting_table.json to the 26.1 shared sources --- .../main/resources/assets/mcheli/items/drafting_table.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/items/drafting_table.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/items/drafting_table.json b/minecraft-26.1/src/main/resources/assets/mcheli/items/drafting_table.json new file mode 100644 index 00000000..a3afc683 --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/items/drafting_table.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "mcheli:block/drafting_table" + } +} From afa285f50bb79f031d47a514f9d61e465062bbf6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:24 +0800 Subject: [PATCH 0580/1461] Add assets/mcheli/items/helicopter.json to the 26.1 shared sources --- .../src/main/resources/assets/mcheli/items/helicopter.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/items/helicopter.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/items/helicopter.json b/minecraft-26.1/src/main/resources/assets/mcheli/items/helicopter.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/items/helicopter.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From 4232e5f284560d7b4524ba776ffaa9f17ec2dbee Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:24 +0800 Subject: [PATCH 0581/1461] Add assets/mcheli/items/plane.json to the 26.1 shared sources --- .../src/main/resources/assets/mcheli/items/plane.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/items/plane.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/items/plane.json b/minecraft-26.1/src/main/resources/assets/mcheli/items/plane.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/items/plane.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From 3e4e1b19a26c811c579681d716e7795326bc989f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:25 +0800 Subject: [PATCH 0582/1461] Add assets/mcheli/items/tank.json to the 26.1 shared sources --- .../src/main/resources/assets/mcheli/items/tank.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/items/tank.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/items/tank.json b/minecraft-26.1/src/main/resources/assets/mcheli/items/tank.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/items/tank.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From c8370f2214a5a4b8a2ece1391fac57f55a904f51 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:25 +0800 Subject: [PATCH 0583/1461] Add assets/mcheli/items/vehicle.json to the 26.1 shared sources --- .../src/main/resources/assets/mcheli/items/vehicle.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/items/vehicle.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/items/vehicle.json b/minecraft-26.1/src/main/resources/assets/mcheli/items/vehicle.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/items/vehicle.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From 375d07a7dd8f21d15ddc63d755af508933795998 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:26 +0800 Subject: [PATCH 0584/1461] Add assets/mcheli/models/block/drafting_table.json to the 26.1 shared sources --- .../mcheli/models/block/drafting_table.json | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/models/block/drafting_table.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/models/block/drafting_table.json b/minecraft-26.1/src/main/resources/assets/mcheli/models/block/drafting_table.json new file mode 100644 index 00000000..8dfa5bcc --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/models/block/drafting_table.json @@ -0,0 +1,27 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/oak_planks", + "legs": "minecraft:block/iron_block", + "board": "minecraft:block/oak_planks", + "paper": "minecraft:block/white_concrete" + }, + "elements": [ + { "from": [1, 0, 2], "to": [2.5, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 2], "to": [15, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [1, 0, 12.5], "to": [2.5, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 12.5], "to": [15, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [0, 10, 1], "to": [16, 11.5, 15], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { + "north": {"texture": "#board"}, "south": {"texture": "#board"}, "east": {"texture": "#board"}, "west": {"texture": "#board"}, + "up": {"texture": "#board"}, "down": {"texture": "#board"} } }, + { "from": [2, 11.5, 2.5], "to": [14, 11.6, 13.5], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { "up": {"texture": "#paper"} } } + ] +} From 97bdc8c5148d77e586c404fa354aa3eeb04f56e1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:26 +0800 Subject: [PATCH 0585/1461] Add assets/mcheli/particles/glow.json to the 26.1 shared sources --- .../src/main/resources/assets/mcheli/particles/glow.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/particles/glow.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/particles/glow.json b/minecraft-26.1/src/main/resources/assets/mcheli/particles/glow.json new file mode 100644 index 00000000..296e5397 --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/particles/glow.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "mcheli:glow" + ] +} From 0865a3e0d230bf65bfc5458edfc92e5ebcf13720 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:26 +0800 Subject: [PATCH 0586/1461] Add assets/mcheli/particles/smoke.json to the 26.1 shared sources --- .../resources/assets/mcheli/particles/smoke.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/particles/smoke.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/particles/smoke.json b/minecraft-26.1/src/main/resources/assets/mcheli/particles/smoke.json new file mode 100644 index 00000000..977381be --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/particles/smoke.json @@ -0,0 +1,16 @@ +{ + "textures": [ + "minecraft:big_smoke_0", + "minecraft:big_smoke_1", + "minecraft:big_smoke_2", + "minecraft:big_smoke_3", + "minecraft:big_smoke_4", + "minecraft:big_smoke_5", + "minecraft:big_smoke_6", + "minecraft:big_smoke_7", + "minecraft:big_smoke_8", + "minecraft:big_smoke_9", + "minecraft:big_smoke_10", + "minecraft:big_smoke_11" + ] +} From 970712d83d74f03b46fe248e8bdeef864790bd58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:27 +0800 Subject: [PATCH 0587/1461] Add assets/mcheli/post_effect/thermal.json to the 26.1 shared sources --- .../assets/mcheli/post_effect/thermal.json | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/post_effect/thermal.json diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/post_effect/thermal.json b/minecraft-26.1/src/main/resources/assets/mcheli/post_effect/thermal.json new file mode 100644 index 00000000..593e905d --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/post_effect/thermal.json @@ -0,0 +1,38 @@ +{ + "targets": { + "swap": {} + }, + "passes": [ + { + "vertex_shader": "minecraft:core/screenquad", + "fragment_shader": "mcheli:post/thermal", + "inputs": [ + { + "sampler_name": "In", + "target": "minecraft:main" + } + ], + "output": "swap" + }, + { + "vertex_shader": "minecraft:core/screenquad", + "fragment_shader": "minecraft:post/blit", + "inputs": [ + { + "sampler_name": "In", + "target": "swap" + } + ], + "uniforms": { + "BlitConfig": [ + { + "name": "ColorModulate", + "type": "vec4", + "value": [ 1.0, 1.0, 1.0, 1.0 ] + } + ] + }, + "output": "minecraft:main" + } + ] +} From 28f9a860075508d3ac728f931d718e7aff4ea30f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:27 +0800 Subject: [PATCH 0588/1461] Write the thermal shader in the GLSL dialect of 26.1 --- .../assets/mcheli/shaders/post/thermal.fsh | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/shaders/post/thermal.fsh diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/shaders/post/thermal.fsh b/minecraft-26.1/src/main/resources/assets/mcheli/shaders/post/thermal.fsh new file mode 100644 index 00000000..24f82a83 --- /dev/null +++ b/minecraft-26.1/src/main/resources/assets/mcheli/shaders/post/thermal.fsh @@ -0,0 +1,37 @@ +#version 330 + +// Thermal view, white hot. A post shader only sees colours, so heat is faked: bright and warm +// colours (fire, lava, skin, lamps) read hot, the sky and water cold, and outlines are drawn +// where the brightness changes sharply, like the original's "pencil" shader. + +uniform sampler2D InSampler; + +in vec2 texCoord; + +layout(std140) uniform SamplerInfo { + vec2 OutSize; + vec2 InSize; +}; + +out vec4 fragColor; + +const vec3 Gray = vec3(0.3, 0.59, 0.11); + +float heat(vec3 c) { + float luma = dot(c, Gray); + float warmth = clamp(c.r - c.b, 0.0, 1.0); + return clamp(luma * 0.7 + warmth * 0.6, 0.0, 1.0); +} + +void main() { + vec2 oneTexel = 1.0 / InSize; + float c = heat(texture(InSampler, texCoord).rgb); + float l = heat(texture(InSampler, texCoord - vec2(oneTexel.x, 0.0)).rgb); + float r = heat(texture(InSampler, texCoord + vec2(oneTexel.x, 0.0)).rgb); + float u = heat(texture(InSampler, texCoord - vec2(0.0, oneTexel.y)).rgb); + float d = heat(texture(InSampler, texCoord + vec2(0.0, oneTexel.y)).rgb); + float edge = clamp(abs(r - l) + abs(d - u), 0.0, 1.0); + + float value = smoothstep(0.08, 0.85, c) + edge * 0.6; + fragColor = vec4(vec3(clamp(value, 0.0, 1.0)), 1.0); +} From fd2eb98a4c737a3a7f27caf3e108a83bc7aa3c3e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:28 +0800 Subject: [PATCH 0589/1461] Add assets/mcheli/textures/particle/glow.png to the 26.1 shared sources --- .../assets/mcheli/textures/particle/glow.png | Bin 0 -> 705 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-26.1/src/main/resources/assets/mcheli/textures/particle/glow.png diff --git a/minecraft-26.1/src/main/resources/assets/mcheli/textures/particle/glow.png b/minecraft-26.1/src/main/resources/assets/mcheli/textures/particle/glow.png new file mode 100644 index 0000000000000000000000000000000000000000..339b97292b3a86cec21a5a2c2d4e9159da1241c6 GIT binary patch literal 705 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFm-skIEGZjoxNt=CF&@0?BmDQ z$G05=nLjo)u{~0d=sc02G=rmQ;>?7sgruZLOlR&iZR9TC);p3q<+=OyiMy`WR_B%6 ze!zcpW%0hxt6uJ`UOHv3s-)z$2S*IQ{M+2aBkl2V^1+p-UiNMId~?@~iHS`VvGKGtbJ(?@PMX`Q_JU)-AEGyIw3c zQrRtbha-<|yVkj!cRxOeDwmZ>DLa>nOo)xwXnye(C@s6DY4^dsUB4m0R;efKDR26HaPb4r4<;3I zGNE&B-ud`Y+f9cz$|Q5W??V51i?`b=Utv|8`!Zyb_`%f=Ny@jd)@AxOp)i%w?!a<|&&Tjr8oN`JQToU~(`->fg1 zZdP9Nqf5}cIn2IwE7PsJ)h7Sugyi*n%i6MQ@`K~}e`S@^nPlVK#j7xFV z+_j8uPnwhRR~zlgnR{cs;OihI&w3VnZD^oOJGP2v)|RI;bPnXz1l3;M6Sw&D^QW!V zDOSc=cfEdS|A;M20|xneiH#;17nkTB-(PMJ>hSJ9`!Q!Lt-~I+X1}kkT&Mak7@_E? z^^gDD5jm{g_J>^m^K5+4zM_`ByS7X6x8|EK^8>{;-q`=xrFCc5-2VzEHJ@mS{q(#h zwlHh)?U((R_Hk6-{(1h>N~aR1kZ_?NOSWy;`Ple&>FE<27@gj_D0q6`GXA*nq@huc lV^{v7@AFerZkX0Ht_;k6d;0MSU0~8=@O1TaS?83{1OQn@Sb6{e literal 0 HcmV?d00001 From 9be8927f94eac6bbf6f788783bcccb87523b6c12 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:28 +0800 Subject: [PATCH 0590/1461] Add data/mcheli/damage_type/bullet.json to the 26.1 shared sources --- .../src/main/resources/data/mcheli/damage_type/bullet.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/damage_type/bullet.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/damage_type/bullet.json b/minecraft-26.1/src/main/resources/data/mcheli/damage_type/bullet.json new file mode 100644 index 00000000..92ee6766 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/damage_type/bullet.json @@ -0,0 +1,5 @@ +{ + "exhaustion": 0.1, + "message_id": "mcheli.bullet", + "scaling": "when_caused_by_living_non_player" +} From 4d4765f68150356f434347584f60590b03ec78e4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:28 +0800 Subject: [PATCH 0591/1461] Add data/mcheli/loot_table/blocks/drafting_table.json to the 26.1 shared sources --- .../mcheli/loot_table/blocks/drafting_table.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json b/minecraft-26.1/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json new file mode 100644 index 00000000..610afa08 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { "type": "minecraft:item", "name": "mcheli:drafting_table" } + ], + "conditions": [ + { "condition": "minecraft:survives_explosion" } + ] + } + ] +} From 6cb680a81d245f8a432865ac03ce2afcaafa6ee7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:29 +0800 Subject: [PATCH 0592/1461] Add data/mcheli/recipe/chain.json to the 26.1 shared sources --- .../main/resources/data/mcheli/recipe/chain.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/chain.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/chain.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/chain.json new file mode 100644 index 00000000..484f5463 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/chain.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": "#c:ingots/iron" + }, + "pattern": [ + "I I", + "III", + "I I" + ], + "result": { + "id": "mcheli:chain" + } +} From 61db550efabb06d839716ceecd30952908b4e470 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:29 +0800 Subject: [PATCH 0593/1461] Add data/mcheli/recipe/container.json to the 26.1 shared sources --- .../resources/data/mcheli/recipe/container.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/container.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/container.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/container.json new file mode 100644 index 00000000..4c3bdcd9 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/container.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "#c:chests/wooden", + "I": "#c:ingots/iron" + }, + "pattern": [ + "CCI" + ], + "result": { + "id": "mcheli:container" + } +} From 8121242cf95da832341fdf032446a9cca3e54235 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:30 +0800 Subject: [PATCH 0594/1461] Add data/mcheli/recipe/drafting_table.json to the 26.1 shared sources --- .../data/mcheli/recipe/drafting_table.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/drafting_table.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/drafting_table.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/drafting_table.json new file mode 100644 index 00000000..f3d6324e --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/drafting_table.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "minecraft:crafting_table", + "F": "#minecraft:wooden_fences", + "P": "#minecraft:planks", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "R ", + "PCP", + "F F" + ], + "result": { + "id": "mcheli:drafting_table" + } +} From 50ff23a313a40879e73187dc8409d58e261ee351 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:30 +0800 Subject: [PATCH 0595/1461] Add data/mcheli/recipe/fgm148.json to the 26.1 shared sources --- .../resources/data/mcheli/recipe/fgm148.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/fgm148.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/fgm148.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fgm148.json new file mode 100644 index 00000000..c2c61fd6 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fgm148.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "III", + "GR " + ], + "result": { + "id": "mcheli:fgm148" + } +} From 8541a24d6eb82b349988cd9cc2696f2aa0af191d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:31 +0800 Subject: [PATCH 0596/1461] Add data/mcheli/recipe/fgm148_bullet.json to the 26.1 shared sources --- .../data/mcheli/recipe/fgm148_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/fgm148_bullet.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/fgm148_bullet.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fgm148_bullet.json new file mode 100644 index 00000000..c87b4800 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fgm148_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:gunpowders", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + " R ", + " I ", + " G " + ], + "result": { + "count": 2, + "id": "mcheli:fgm148_bullet" + } +} From f96915615e69bdebcf6a17683ab1d3bfed3d450d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:31 +0800 Subject: [PATCH 0597/1461] Add data/mcheli/recipe/fim92.json to the 26.1 shared sources --- .../resources/data/mcheli/recipe/fim92.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/fim92.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/fim92.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fim92.json new file mode 100644 index 00000000..33a068e1 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fim92.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "G ", + "III", + "RI " + ], + "result": { + "id": "mcheli:fim92" + } +} From 64dadc3c0a93899aaeac18301bb075acf2b72ddf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:31 +0800 Subject: [PATCH 0598/1461] Add data/mcheli/recipe/fim92_bullet.json to the 26.1 shared sources --- .../data/mcheli/recipe/fim92_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/fim92_bullet.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/fim92_bullet.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fim92_bullet.json new file mode 100644 index 00000000..c472fd6e --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fim92_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:gunpowders", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "R ", + " I ", + " G" + ], + "result": { + "count": 2, + "id": "mcheli:fim92_bullet" + } +} From 4ce40185444e16a9ab5f71d9169f110760abbb3d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:32 +0800 Subject: [PATCH 0599/1461] Add data/mcheli/recipe/fuel.json to the 26.1 shared sources --- .../main/resources/data/mcheli/recipe/fuel.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/fuel.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/fuel.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fuel.json new file mode 100644 index 00000000..d076b041 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/fuel.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": "minecraft:coal", + "I": "#c:ingots/iron" + }, + "pattern": [ + "ICI", + "III" + ], + "result": { + "id": "mcheli:fuel" + } +} From 3978f0cfda2f5516e394246737a82a18e1fc3321 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:32 +0800 Subject: [PATCH 0600/1461] Add data/mcheli/recipe/gltd.json to the 26.1 shared sources --- .../resources/data/mcheli/recipe/gltd.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/gltd.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/gltd.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/gltd.json new file mode 100644 index 00000000..104ac108 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/gltd.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": "#c:storage_blocks/iron", + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + " B ", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:gltd" + } +} From 899e24b629e65b340bfb8bbe7f459a241af6feae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:33 +0800 Subject: [PATCH 0601/1461] Add data/mcheli/recipe/m18_green.json to the 26.1 shared sources --- .../data/mcheli/recipe/m18_green.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_green.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_green.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_green.json new file mode 100644 index 00000000..1046d9cf --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_green.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:green_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_green", + "minecraft:item_model": "mcheli:m18_green", + "minecraft:max_stack_size": 1 + } + } +} From cfaaa964b141767470eade3791a524bce63ed879 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:33 +0800 Subject: [PATCH 0602/1461] Add data/mcheli/recipe/m18_red.json to the 26.1 shared sources --- .../resources/data/mcheli/recipe/m18_red.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_red.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_red.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_red.json new file mode 100644 index 00000000..f98ac51f --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_red.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:red_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_red", + "minecraft:item_model": "mcheli:m18_red", + "minecraft:max_stack_size": 1 + } + } +} From 4c9f87696ab2ac5b9b49ce9ce0a889a041968013 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:34 +0800 Subject: [PATCH 0603/1461] Add data/mcheli/recipe/m18_violet.json to the 26.1 shared sources --- .../data/mcheli/recipe/m18_violet.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_violet.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_violet.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_violet.json new file mode 100644 index 00000000..b05f48a7 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_violet.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:purple_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_violet", + "minecraft:item_model": "mcheli:m18_violet", + "minecraft:max_stack_size": 1 + } + } +} From 79ad1ea43b57cbd1db48f1664ff2c9ff9a72b8ae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:34 +0800 Subject: [PATCH 0604/1461] Add data/mcheli/recipe/m18_yellow.json to the 26.1 shared sources --- .../data/mcheli/recipe/m18_yellow.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_yellow.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_yellow.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_yellow.json new file mode 100644 index 00000000..57ecd202 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/m18_yellow.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:yellow_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_yellow", + "minecraft:item_model": "mcheli:m18_yellow", + "minecraft:max_stack_size": 1 + } + } +} From e7787a06441c2894ed1e34a5350b214b86889041 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:34 +0800 Subject: [PATCH 0605/1461] Add data/mcheli/recipe/parachute.json to the 26.1 shared sources --- .../resources/data/mcheli/recipe/parachute.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/parachute.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/parachute.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/parachute.json new file mode 100644 index 00000000..69eab191 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/parachute.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "S": "#c:strings", + "W": "#minecraft:wool" + }, + "pattern": [ + "WWW", + "S S", + " W " + ], + "result": { + "id": "mcheli:parachute" + } +} From 0f05afe9928a1344d8b87fe371ba1b9171055242 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:35 +0800 Subject: [PATCH 0606/1461] Add data/mcheli/recipe/rangefinder.json to the 26.1 shared sources --- .../data/mcheli/recipe/rangefinder.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/rangefinder.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/rangefinder.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/rangefinder.json new file mode 100644 index 00000000..58a13385 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/rangefinder.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "III", + "RGR", + "III" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From abb278fe5d27e995bf6aada2dbb00c1aab48e8bf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:35 +0800 Subject: [PATCH 0607/1461] Add data/mcheli/recipe/rangefinder_recharge.json to the 26.1 shared sources --- .../data/mcheli/recipe/rangefinder_recharge.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json new file mode 100644 index 00000000..d6bac29a --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "equipment", + "ingredients": [ + "mcheli:rangefinder", + "#c:dusts/redstone" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From 92b2f3743c2717359e9434043091307844a709e6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:36 +0800 Subject: [PATCH 0608/1461] Add data/mcheli/recipe/uav_station.json to the 26.1 shared sources --- .../data/mcheli/recipe/uav_station.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/uav_station.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/uav_station.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/uav_station.json new file mode 100644 index 00000000..24b97d55 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/uav_station.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:storage_blocks/redstone" + }, + "pattern": [ + "III", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station" + } +} From 3ea00b17ee1e28b7ba96b93f5a761efca0d2b597 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:36 +0800 Subject: [PATCH 0609/1461] Add data/mcheli/recipe/uav_station2.json to the 26.1 shared sources --- .../data/mcheli/recipe/uav_station2.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/uav_station2.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/uav_station2.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/uav_station2.json new file mode 100644 index 00000000..74bf0d20 --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/uav_station2.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station2" + } +} From 66d0579ba9da472133bddb03b212f1a93d2c4485 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:37 +0800 Subject: [PATCH 0610/1461] Add data/mcheli/recipe/wrench.json to the 26.1 shared sources --- .../main/resources/data/mcheli/recipe/wrench.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/mcheli/recipe/wrench.json diff --git a/minecraft-26.1/src/main/resources/data/mcheli/recipe/wrench.json b/minecraft-26.1/src/main/resources/data/mcheli/recipe/wrench.json new file mode 100644 index 00000000..b8c9bcff --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/mcheli/recipe/wrench.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": "#c:ingots/iron" + }, + "pattern": [ + " I ", + " II", + "I " + ], + "result": { + "id": "mcheli:wrench" + } +} From a20c7b313a3066b94ad95099efd22d1d63fbf0a7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:37 +0800 Subject: [PATCH 0611/1461] Add data/minecraft/tags/damage_type/is_projectile.json to the 26.1 shared sources --- .../data/minecraft/tags/damage_type/is_projectile.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json diff --git a/minecraft-26.1/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json b/minecraft-26.1/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json new file mode 100644 index 00000000..1042db9e --- /dev/null +++ b/minecraft-26.1/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "mcheli:bullet" + ] +} From 6925cdc6c2f40ea83ad4ed5d2dd3fe22b06daebd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:37 +0800 Subject: [PATCH 0612/1461] Add mcheli_logo.png to the 26.1 shared sources --- .../src/main/resources/mcheli_logo.png | Bin 0 -> 23745 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-26.1/src/main/resources/mcheli_logo.png diff --git a/minecraft-26.1/src/main/resources/mcheli_logo.png b/minecraft-26.1/src/main/resources/mcheli_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..95c72198d76d40c8c513965b3c40c83712ee54cf GIT binary patch literal 23745 zcmYhi2|SeF7e9VyVeAH3vJQ%}WZ#z=Nfe=wO16}(vV_PoGbpr(RV-1m9k^W3+#H05LyW&;3lo-#Xe4geT* z2?H$5(8uLlh3?RM+^G{rwjoZ7>A@j(O+%YqnPXv(3ohVbbBWfxnA*g|DmnsC@J^w*!<}JV&wSul!dC2 zLt!s!f3yeeO~1PMF_^Jlz;>iuWCky}UX7-%PPN%Be-|7!waf1JPBX3xr#?ui9P13> z$qlu={F6Yvx3fAWU%aDK(?Y+HWfE`}YVg{{-?_QA-zRNN3C{!luNx30``K#p+_uQ> z>eQRnxLpl*lQh}^f|_$(xZUaW2}b|Ugt*Vii7|GD(eD?ATQeAdss?DCCf2+dn4{E>X`G>og9cU(oP_I^ZE-5r} zL$cd%@N9*d{n6Y1edqq}=#faPjf=N8_kVc0X><2V!``Y`1a;L;aie~C55EJA$;rD_ z&kdaGyb~*_^bJN`_<+I-f+g-_Pm+@Jz-2=K$@pazHV!pBj|MXu-z1?~3@E&s_Dxw+ z92;*-DiOx~iCAY8D2jwb{qhA0gY6RcXeH;m*Y%8rguRIWcl*o#Mkx&k+pBibQwTD2 zr-B54I&-0d<*7H!Z78CWv&IEz#3sjbIULO(;?6VZ78M??L$=jkO%I);xltZaQ~pnt zUn*JiI(<_XU$ZyjothE`?mI_|32hq;*zWfvr}Z&_@7Cn&*L=n+0eWAeoYi9C^)o3_ z&{zt$RQ0t4w8XW%tUD%-Fq~j3*Cz)%Deq+9U!cK_ojAHUn>amG+~Yn>Lcc!vsj(Wi z3`aWI51cXt-lc*06>9ogPn$e7Wl9##ZlwHvNVzV{82#kuvNgfPGQiIaHAJmXH=kOTnIgcebhPTT{t> z=YUFFVKHVIW*yI;KDQAw(>&1}pc;aBPk7rj*1(FU4H>|)QW~HRG!AIh9cv0!txf;) zIjeH+pT(%MBx8c&g+k%EmvE(DiP5-wMs|oVnVtNCi*G__Ylz+&YIY7t<-odau`1l_ z&#!M72OStVbHv`jRk(=#)x0eE>Xdh)r(FZv0fKLwvvfUbgVDcxe!=+1S+iQE`Ex(s zy}f>M>w{v5h@B=C6f(1oZBH3>oGB4Lu3@0{RPdSBt+SH?e;#H=LpI{#qB#XBTSvMu zMZ%-yxZo1N?n1^RO84&t3*W&8y@MI4%I5-FPSY+0x|AZK9B#YZi#%6vhejHL*QB&Z zzKe?^@g-`t_6-4Rzlu(^ZQrj?O8?3f#l`GogmJb0Wi*Z;J@{6=k`4_%*0Juy*%e>( z+UwzdGkfiyurI94uGg53Va9KU$^3;{wsvm+upRrgJ-q&UB`AL;-ybr+O#jmpgLM*d&o7 zQm`mNp%d&;-<1_0L)~wRr#xRW^$wTq3;6g`BqO!k`;r~&yAL@6*aAI_6bc?|bQNg+PJr@S`nVCBJZTpXKMrzl$@%#R00=F#gE<4gmOB5-}{2myYcpwac+^dih z%FCr#w{X3_H_d~oqod2iA;To=hd(a*3lglvM7PTqh@@y(iM$v`O%x{06_yYm(u%?z zep2Ct{nR|TvFVc^tg5G#XWMpuwEuOvUATna%2pX7S`3}$g0?)5=3#-~443%;lK?Mp zPGJ!mBMJ9bav)oZXyv6BO&*5yqTI;FTw~e56gg5#%Kiv7^thnA7TdS*=2=*eu(L;K zNz9CUnax7(zvTOwzb*g3jku#*jYyUPF{zx5B5*^{nJ8+5=dP`@pZr}w{V_WFKU=+) zJ9<>!ePU*l4W)f{mX zBPIJWAm{iu{{sXiJ?-V|Gq~K@0%#+?ea*TOmCNSzmC2l;3hm~bp6o!f%ynz`-qd}{T^UA=TGd&kP%M^Nu z%JrVMt)9aQ#zA-IWvMXR9w0L)7+i@!aA zq+#bnvoL;lDfV@_D|51xTH#;f(4nL^H+lKgu2lc!czAB;YV&290S2!~yEL$yxRpOz z@)qHw^`Y~-k;~~H@81UW*WY0OrRgbKK62F|+wq!6PHoSUDl?ipj{TtY$r|JY>bSmS zG1^&wx(1q}=)IUf(^`*8k|j9brbxg@(4SOCSG-b(MDfZUmo}n(JjFW{^)cYt@uGdZ zhzb28jw2D}P9f3#g%i9V${e$$K;i>ow-;*-kxvgw4}LHj;>ve4L<^e|oNOy_&wXMU}S&)`0s{Ce!Q!GLeN zDd*GG!i&{X-s08|%dVXMu8{SYc$@`wlFM7TylQsy-3ipC0=WfdE5S15A+V8 z?7Hg`S3l?>U!-T4Up)TGOXs;9F>y#!t?+=!V(!pYA#ZsyxYy3Za+s-KmvsKy#h6QM zN6gASZ_1s;VI)b@$2*XsxhZT%aLfSieZbln7R3x35UjE$lANx5%BngZ)Ho)1DEaC8 zd)me|l1o=Pq{Qz$xW#0?fpFJ!z`hCUw>5uy}R73dwF z+Q`0|)t_DW{QPi&s`<`YpP9%QwkqztZ1E|%C05Ry$FLW1^?Dn^r_=9MnQdqeXpnr4 z=S9Sv+cGph@Q0EG>Bs|D+qh8M_I;p$(oJ06Ma!|9dC>gMbaixU{N;Gy%%1B+A>yNp zEQkefx4`^Jj?%_Ir;`S!cdZ9I4GJGxo5EgAWFNEb&bq7nm*2+6Qu5k00qf99;(2+W z3N&wi(2oY_L<~R%VoIZl$Vd@n6ebS9HF!Ze-*20dT=?s;{8nYPN5iTGv95pnv+>V= zyvJUVDvTfaZ3 zwsA7D|Nec^%nL4b)Qj3*b>?xw4UD!d35V-oK7>5PQga*tzbITe68En0a>IE1xzij0 z=5DcW^L_`E)*7~NPjB@uylJ-Ypj-|2iSs+7#HoYt*S=IS?G(B(fLNaz9P3u9r~9zf zPnL%@eY&61(qn>%y?dC8Nfx(!{h=_kE*g1gVW|C6yJq6eD9!mF5k!>}@<*0V=};Yy zYP^v?oHuQA<<_U$X6WlIehFrFcL2u3)O+;$&%E``FA33+(-7Y64U-5z(Qwi-H@ECK zEb4|SOQCU;r=;~Y7*RzhbaU9pJ%O{eQe!uOP1Vfb%HN~|tLeJ{%-&|8zgF9hd zv{VYCaW9x~q2Gv!>a^I*#}0D+5O&U$FyUy+5|6<*IqqJUMY*v?%CUnC*o~UKv*r+l zw|h~x_{EfSEqFoWXGs6=4~`j&d{GySNZ;YO)15Gv55B8GYqOo@4xUpxyOa6V(L?UD z;w#ntaw`>CoZa-Yp>EAfIi8ATDz*#oD*K#`2b#kk11C+kj8ZLb-s*Ap{8*%@IrO?m zQ&3EMIpuKa$a;$QG(+*X{`FKruj2TJV(tC*44l#SR(PEMm$Qsx};E zcSNRzpGDv7Wj`wUElnse%wqN+oDFCrU+aUw2ZJ_1DPgy!bMrTLk~^ZVBy&oMP)xaY zJF{#4cgNo~(|}vk5!=8ucYLKR{_{Dv>@x zOF=x{YR**sX>iIqQ~al)ii;KdUuk1%rE)!q{n}KZx;URq`8TeMY9t4xaVEH>6-pb~ zuK^$61wIb~#cUbkn z-(4YISY7*y|B+cTxul0{f44`HUFNU9z0!C*+pzy#0Qdax`{%j>L0Mc>v7=fA#1E>g zM>Gz%+&-K&@>Oyq6jp*Rbx_ES#>F$?w9nbYHPK|xVM`9JUcIvqA06x0Ef6XHrkMAf z%iw4^N%mUsKXaYK^umY&hHZBchX1yRS$gP6Up(W6b40PknUZ}^M=RyexQDy6oqxEJ zCyw^onf+pXT>YeU9e0w*fih(w?c|vCMPE-pN}YPChlFtVN39n@3qFM&r|jhv=PVo_ zo-VX8QsJNBw|4bGeQ)!&T2%XObKYCvT3K7}`t5@;%dZdtuik&|*S>n^{fDv($8k}( ziYR3$bpkawGKz@$ropJ%pnte^wx`g~)=(H0A9>PfvNgs#V>p9%=8(&oi z87H}!imoreHzc@RBnbBh6SXGwlASb!wWQkb>`f~QOq$>PUH0na`hrq_Qq0EOyMaU} z125~XUJ~=m+XZ;Xs&f}-I;ZCQUxf+?qUntoM$3Kc%zCV&W1E(KMy$8*C|0q- z+6lMFKb4o_En)3*nt3WW<-4WI3w~?&)A=wME1ddAdSi{5rp7$)!Cr&IiSeFbgRui4 zWH_j7okJM@Np{vuUMZ?OKgIc=*+jaH*>tiEmlzp|F)GD)Mr95+6#VS2K4S3rV2C?f zIJ233-J0bbQsM6Si!Tb%B9X_U4L#vr4~V(`E>cBNWw-eAJ&<+>x?k&2J_tE`4?N)S zSCKZj!T;N=uHbp7!FSN{v$^J`avS@TSD;}2-LK;idS zfXVgn^f|l9qWrtxKCk4Re|Yool}xjyFQn*bE*4#(6KwF3U$`1cQuD=1^fGLmWY*CI z8^4NY1xnLN5@+o?oEar}q2cGN^elk2<`q z3*{hR{|;|Yfy#%v*RGyW5`TUyn;PCL(aC|9WL3 z`gL^K5Pa~}8P2|p?iE8TYVG=tr2)P&pIaqISr(G9#2utJ;jIO^pYG28Mcy-W8Gp_L z^|+l#>|(DpA|K_mhM|u^mJ6}YQzu|N)0%hnr=T6GrH3dHdeZ zSc&`*r5c>+<8Ut(L%~fd?PFJT=w9vC+h6druns2Lq3u*q`&vz^_r6K~_*AOd2fWj$2E{S_mH7N2;qD$jtk4h#|LYi1NtG zGR!(iZSeITE)PkT%@ljDo|}HFitCf8#C^H!tzb1Qg`T4VWku3Wp&mF+Z*yW@ppxDCQOXg`8oSIPV;g~ zKgT?eV>Z_u_z*ADcc0lu@%f?WqShik+c!;=%lKeV;gUyAB<4z4pR>r5CZ1yg=Ma4O zNT3EV(bh;$C)`OTeimBHFz;?Nz1&8T@)k2>}>f=v6~QOCV$*9Kaj({6IH&N9!Lhz;W^ z?=h6R+g@zh;$+EePUdj^6SIMDHU(aNRT}-(N3d^i;Mif;%+OrOC~Hj_;@N$;X}c8Et#(&BX+mb zcQ%4k!j|J?*PN-}jZ#*7cLS-TTeBYwD7{|?vDp#MdqO|bzWlM`>dAf5Q26GiCR$gE z9Euq!y&q~UiJZf=|8CE}Or1;Ngf_%q zFP$*xE(KCAe0-vfOBXPm&C@WpK}e-65Y#&2BNXZ_xtg#9fOYRIL%EA;JtMuuTmmN zL|?M7>v8xZ3lYNt-(9cWzSVXo=(Tq(;m|bY;$<`E@IMhocGrH~cgE+u@)g_k!RIo% zYLB+>F~<{{)%PwW@7=fe>Yv=5ooI2&>Pe_S?l4M!Zpm1EGexglz8HNlvIGFd$}tk2+>H@1j#L_mCAY^(L4m^j?#WHRXiy zgZFodk94#^;YIR66ljLHk8^wloviGj`{x^Xhf(7fOu$tiS|)ZWqhrmqZaY5NOHmcC z7g95=Qq>s3Yi`YB%GhRl4Ar^7|H?4;T0$f>_|>$;(RS>_Sucix`ud*{_3bZ$p52J9 zKJsp6j$KK__)hM4HKnfLVOyvOdVCF@F@uylB>>X>fy>tf5$D&t-S-1mZD$X?6ag8( z=ntk0Z0&3*&OOtV-{oqKDtLXx6uqQP%5M+W;dzq7sO|~-y6|rGz0sd31o9`3qOq4A zv&}P3Ic^Hmm0S@tR?Z_wSCO#arg!-POct~%j6Z^tTwg~}ls4v`lo-p6aRTha=Ig)e z3Z`l?=9CgZez%p-^b>n&E$MHNGK00{(e>i27j2Z>mL~1J#fq__TW@xy1~wbU8V6_* zTG%t!QZurzXN25)oS}R34#95M`Od4;0jdVZ0K}n?X<(c=QjLpv+ay9UW=41U$F3wl zg&E9NeKuF5Fx^|5wer9hjE;5C{;V@k+lmS3!Ksz}0Z6dVA&88u!>)AN-?Tl)-Y8^4?kub=Ox>i;%gSk?SOq zK{~4EoGn4P9YY@C4?b6+zz3Wp(bqxcHCUn;KzDf@0G4z0{lzm*Y(;@e;Abr*s4Yxq zgOQjtD#4$xR_{njI2jo@O-Zyr(GbH`uuxJPx%AmX!{GhGsnD(3=s?=vu9vvAYiyZ2 zspWeOm8tbJJC6O6qb+mZu*k`RMG3wu@l7w>OqnFd)m4OiSz0gj!N|whwI{*sPv5ep z<;x-x;R?CEwE)PErQ`;4-BDr(qM-y<;KT3>+xxWmp~#~IrtY&;)QVDWb_ zv2JHt&s>?hns3S#Att?sm2LOUXY|r)wO*#y?^d-G26k9ht<2FM%O72dH?Ca^Fvg6R zT~QWh0U(hJJaLCXb9Q8o|0tAkedML-ak*i3K>9Y=#ePRAS)zA?!3&;jGIXfvUdtwK zo;ze+?Zg#vvA)fN7XI(_^D8J)MQ4U=_GLWTvE_e>I=r$T|$uYD|N zBd7n`0N15jia}dY;iZ6?TXOu(6|uM*0G+V!b!2m9^ijy8KPqh-z8u9q`iAvlyey*j zmk(l6{C?S1QGZ-3+sG0t+bge_t(Hi*1pjIuUOro%AlFVyr;Pf{=2_K$xE$O6ep1%+ zDn*>1J5u=r@ide^1b`<1Wb)uBzojawR}BAHF!BTg3nX)7`hq?Rc7HEVoxSf7+}9Fo z_))#8M6EzDAzbhib?Q;QI<#yfmcqU(6G}L%3k%fuhSK}br0ivw7WRE#w7oTGZ2M{R z?9Oy0oz|YiZRi3pKhS(r_IL>9ITSH4xb2N5~5pVOL$Sw1a`t*YZ zuRkBE=ORrrNC@9rQ`0v+6@SiqxL)yD{pcHVX-bfNO|T>+@xf9!(Pwz$!PKSPbDgi8kaT6Biu^u%F=)RldQC*cD;$r}CVMgq z-NXoHUcCB=z<4=#EaxihUB>kwW650!~H?6NQZYL!s1}t0~K!PtaxX`<>wn( zwhCdiSFs=e&3$O4v*qRWCzrGXbYD#8ikur{4&;^NnNL0(1H1Tu+C*W3vst5H_rfJv ziI-t9msiwGkI0`%!AAG?-K8=i&oxnw5{a-V0FXjYo4O9cB7fXO93*c4%DyRZ_`w(- z5}6{4QYHR94M3q|3tnK-6b^JYx9rpcmv`ONRg0$yM79(2V@AnXiRt>AYE#EXPay0m zrF6N~q^*DXOP^&x)lu~9KOQ&$jj+UY_j^P!FckL|cbteI8k5d}@qdk?-{r)vs1URP zc?-7Oylh7=U!DrSA+V!evbs>WsnSRL+U}T3wNIH=x~)v;%-%h+@FTu#cT!1V5bk=| zqkcl)lqDIocEd3#3OE#~P=I;`aAe>sq9E8dAao2=kBPtYTk|+JjI%dfehc}8yXC8Y zEI?`Biu&y1VEiH^6X`Sx9BpU)yCuHF3g(01JFh=i%{5hIYhm4K_oxqZsVYm(a?SLL zn_0t$^Qj+Q84jA(%~2cAjoM3A2{{p3IZ7#DcOWUC9z zG|c3)J~c&nE%jC#%h}`Qmsg$@c&;@hILM9Jcl4z?@1a-Z>MePau@Q{g=eO5%ho~>{ zs>?5GtBX8*QeZGc$yOMt4gL>cK0?I@9{?^ApdeR~GXS}rR^JPDB_bZ+X>;0(;X5Xi zlV5Dy6?0?Y=v$f7I(g4+f5!G!b7cD%%LoexHE{?NHf~hi`T3>7f_AvJ?n+KcJwt|tc;PBc6w*~UjJ%*s>P2AAr7H$n zUeQdKwPp8ya;Dtj>cJ|~HO&E@*S}RC&X<4BX1_Pju0f6^T=F>UQ!v`^R9H5qEUbP- zpR;zGw`zXqSKvnZN6)3G!(~^0IhOq^wkzBC66=-P-PcuX7Unc+y1Q9_G8Ri8?tahr zhkjkodBD7ORdwPelOH&t#!>hQ5UE{f+CPAC+RIR&?&-)yJh}bhZmxoN%5yJ$c@0Cl zEI#||)u}A0_yoo$4S6B{csA21k<49gw2of2K@es1Vb#{GiC{uya&y8#D|?HoNA6?F zech|qg1f)#QL;I=igQfG@yWMYYH6!&Wxiwb>z#U(Zja0epVm3)qB^5~0&qs3gum-?@zhqw2=hX` z64O*qx2NB)Z5AYwDGFf}H42B>{CJ;uY`!o0{Q zDt?)Lb0uge$+^fXRsjLL|8KNlafb>2rxzVy-AmvHl5_QNu+Gp0g|+oa;?QmYAf3f| zZ~-s8wId23VW9{vfaHuq8Qpa`Zk-iuMEvHGs$H3R7!zUlwvIae+a(Oz#YMr2D6=2H z4Tytq8aG&8cZTE6>_vmpQ+sEh=+n93xHh(SyOPLTeONW8>mr{=3(fltZ<+|QBo}bwIar22&R+KB=R(DojMAA{+tjbg9 zS?{Zft+!=?J$`dAXu?LpA0zdy=H6#EZAgV#r@fMif~z8OZEmNqB&+jid%@PE3%jdZ zG3FZmZz6*_=CiXnDAKf>%q7BRmY+u`k-$E&mG-H$!3%HFVoBORW>d>|h!=q}*W z%s$bKBp$SilMgtH)=dkPUTRk$KQe7RJi+JxAjD)i1)@<^U&{g)MgX6hBG9hCb_BZK z5eDiIpN9fBhw>bt+&eSFoe2o543et&fjh;QIf@NCtl++s|D(oKdOWIQaK8`c_qZlEYOd@Uf$GQ_|n zyNgRA{{tauVKHNVFUy>_s2e&I<<@_~pTH`K-HKo4LjiZ1D>Wy{HT= zA9t7lKu$bC0DlKJKyKON1@qfuJV1e-1jZ)`wtfoDd+*KmctTjnEfRP#Z`wHq8e{uj z7#D(Suq?=Nr!3j*=C#CgzIqHMVA0-z~(%1T;z_KdG_`U>Ce7wxt_bij_zV zAj^V=J8+WNo(Nbrgb8(SJ7+Rdzclp`keH~IsugMa;YNN^`j&5gB(+E#*RM05jv(o? zmv$mZjyfQ18m0}h(^=lW2XSqD#3Qoc&JIPL5qx^Dhg}lQlW4UV(#l&}ccox4^+(@$ zR2dpnaw4? zpAY**m=y5I67pZL&vcJ-6{(vg6Rc74kQ5C`a1 zxc2}0`}>TDr8^4cRn1Bq_h9eKl;$85Z{NvK4pVfc4Oz?MaF9vg%C|dN%^uI%IVFWa zH~SvxHxsIRuN?mDF91WhgU`Ev*j~}Q3bI#En0z{UE$Z#j$O4e)Njz|tT9j?n+Y1i> zo}%iuCz+?>PDXREkW_j~BbhY;F(wv`)DeQmkbg#kqps}hi(e`ml zjjvRY5ICa588=E#^tOU??NAhv!t=Dm&3LmD*J|LnR(4>0aT-lN2zqHwAW@0M7l!+! z1u)5hVD>tK_=V}H`s7od^>-{+#DCVM1^xYKlVSWlOR{vzxxhZwCyI-AuIP>2UOXW@sFQv&*dp)Aaa|}_K=7v>J`5v5 zRAm6bh-(A1mMDl*QUbV|9~Xeo7-Ad-X6e%VaYGM=%ir?>iM3c1(PGaS=E9uD27aj( z0o=ILA@zl)4`*M@WajtPGrUEB`6wcKnL6ktPAUx#i`-9!~>kDRYrmYG_vMY9%N3`Z<6R?-s&pHOK{ynv` z)YdiEU5%Q3S8LU~#$B9`O6tm*vggh@i1;{{GU^p*r*u}w<_*T+kL67dm95qjBCC4Qd z!)Hqw$*0~N>&PyD_R*6*b+OF{ZGu=6Aw`Sj8{Jg%MCK@`? zyt&>#yt&?9v$_6{wz;0xy~e)!D05q+#7%FrfuYUSGO4dE(fM#~GGZAY;#`9*o}A^* z+GE|R6EPJTj9+MpXctdDsfNn!Td(s9bi9}X1H)QN&Tz01wPR$ERo96GXbC4k3<*FG z(@@aa1dNyl9?3#c1y}-WS&$CMnE5iNU?!N4r zMs4Vwd--RM8TeFd8vvq3l{pyCFjw`8S01lw`{3EYe=%_E#aEKeN6+6HI_up*I%SSR z!7~=Fc-N~?9fMdt!PPZvD8k&ydVgABX!wM-Xu{ffxVBPf!q%7FKM^a8%Ew!qF?j66 z=;E743(XsohJbhx4%Bv70Sk-80v{Xbk41v{QxMRXg>aImEacbKVbE0eXVfYHB4vR; z69_{JT?ez`u=)KRAPA68yksM?vVr1RCoZBDHWb^4`~A|%JRtTt!eylufaa^c>2g5k zT5s(v{hp+j@7=o=)8kHDa`8zTgdwK7PF{%xQS#s+nsi%km^2Rj?8W@-X65bXp-^Yo8 zzyv6q|JBD`h9%A;Fmj;qI+%a~9hi_6DC8sF29;%RSRk?v*l$|oq@E07r;2_lf{tUN z&fZf`fVs>Sa$FJsX85Em zqj}-yv5!DBcG6!wNXP{W1DR~Za(jWEB4_;3SYl-+e^Qe0V8U)9sLZN|6P>)8=_25W z#zGS(xSarZV+D=(xq;w-I#|{K?~&vmdOmD(2G9wDovTovwkQZb0xksI_rkFf$G5SG z40C`)hRuDxCZyr7cna#sOm zk^6H(i+z_JNOZ~qr4Af_5tx&!Fti@1q4owyHvcNed!YflKES1eMr}tpS!l28E+nmon}Zb5T%xlPpvo!8qDNBAP;z-`}ZgTIDcON7o!JS$NL5dkhdvbCE+ zU^GGU52Yl_G;N+MX;U`Lw%<5h`raBzZuw9AWvvRb-DT?lG92@Y{Vgb5ja?r=m*@~* ze3ZGDJe{h9)p``A@{tR?2;u^R(F;3_1*PLl9&KO0@oS=%TR(Mt*%~>9--4k{n-D-_ zS)ZlfUJ|I9VMAe}WPQM#0Ws(I~}zE;WU1>k2;Ssa^mS>P6S48L(_co25EzZ?;f zMHOe}2Mk@Jko+aEEf2Gqn`X`0b3g~cKB8uX<)>YeUzhQsA%Uv`u861WAJi@I9~?kT zeB$+U@fQ&54u^OydndMo@L4ckf_!wG;Q{-K06#uSm%jfYc72p*nR_I_!K}M(V>KYg z$bC}_=H~`L@~QfuyLhQ?A0fZJvQ-7(7QzoVqntti70z$5qQwsyw@W0`8t(WRRJO(cEak6P2ijfT^bH18rT7? z3w|gDMNFZ2;lf{Y!s4wy7lDwy4SQqw7DLZhwYeqvNQ1Q6$TOhBO^$_KM>GHpc5?Cv zJQA=de?fgTKgfp$!1jhe&R(S9F4PctV2e;ciWt z9GBiAYwlQRHDH8f*Y1F7SK^dEDUH3`$R$Qc7LA0dSJQ%P9xwicxZpJVMf%hv_SJtP z$d}AoUSK^!;oQ<8Ts=FBB&|Xi#5xoFjF2F2kBZG@G->sjqKuQyya}CuFV?SzHTda1 zk0h~yvI;@ax>7F!XcUACC_FmGqVDA>QRUiuAw|c_!uq3ZLY7X_tE#g!!>v^nbt_K> zznkZu7jCoXv&2Ns#~_#NGYO~Vi$%=k3lj3G@15@wHNEcsI4|g9OHYi>q~~P(q-Wo- zf7!LskReTizCo#rzCmDHNazL6mWh?JfE8ruzfY`X*XNL-SALxcU449ieLbCT_LZJ& zZ-D%MnMZq)v+R2r683ZRy|dHyPUUluqoRCUI4Wxy>y)QC>Xcxh1|b-q2-DE0iY z9FN&Sibp+Lw*dH#Bq0F-?t%gvJS0AdIoIF0Bl@(WDf=k>%k|FMjkC=iW z0^~5j#QxXEeFO;HfD=RBF(q2T!$F!3J$zn$@y@1KN)Q#<77Poat0sC5f}b!V7Jh;z z^_UaT&LrpR&Pjaz>%rcTDUHuyXKfhdoh}&>Mz?0KIk24DwZ3gYn}lAM2FE zja|1-eqD~gml8LY%BI1`{{kcT_OtbIy^p<@NW5oXWk)?bVIuo5^JeM9XZtr*KSHHmQBB!mpo#@WbqVn}eeTRevP;?w`j z;^l_~7ey`h0@}54`a;v(jjR(){?Xe;qLLZ^Veo?!oc^)f#-dW!h(Gjh|G|%7qzWO)Qv7KoLE?+P_ zy4@-jaAmpYFyCy?@8&*Bi(yuSV`U4sEqK{n&m~-c^;vA!mGIx53jqX9%N<2^S^6>q z+V8lF*#~168aF1Y{OOh4!`w^q+ElHM8sA~;JZvNKKYzD&PU4)mbPGGsP5OF3_(Jk& z1e?VMg%r+=qdmS2<=J6bBif+5&J&`Q8W9ln&3Hto}yNpBXI?sMsXl9O;RM%V%$t_^q z)E~>dv6L|BS9AHF+uJR{ue;;)<+>2UWmK^#ep6!b{z6mGQX!3vQoZxk~BU=UyNUNkSUp_jiznx$H9) z${&ePS_SdWWMv!*Sijh(KtutloC}bunLs3zJ^lBEHERZFf)zxCptxa2WPdIHnB__IYAe zXl}qU2wL7c|M3UlJ4CRka}!;9|7Y@L1rWKY!CtAG$aLA^<;lSipG(Q3u4KZ0nBPFLJhFK=MPP< z3CWl!#Z$P5{yhP2loN5|Pk(mpfq$U_Cir|B2%U7es0k*sXEbA(r<6;dsT*vz`1fvq7;J&Efca!>BMot2?fN+a zw4e1U*BeuO!`BD1wi0CM!NPmeh9Nr=4_V3HtiTMSgiesSz#o0qxT{jiPwx;tp-o2? zF`(cQDBFjetp3TtB#I%=AO$WTibYOga0&0e;^44aU5jJ)>hu2C^|gsh%&3LZd8T)l zRqCHngNm?AgBP|a@3*vIE@Qit1hd7yx! z06CKdGuyNKi<3!I64QG_M;7o+I0XMqgerXanR!_*-%3@lO<&nZPG0!Sre;IFZg|J} zgk4jE$}ZY(1a5GD6jN13a0)n~Z+{*9JcGOiv^x-fpfOaO>|;P2bS3s>W5l4da^k3u zb_`tXoiU7ROSEFU#ZqX6>X$TPVSyVXSP}dzy5Ayl=lpBDX^GDX*3=vIO|%=mN>_h4 zpP{c5)#K(V9H5qZhe9daZZY5S-KAjxFyEp0)10LNy7PpKNjpOO9V*UU2)%Tb#D|PB zj^yn-Fdt653!1yCEO3Tm*0IO{=2T`oBeLtbT+{mb$=H0_txwK_YbYov-^*;LHG41& z3K*Th52}+ovzMN+7pul7(E%(0Qp)GofaL-#TZ}m*onsviV+Do>N>jPXxvy?(qeD&< zaWjU>R*LaW)@ zQl$!>*`ch{#=K|=A;m8NF%Wd0L7r6QMt=(>7RJC}EI(nmB&HK;f|yLUWh&r@zx+d7 zjkE4x4#Zfy62(PhnzzNKr@!uJp~o#sRO*Hn+Ds#cX&+{irU&D9bLQwl(-jN!bb-AS z&lq8M7`~1ELCm;HzR271aA_y*L~o{j+e~I-m|@8C3Lk(N@(~pzpc5C6QETwiniwNY zEVC%H3_921S*ohD%@%tK9!J%77REvk!DOt3sno9ojh#&yE82>!+&4?*wkZry=(V|f zRV##eK!hB7R>Y$6YfzZm8OH|>9rON9Yw}{8M^8J5Wy7#?(uc)?mlJA z2ph;?x&W*rFA_zN+kG%YrEw(u6~CE~8OH30-?y8$obSBt`K>3EnXm;pRVsA^O65k! zZmwD#W86PPx0l$HeEdcOV8p=JbX45DzsqZW=$Ne(fz$s8l+f0)OghNFjikFcKQq!* zgecPNn-0ezbTMc0kmXNtb$zYc=56m1B^yTKFFXn=$AyZpumZL9gH6~cxqHyb&Eqw| z)c{f2S5!FsE*)r;Bry?<1;~<^=(EQ`q%b?n1#zAal95nJnUPC7kFpRla$M?jiFat4 z)pZ*ALS_MtcGg*;y-SvXDVZMJV0Z?nQ=!1+NTF!ZP4x#^z=tg+5$kIqA2~g z_P|$kMb<}NDnD1Q|2crK!t63ZJJ~9y)=ZDLHf09rrI)wcD;LCU-ZvDe0bfA2u zw6!ZH5_7oY-RY5I31`nFtbMo56v6&nXnPJ-6ZyD7J!5(I^)ez(zw?R~aEWFEC5>}n zoT1$vAv9iggyn@VikN&A#sOzhHk1@Ph-@bXM-zR_&`&v!Ko8PHvLRWS^on-Bxj_$R zHqX@7rL2aj)kn-z-0?vZU4V#MGwp-6{n7>c)P#f=zA>O2MNDdm?BPJ92{8HZuON3A z)15`Hq5d~sZPRDgr$tKog2y6E>*<>!^sV~6JOX{eIgs{KWoMke$%iE*ZZs7bq)HzO zPcnn_iT8j#^s2P)L+Y~Gvirk{S> zF46X%2gAX%X~FqPQ0hmO>uP`q+fUW!S8RR?R6q!Z6tSycMXB4J#qLqLv33`xG6^)J z;E}q!y5-P;`aJZohKZJ}i`Jq2k#1$++?kCqND?W^NcMAr{>cnu4JhajIE@v}wkKqa z72UnbV3A>@g%T=E36{?YMu29Q+caS0)HX^l>C^eTJj2$IDQjzhcSHfJ=NS|y5Y&0 zQm;SE-18_ZQ*J>LiAMIw!-t~EMBo|{7+kk5>h@Tax-tFkP6O?p<}aeMpnBtBgb>Va zVkK2sN&Pgyuc54%7>>A2~3E`${{`bX=@)j0%U}`ISsLm2=sG#(ItY+Mfa< z?tK0%y{Rh@MpTDI_j3qg8{#Ayc_vu>?`E|Eol9fu6~%_lK%xPFJ{2mO=Rd?pNFj&ALTOe)uT+ z?9X@4@!Kn=cu$MA>5c307>-CB0K9NZrZ3!lW@xgg&o81)Bx|0L>v=Au-E2qRw&uXH zlZ8^ek}NgtfqI=c!|7N6kOOkVw6p6B4RH|Q>s-VHl|}e`b~}vie_sf?ufcAreVFa2 z7@dy2_&pM_>-C40>eWqx^%vzP>;9dxE?F9xO?gw$%~vg+L)hDOrX?n>*6;1stNS!! zOrg0Wu@)Gk@xvm1a>HqIXlj07s)gSffeK9-|d|vO!#CTYe}#_z$iya zqD5JFk?c|Kt>n7(o>+bPpI10P<4y}ZYY-BM ze{1fGuIX*i&V$Bg+R7|_VNwONZ*@m1JpsFIzW$q1Z?pLKa*F~9UoGl}bqbt$c5lCE zL3LHP`#zuSw&a)9FR>EmPR(?l%*k09!C0SrZbhlzI)*({N*mjwOE~IRwzF735Q;5I zLM9PY+Qw{)*5>~|C0utrRsa9Lwv4Wk6`6%Dvb$N`8=+73u5gWzz4y5GCW-7-wnEA( z*Ua9tOE$R(ab0yUF86o({?6n4dB*#^&+DA$^Zj}~pD({UI2-g62iE6a9wPEu-AwhP z&G(q>e!ML*?9fUYlh%j_gZMLQqW6b=B-@9rWF8N#83@u3USk?$(57~v1=-R4Z`a4$ zloA9q03qgOsOXr{w0e1E^01~*iS)F3X~g)h96rmO@Vndl?8NEU_jY5sd)M#lP{Q+Q zW8|2bBDkkN%E4lu1u)y`B-{uT+CHY>hSF#{_uBN4Y( zeTkKPyErwzDOn}n5-PNA#oJrC-MFiw`Z1lcKRx?KCBD1)QCkx8ayZri^5l#BU`;}?=o zdC4Pi&tAm9tyiYEh)kP_T^mY=7)cs>$l_HHACf4&s`3OQk>gPZma5|8rMz2bHKNBM zZiU2tVa`HIvt%w7yeT~&}nn3P-CcgNK(gvtK$P1)QnieB6 z1)p~9`EI@|CvI6w1A<6$x;DSY319E{5Bz})PF+z;f;$2U4$svk9g7ua7u1fC-(do(T9U~R z4`Du$7Y}(M=BjY2t`i05v1RlYR-}kl{Hixi#0^R&3tAaU?x?F|v;v&WnGj$U1$5-a zYVd5vDge8rO84K46br_vuE&b-ZpFu%to$I!Fp7`U<{{)evR!^+G>5?oY)UxXEMTDZ zB8vdr4~JHgU#LOW!v(RkZZSxe@b!rGZgDsjyfRYgv`%9;1WWl9`CIkU`(qVI|8s;6 z9@Z7Wm_^lcN9HOXB_f^jSesVC6|xIVBzcEJIT8OlIX!P z$$@j-TP!#qQ123vHzYiVPt5YY3P^ZaDs9{)U!?!=`-^SpQgGW95Ow52ayHg-w|Ill z?-V5G_gA^&_7nXDMcVy)yt2waJ&>KplB_0cK02`dd= z?*53iSy2d)IZwtL#0VLx(2DfuAzdenz@o>bn-5ivgjXNxb+l$*PC>qrM^pxM_6D$RlAu*}RgJf0_h*8X zKs=|v#5!C)`tDB6kVpK_vKR|8PFFujy~R-GuYyWF==eRAwm(TLUQaA#k=CSFo|mAn#Yt8;i={EK>@#-=wQiRon_*H>p>;Kl_-3s~ZI z@T(tuBt$juD{0%uItd)<`yJg7n>0Y4|1tZudK=nH@Q#|O_S}5_Zr1ISoGAuP#?Ouk z*!vfn8rol0<`Ig9Fz_oh{@p9_l8+?Mj%0&j4V@CjcRj+|?L{7R?8ePk$w}GpDU9JKt_f{_u5H(xPT4Ziz3_-m=Vhr0p@T>G)(K6cM4p!y#qdn(W^6W(t;@}WZv zMBcwWfiFzTJJz%XIC1B@QDDtS4jL@!6fT>3ny5OqnCtwbf70rDO9c!nV{b;C+Mam{ zl-I}IS2WItI$Ycf^unFyqb?!iRF>~^e|{r$A{Jd0J53;|dEShxJmUVKF6i0(o7w#( zeh^9(M0QPr67s3ahq|WOV|fsWUZ-pe*T}fMX+6&Acs>?!b98-k&gwyW(T~0Nz_1>d zVFDfl#zGTN7qI~pAxGk(>3S;Vk8Acd&9Ivp>`4fBfAuK5t_lrrn2~1o#^J4k-G+YA-09X*TdE@*IL8vlv? za&^^{{M`fPD%a|K@Brk?cQ3``D!U5k`n36B#{du%MbU9wsUgjdsAsA9MmLx_8J{)2 ztEhc_yTMKA@nd?;d((QMX{3AR?1I~gj{;bnnm+r_7K z(zgfFF6?rr0@^g1(30V(?NIHE-iOZf%8|c%1{q@`ZF++lN2q-0F2Awg?#iYfnCTYx zLAlP0ginh*0ZYpa%_qo{Y(J+2*V^lLE7kOQ{vCN@1 zqsk=zZszN(4O%k4M<7}t>0p%J&3BcyAn6D9`#t)jb}FoB=jE~1UgquM$Z{%)_U z)Wr-sd*)N~bWcoRj;9oQO{LAo|8Vm- zDbw=yz5)9uz>@h4wn;h~A4qju`ntS3&w6R;?b`>Uva)kth_33YG!)lZzR!;Xz_LSoceQ=wcSo2t+X2Z^3r#dle8%!2^HH6uTpc8{5Pd^*^nKFaE1<_H6o@~ zFs@67UJaHXab@5|QFwaAvQ)kdeNq)-1~dC4MaewL%TY;Mcx?EQJpY<8flgI~=@BQu zN~=nK&FlU35o%Y&IL-uCiHgM@PcH(#Z<+E&C!POl&GmiCHplXemNjfe=Sr9hZ&c)- zb|79h$Rh3KIssV_YZ@;vtFOu6k%j-Q_hsgT$YpAV^)^oXqns&>X&n>u=@us3dDoVszgzbr=dE~?>9iz&jRw%OBX+~9xnB`3vg}7r ze|JZTfOURU#L9_-sS6z@TI!zak9+`3%d4~eCO?gT-rxQ{u%yOXBh=sXCr_@F`dz%+ z>l|xsq?f{*vz{(Pjs#rcYL#@M(0C^j#}53K7;dP1x9xY_0?tgIWsM2GYm7#qF81xD zzeL-GRySb!2=de*H(ZkVc?W7$5nLjcBdMRJ7TYu;iD{#vuMcgq$!i+%wE!O_wN@x5 z)~cwLG_LZWWq+enONllwbI9%HP`6fFdZehp)=@ueZ&Zl|fNV3%E$gE6jnO)uM_1(w z7fCyJl_o;8_7hIxe7tk)%n4@M9ZCkfd%KciRpN>MTNGJTs!-$3OEzEu7bSkP#RzIf ztKGT*IkUut#X3LzyxuX%&p_#XFLJDpacj;Z)T+8-==O<6J}D{YTx)o=fk$ZWOMsQ4 z*YVBv%kPUPU4JzlyI+^fxgWF5HiSqzFMX}G4+mIJ$R9&JMDPWjY$ws28LGsw@#P0a zNzk)qd#ewOM}|^XU1>UxCf3`il0Q6IVfqv~&h{-a4Xi+d@{4mL1Gk^c+4kIYE-ch+ z%DZWJP$40DtNzuUHjH90?jV35kQL>@)k7!Eo4`Wp9yz8{VssC)ab6R=KUDc|1+?}D zTBZA$J?r^LPlsYP_V`VLv^#q6aDo*Sxa{1k<1XUvDH5AgNV>rURmx7;b_PfEAIONG z7tP$8k`B}8_TL{!bDJK&jMj9f!cnI)zA@ETg%vCIfm5Bh!cX61!G8iIdx_HBTb2X* zuBKpP{N*0m{mY-Pr1>UG)LG^{rt*SM_#R3gd?wkNW(t&kz|punVDLA$jK%-dBycbN zx3s29CyhF56Lj}4v{N<<0a?r|_I;8(8ID8~>IA58ltj)drx5)Qd>7Ig!h$&Zft&6aseGw2)*or#;OV4(YudxP1tQm2>tsQP`aCLm;9j2 zCmYiJ&-F#*ivU79uBZKJpI+igFwLrbnRQVFABE+soY4SAcEl9QYc_6VZ#uf;@~;vy zscQH*#58%OJ@keGcyyQ!RCba>nc5ucO)zpUm>u_aL)4`o?rm`*D%W$0xfhd{Mh~e# z%Ky8A%a7?#l0O?O_|z{LB5R(F?VDv{hyA1B7p4b0mj_A&r=B|lFZrOt)IqNlBD<AZ!Fi@ep8VX+FM_69k6sC%CUcc!-rIU+_D_RG3T++&thk> z4ua$7Cv|jV5BJKn>hm72#-wuA0MpolL+te}s2U6e}?{kGb){YJn*7OOZ&YCcUGpCAo&p=9#r@x-KgsFT0Ig{!o3-kq*2NhCg?;@- zH%7RK`5wfY!M*Aif8p+O@7}Zysul$SQa7QXUVYSt-v*9n6;%;>%pG2F2bz7PM4;VynSwu88H_0t8&ovDA}^(6aKk6rH%PBSGE>L~ry` z#x+*2XcG50;-%bBV_wkz1a9k;@ z*j|6+ww)vg15{|^8;oVF9*%BOl*sLF)|Y{kaboT{zxa`9>q%&@d(%xG>Un&}Wcp+q zjODBzo{J@ul!mPv4|oz=(`#N6+8+u6A)VAxU0%qBeKM6eP+$hMM&!p*&m=-ox8Rwb zL2z|J-r4jd?hi6aRz77Lew>Mc=0CV#z?@&6z&PZHlHpTkp}pMWy2`Ywsu3{C53_TdFoW* zNkKO7SztoG{B^9)fEwzhAe)D*mJ(#6!CHjZe4TEE#7YPB>9mA-j5h7`##x*>_+4#B zNk!k;T^#mK$#jJyQM$_a>_F{d<`oU?VGcf8cRf9yW;GoA>C4T6kB+!HxPHTi=4Uco zBGeNb4}KV*oz%IWIfd0>ac_64XWRL^ZLbv)+Tm5D9u$6ZPiL87aaz0sR`GRwS$3`= z9!|ndj?2BDRIXzc6OGqT&e)uggHoN~h{0Y8^U4wG#%3N7Ha1WUdzy!fYq7k{ZAyme zHN)NNF;#N!@3b>=TKz2zl<#YH$$S=ezrL-Ns94_^*zxUyZ{r3p2zzLTyY*mxEk`bH z98NL8^xx&(kU%5EYp)}#JL3A?3g%->EvLrk|JLo~61=|p98vqTu2j0jQ*O?;(@(2K z?U~xR&RIhd1w1U7A(%?;`fT=_&&W+{+!iY|($ZGWvb@|x%Poy+<~}l)Cl$9Ob*9x% zNH+_duL&EL%Sy8Umh+g3`K~R!N81jCjUVtBQ8&vVLZ1I8EU%R^P9(0B8YB!^_AbsQ z2NEBO@qrA0s&F;#y8PYdu+F;!pT>UkpccH9gyCl=} zubU#lkDll2m&Ww>U#`{<-R-~?(|nvw`p?0?!CG5Xxy-@B?r(>bG&B=Z7zA`fb^SgC ze)p1ax6;SrduJ0C?~*Lj8wPFOKU60o-?2ek@&nDkCJyL)00N(G7Zee`u^?D|d)ep9 zLYoRya@o6JMj`N9o6|_+wM~Lo*fXw3lbo3)0{5De=;+I0f|6`)ZbO*yL{|SQ7?Evz z#n$9dTz;El5{GlsH=2rme%{uSJt5?o@n8ux8APsJn~^{L3!Zg?suDsLEGyiRkPSf& zy6u!CS%rV-O+64^K_Yse3un=hcg4dAuOl5R>&>?G`9Ny%ef*I}vZC4dOxO zGTquFsIHFm|9b6SWrrX42|5_lREoze8e>D&&opKY z3|_X_^aheC*h%i3VsS@Fu0qLc=Qx*y_|}H@gqA`MY+z!!>gXW_e<;>E>uh($;;X|W z7J26Oo3OZKzW1zPIun_4lE2^G!kH50Fz?FTOZ;J`Bjs|}xI{{)GzT_d3aFwI+Ma6p z41%z0aW7X~I`C9FC*NZDLEqJ>L8Fq|+<6bjRJ2!yo_iKgjO%bzcs#`G$bY8Mc09(X zRJK+FgUB_*K1pn9C53@vs!JZI3!&LHzBF2iona9())UF_K4lQ_6%|Ey-sy{06;Y^$ z1{ia2RiGXUe@+9k-s|AjR507#GEI|=rR* z1(pR5W2d|r4mh@#EDQ=^Ymsso#Gb`fpOG}z{IA~O<+qTH&u8@>An$(`V&ArQmvSTS z{aB+_6#ObpLE|L^$JwSCi>47K!y-cSAwl84T*4Hy(9CfVBIli;tIDx}_UT*UR>qHK zFB4+}3j^6aiamQ5?<1nnUr4Dv0Vnp7N;pKNGVBmt9c$l~?X%Mxtqy_C6c93gSr%}9 zy77(7FT~)SQft!NhHsy~ir+*tE$tMf%v%6cT#%A7JdWDOuy0nqJ{+>j21QaQ*wC&1 z-r`}*gGytVJX2fmbf~akgK5oBZUDmgq58u33<={MC!b^wpZ>W{HRJuE;5~X2c?T|E zQ8;@Mv<9a|sfmrQT)a0~I7y>dYbh4$zvN^eJP2=_2)Kk!H zbM8QV@l>IpH!U)dX=vc(`#X|)BohIXY;;s-5Yu9H+Msar#G~>FGY2k}1CD?fGeWY{ zDdH51UvgW2svM!;HqIU^_LTAdd&uoXo9=#{ z{u#97VMe$TX2OWIPKj-OS`$rB=~qQbjF+_*3qg>=`hI=UvRNArHFO(zCN1n&SVq#< z?d`r|)~X*H-PgfhpG~N-E-CSWmc>r$${4&MA^)@{c!SJ>d80q2oDgCX6If`bCBQq|vDCNs@slX#c4u~vWRKY#8`#%sYs1fEp*QU5+dk4Yn?I~jxX!iujA7A5 zy+4IRJ(AOAbL>zJKlV=iu;}j=h{5G56(ebLg4-DEHDhd#4S6pfWwyrkyEHJ91h)Og V`gRq~qyPf8|I~FK)~Q-Y{U4=vt$qLi literal 0 HcmV?d00001 From 614e0f8f9e137a74febe1186cb4a0b3e4bbcb61c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:38 +0800 Subject: [PATCH 0613/1461] Declare the 26.1 pack formats --- minecraft-26.1/src/main/resources/pack.mcmeta | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 minecraft-26.1/src/main/resources/pack.mcmeta diff --git a/minecraft-26.1/src/main/resources/pack.mcmeta b/minecraft-26.1/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..6c0bb58d --- /dev/null +++ b/minecraft-26.1/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "${mod_name} resources", + "min_format": 84, + "max_format": 101 + } +} From 65862cbddf16238be2b903c255edbb5c486116f9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:38 +0800 Subject: [PATCH 0614/1461] Add ForgeClient.java to the Forge 26.1 glue --- .../jnhprojects/mcheli/forge/ForgeClient.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java diff --git a/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java new file mode 100644 index 00000000..fb861c5c --- /dev/null +++ b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -0,0 +1,117 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.client.CameraAngles; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.client.event.EntityRenderersEvent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.client.event.RegisterParticleProvidersEvent; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.client.event.RenderLivingEvent; +import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; + +/** Client events on Forge. Only loaded on a physical client. */ +final class ForgeClient { + private ForgeClient() { + } + + static void init(BusGroup modBus) { + ClientHooks.init(effect -> Minecraft.getInstance().gameRenderer.setPostEffect(effect)); + FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); + EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + RegisterParticleProvidersEvent.BUS.addListener(event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + RegisterKeyMappingsEvent.BUS.addListener(event -> ClientHooks.keyMappings().forEach(event::register)); + RegisterClientReloadListenersEvent.BUS.addListener(event -> ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); + AddGuiOverlayLayersEvent.BUS.addListener(event -> event.getLayeredDraw() + .add(ForgeLayeredDraw.POST_SLEEP_STACK, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + TickEvent.ClientTickEvent.Post.BUS.addListener(event -> ClientHooks.clientTickEnd()); + ViewportEvent.ComputeCameraAngles.BUS.addListener(event -> ClientHooks.computeCameraAngles(new Angles(event))); + ViewportEvent.ComputeFov.BUS.addListener(event -> event.setFOV(ClientHooks.fov(event.getFOV()))); + RenderHandEvent.BUS.addListener(ForgeClient::hideHand); + RenderLivingEvent.Pre.BUS.addListener(event -> { + ClientHooks.beforeRenderLiving(event.getState(), event.getPoseStack(), ClientHooks.cameraPartialTicks()); + return false; + }); + InputEvent.InteractionKeyMappingTriggered.BUS.addListener(ForgeClient::onInteractionKey); + ClientPlayerNetworkEvent.LoggingIn.BUS.addListener(event -> ClientHooks.loggedIn(event.getPlayer())); + ClientPlayerNetworkEvent.LoggingOut.BUS.addListener(event -> ClientHooks.loggedOut()); + } + + private static boolean hideHand(RenderHandEvent event) { + return ClientHooks.hidesHand(); + } + + private static boolean onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + return switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> false; + case CONSUME -> { + event.setSwingHand(false); + yield true; + } + case CONSUME_AND_SWING -> true; + }; + } + return event.isPickBlock() && ClientHooks.onPickBlockKey(); + } + + private record Angles(ViewportEvent.ComputeCameraAngles event) implements CameraAngles { + @Override + public Camera camera() { + return event.getCamera(); + } + + @Override + public float partialTick() { + return event.getPartialTick(); + } + + @Override + public float yaw() { + return event.getYaw(); + } + + @Override + public float pitch() { + return event.getPitch(); + } + + @Override + public float roll() { + return event.getRoll(); + } + + @Override + public void setYaw(float yaw) { + event.setYaw(yaw); + } + + @Override + public void setPitch(float pitch) { + event.setPitch(pitch); + } + + @Override + public void setRoll(float roll) { + event.setRoll(roll); + } + } +} From d803aece6a64fa78f89c59a581f152cc61c16160 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:39 +0800 Subject: [PATCH 0615/1461] Add ForgeNetwork.java to the Forge 26.1 glue --- .../mcheli/forge/ForgeNetwork.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java diff --git a/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java new file mode 100644 index 00000000..54a48e84 --- /dev/null +++ b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraftforge.network.Channel; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.payload.PayloadFlow; + +/** + * Registers the shared packets on a Forge payload channel. Each packet travels under its own id, + * as on NeoForge and Fabric; the channel only adds Forge's version check. + */ +final class ForgeNetwork { + private static Channel channel; + + private ForgeNetwork() { + } + + static void register() { + PayloadFlow flow = ChannelBuilder + .named(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "main")) + .networkProtocolVersion(ModNetwork.PROTOCOL_VERSION) + .payloadChannel() + .play() + .serverbound(); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + flow = serverbound(flow, packet); + } + flow = flow.clientbound(); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + flow = clientbound(flow, packet); + } + channel = flow.build(); + } + + static Channel channel() { + return channel; + } + + private static PayloadFlow serverbound( + PayloadFlow flow, ModNetwork.Serverbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg, ctx.getSender())); + ctx.setPacketHandled(true); + }); + } + + private static PayloadFlow clientbound( + PayloadFlow flow, ModNetwork.Clientbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg)); + ctx.setPacketHandled(true); + }); + } +} From 2a54a1987937836bdf14278eba36f402d7487916 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:39 +0800 Subject: [PATCH 0616/1461] Add ForgePlatform.java to the Forge 26.1 glue --- .../mcheli/forge/ForgePlatform.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java diff --git a/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java new file mode 100644 index 00000000..1f197a36 --- /dev/null +++ b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java @@ -0,0 +1,70 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.network.PacketDistributor; + +import java.nio.file.Path; +import java.util.Set; +import java.util.function.BiFunction; + +final class ForgePlatform implements Platform { + @Override + public String loaderName() { + return "forge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.SERVER.noArg()); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.PLAYER.with(player)); + } + + @Override + public BlockEntityType blockEntityType(BiFunction factory, Block block) { + return new BlockEntityType<>(factory::apply, Set.of(block)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From 1e6009b4f3d5263ffd99fb6e2a3a88c93a64572d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:40 +0800 Subject: [PATCH 0617/1461] Add ForgeVehicleIngredient.java to the Forge 26.1 glue --- .../mcheli/forge/ForgeVehicleIngredient.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java diff --git a/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java new file mode 100644 index 00000000..51a8d1a9 --- /dev/null +++ b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraftforge.common.crafting.ingredients.AbstractIngredient; +import net.minecraftforge.common.crafting.ingredients.IIngredientSerializer; +import org.jspecify.annotations.Nullable; + +/** {@link VehicleIngredient} as a Forge ingredient, read from the {@code type} key. */ +final class ForgeVehicleIngredient extends AbstractIngredient { + static final MapCodec CODEC = VehicleIngredient.codec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final StreamCodec STREAM_CODEC = + VehicleIngredient.streamCodec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final IIngredientSerializer SERIALIZER = new IIngredientSerializer<>() { + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + public void write(RegistryFriendlyByteBuf buffer, ForgeVehicleIngredient value) { + STREAM_CODEC.encode(buffer, value); + } + + @Override + public ForgeVehicleIngredient read(RegistryFriendlyByteBuf buffer) { + return STREAM_CODEC.decode(buffer); + } + }; + + private final String vehicle; + + private ForgeVehicleIngredient(String vehicle) { + super(VehicleIngredient.kindItems()); + this.vehicle = vehicle; + } + + String vehicle() { + return vehicle; + } + + @Override + public boolean test(@Nullable ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IIngredientSerializer serializer() { + return SERIALIZER; + } + + @Override + public boolean equals(Object o) { + return o instanceof ForgeVehicleIngredient other && vehicle.equals(other.vehicle); + } + + @Override + public int hashCode() { + return vehicle.hashCode(); + } +} From 5a02c6972cace6775e1ec166cbea42b39e63a6e1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:40 +0800 Subject: [PATCH 0618/1461] Replace the Forge 26.1 placeholder entry point with the real one --- .../jnhprojects/mcheli/forge/McheliForge.java | 71 ++++++++++++++++--- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java index 257d9d92..4028f536 100644 --- a/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java +++ b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java @@ -1,19 +1,70 @@ package com.jnhprojects.mcheli.forge; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; import net.minecraftforge.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegisterEvent; -/** - * Placeholder entry point for Minecraft 26.1. It only proves that the module builds and loads; - * the Forge glue from forge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +/** The Forge entry point: everything here only connects Forge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliForge(FMLJavaModLoadingContext context) { + BusGroup modBus = context.getModBusGroup(); + Platform.install(new ForgePlatform()); + MCHeli.init(); - public McheliForge() { - LOGGER.info("MCHeli Rework placeholder loaded on Forge for Minecraft 26.1; no content yet"); + RegisterEvent.getBus(modBus).addListener(event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(ForgeRegistries.Keys.INGREDIENT_SERIALIZERS, + helper -> helper.register(VehicleIngredient.ID, ForgeVehicleIngredient.SERIALIZER)); + }); + ForgeNetwork.register(); + FMLCommonSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(CommonHooks::commonSetup)); + + AddPackFindersEvent.BUS.addListener(event -> event.addRepositorySource(CommonHooks.packSource())); + ServerAboutToStartEvent.BUS.addListener(event -> CommonHooks.serverAboutToStart(event.getServer())); + ServerStoppedEvent.BUS.addListener(event -> CommonHooks.serverStopped()); + TickEvent.ServerTickEvent.Post.BUS.addListener(event -> CommonHooks.serverTickEnd(event.server())); + RegisterCommandsEvent.BUS.addListener(event -> CommonHooks.registerCommands(event.getDispatcher())); + PlayerEvent.PlayerLoggedInEvent.BUS.addListener(event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + PlayerEvent.PlayerLoggedOutEvent.BUS.addListener(event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + ForgeClient.init(modBus); + } + } + + /** Only once Forge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } } } From 285ef33efd8fa27d81bd4de581606893cd18cb05 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:41 +0800 Subject: [PATCH 0619/1461] Open the post effect setter on Forge 26.1 for the thermal view --- forge-26.1/src/main/resources/META-INF/accesstransformer.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 forge-26.1/src/main/resources/META-INF/accesstransformer.cfg diff --git a/forge-26.1/src/main/resources/META-INF/accesstransformer.cfg b/forge-26.1/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 00000000..df469780 --- /dev/null +++ b/forge-26.1/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,2 @@ +# Vanilla keeps the post effect setter private on 26.1; the thermal view needs it. +public net.minecraft.client.renderer.GameRenderer setPostEffect(Lnet/minecraft/resources/Identifier;)V From ae3c83da1f026d6b18c570e99b060ad2714a9c79 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:41 +0800 Subject: [PATCH 0620/1461] Drop the placeholder wording from the Forge 26.1 metadata --- forge-26.1/src/main/resources/META-INF/mods.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/forge-26.1/src/main/resources/META-INF/mods.toml b/forge-26.1/src/main/resources/META-INF/mods.toml index 47ec16fb..1fdc3b48 100644 --- a/forge-26.1/src/main/resources/META-INF/mods.toml +++ b/forge-26.1/src/main/resources/META-INF/mods.toml @@ -13,7 +13,6 @@ authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft Forge. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. From e25fc95d65cdb8235c4cdb10b3ee3c1035cf4e9f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:41 +0800 Subject: [PATCH 0621/1461] Set up the Forge 26.1 runs and apply its access transformer --- forge-26.1/build.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/forge-26.1/build.gradle b/forge-26.1/build.gradle index 372dd1f6..fe4e68cd 100644 --- a/forge-26.1/build.gradle +++ b/forge-26.1/build.gradle @@ -7,10 +7,18 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + minecraft { + useDefaultAccessTransformer() + runs { configureEach { workingDir = rootProject.layout.projectDirectory.dir("run-${minecraft_version}") + systemProperty 'eventbus.api.strictRuntimeChecks', 'true' + systemProperty 'mcheli.legacyZip', legacyZip } register('client') @@ -30,4 +38,5 @@ repositories { dependencies { implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}") + annotationProcessor "net.minecraftforge:eventbus-validator:${eventbus_version}" } From 56277d537a7cd8a3a5690359e70af63898324ecf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:42 +0800 Subject: [PATCH 0622/1461] Stop treating Forge 26.1 as a placeholder --- forge-26.1/gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-26.1/gradle.properties b/forge-26.1/gradle.properties index 27bc7407..3428f249 100644 --- a/forge-26.1/gradle.properties +++ b/forge-26.1/gradle.properties @@ -1,8 +1,8 @@ -# Minecraft 26.1 on Forge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 26.1 on Forge loader=forge minecraft_version=26.1 minecraft_version_range=[26.1,26.2) forge_version=62.0.9 loader_version_range=[62,) +eventbus_version=7.0.6 java_version=25 -scaffold=true From df5a4a2bb5a36d29972d18db78092811d32343c4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:42 +0800 Subject: [PATCH 0623/1461] Replace the NeoForge 26.1 placeholder entry point with the real one --- .../mcheli/neoforge/McheliNeoForge.java | 96 ++++++++++++++++--- 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java index fa74df62..b5bd9c45 100644 --- a/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java +++ b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java @@ -1,19 +1,93 @@ package com.jnhprojects.mcheli.neoforge; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Placeholder entry point for Minecraft 26.1. It only proves that the module builds and loads; - * the NeoForge glue from neoforge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.AddPackFindersEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import net.neoforged.neoforge.registries.RegisterEvent; + +/** The NeoForge entry point: everything here only connects NeoForge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliNeoForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliNeoForge(IEventBus modBus) { + Platform.install(new NeoForgePlatform()); + MCHeli.init(); + + modBus.addListener(RegisterEvent.class, event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(NeoForgeRegistries.Keys.INGREDIENT_TYPES, + helper -> helper.register(VehicleIngredient.ID, NeoForgeVehicleIngredient.TYPE)); + }); + modBus.addListener(RegisterPayloadHandlersEvent.class, McheliNeoForge::registerPackets); + modBus.addListener(FMLCommonSetupEvent.class, event -> event.enqueueWork(CommonHooks::commonSetup)); + modBus.addListener(AddPackFindersEvent.class, event -> event.addRepositorySource(CommonHooks.packSource())); + + NeoForge.EVENT_BUS.addListener(ServerAboutToStartEvent.class, event -> CommonHooks.serverAboutToStart(event.getServer())); + NeoForge.EVENT_BUS.addListener(ServerStoppedEvent.class, event -> CommonHooks.serverStopped()); + NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> CommonHooks.serverTickEnd(event.getServer())); + NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> CommonHooks.registerCommands(event.getDispatcher())); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedInEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedOutEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + NeoForgeClient.init(modBus); + } + } + + /** Only once NeoForge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } + } + + /** Handlers run on the main thread, which is NeoForge's default. */ + private static void registerPackets(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(Integer.toString(ModNetwork.PROTOCOL_VERSION)); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(registrar, packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(registrar, packet); + } + } + + private static void serverbound(PayloadRegistrar registrar, ModNetwork.Serverbound packet) { + registrar.playToServer(packet.type(), packet.codec(), + (msg, context) -> packet.handler().accept(msg, (ServerPlayer) context.player())); + } - public McheliNeoForge() { - LOGGER.info("MCHeli Rework placeholder loaded on NeoForge for Minecraft 26.1; no content yet"); + private static void clientbound(PayloadRegistrar registrar, ModNetwork.Clientbound packet) { + registrar.playToClient(packet.type(), packet.codec(), (msg, context) -> packet.handler().accept(msg)); } } From c266f32e5def70a595f820617266b2b61550c8d2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:43 +0800 Subject: [PATCH 0624/1461] Add NeoForgeClient.java to the NeoForge 26.1 glue --- .../mcheli/neoforge/NeoForgeClient.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java diff --git a/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java new file mode 100644 index 00000000..9c8d7d61 --- /dev/null +++ b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.neoforge.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Minecraft; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; +import net.neoforged.neoforge.client.event.InputEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; +import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent; +import net.neoforged.neoforge.client.event.RenderHandEvent; +import net.neoforged.neoforge.client.event.RenderLivingEvent; +import net.neoforged.neoforge.client.event.ViewportEvent; +import net.neoforged.neoforge.client.gui.VanillaGuiLayers; +import net.neoforged.neoforge.common.NeoForge; + +/** + * Client events on NeoForge. Only loaded on a physical client. The camera hook is a mixin + * ({@code GameRendererMixin}): NeoForge's camera event fires before vanilla positions the camera, + * so a cockpit eye set from it would be overwritten. + */ +final class NeoForgeClient { + private NeoForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$setPostEffect(effect)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + modBus.addListener(RegisterMenuScreensEvent.class, event -> ClientHooks.registerMenuScreens(event::register)); + modBus.addListener(RegisterParticleProvidersEvent.class, event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + modBus.addListener(RegisterKeyMappingsEvent.class, event -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener(AddClientReloadListenersEvent.class, event -> ClientHooks.reloadListeners().forEach(event::addListener)); + modBus.addListener(RegisterGuiLayersEvent.class, + event -> event.registerAbove(VanillaGuiLayers.SLEEP_OVERLAY, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post.class, event -> ClientHooks.clientTickEnd()); + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeFov.class, event -> event.setFOV(ClientHooks.fov(event.getFOV()))); + NeoForge.EVENT_BUS.addListener(RenderHandEvent.class, event -> event.setCanceled(ClientHooks.hidesHand())); + NeoForge.EVENT_BUS.addListener(RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getRenderState(), event.getPoseStack(), event.getPartialTick())); + NeoForge.EVENT_BUS.addListener(InputEvent.InteractionKeyMappingTriggered.class, NeoForgeClient::onInteractionKey); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingIn.class, event -> ClientHooks.loggedIn(event.getPlayer())); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingOut.class, event -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } +} From cd4e2d2ed03455c2031d1d9595d23328de2d65b6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:43 +0800 Subject: [PATCH 0625/1461] Add NeoForgePlatform.java to the NeoForge 26.1 glue --- .../mcheli/neoforge/NeoForgePlatform.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java diff --git a/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java new file mode 100644 index 00000000..920adbdb --- /dev/null +++ b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; +import net.neoforged.neoforge.network.PacketDistributor; + +import java.nio.file.Path; +import java.util.Set; +import java.util.function.BiFunction; + +final class NeoForgePlatform implements Platform { + @Override + public String loaderName() { + return "neoforge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.getDist() == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPacketDistributor.sendToServer(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + + @Override + public BlockEntityType blockEntityType(BiFunction factory, Block block) { + return new BlockEntityType<>(factory::apply, Set.of(block)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From c7956ee23c77b95dce6c589e010eaa3cef8a0de7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:44 +0800 Subject: [PATCH 0626/1461] Add NeoForgeVehicleIngredient.java to the NeoForge 26.1 glue --- .../neoforge/NeoForgeVehicleIngredient.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java diff --git a/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java new file mode 100644 index 00000000..195be00a --- /dev/null +++ b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import net.minecraft.core.Holder; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.neoforged.neoforge.common.crafting.ICustomIngredient; +import net.neoforged.neoforge.common.crafting.IngredientType; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a NeoForge ingredient, read from the {@code neoforge:ingredient_type} key. */ +record NeoForgeVehicleIngredient(String vehicle) implements ICustomIngredient { + static final IngredientType TYPE = new IngredientType<>( + VehicleIngredient.codec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle), + VehicleIngredient.streamCodec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle)); + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> items() { + return VehicleIngredient.items(); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IngredientType getType() { + return TYPE; + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } +} From a06e6cbbf635612ffd9b7bb5c2ef0aeafd071f85 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:44 +0800 Subject: [PATCH 0627/1461] Add CameraInvoker.java to the NeoForge 26.1 glue --- .../mcheli/neoforge/mixin/CameraInvoker.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java diff --git a/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java new file mode 100644 index 00000000..4c289d07 --- /dev/null +++ b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java @@ -0,0 +1,12 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** NeoForge adds a roll to {@code Camera#setRotation}, but leaves it protected. */ +@Mixin(Camera.class) +public interface CameraInvoker { + @Invoker("setRotation") + void mcheli$setRotation(float yRot, float xRot, float roll); +} From 0c0b1325aedc9aa3982324c9cf539b108eb0528c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:44 +0800 Subject: [PATCH 0628/1461] Invoke the private post effect setter on NeoForge 26.1 --- .../mcheli/neoforge/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java diff --git a/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..06a53c9a --- /dev/null +++ b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#setPostEffect} private on 26.1. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("setPostEffect") + void mcheli$setPostEffect(Identifier effect); +} From 682b8bbf4444dc28f374c043ce4f0402ac080c1f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:45 +0800 Subject: [PATCH 0629/1461] Add the NeoForge camera hook for 26.1, whose update takes a second argument --- .../neoforge/mixin/GameRendererMixin.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java diff --git a/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java new file mode 100644 index 00000000..073f2c37 --- /dev/null +++ b/neoforge-26.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java @@ -0,0 +1,39 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, at the two places Forge fires its camera angle event: once vanilla has placed + * the camera for the frame, and again before the camera is copied into the render state. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;update(Lnet/minecraft/client/DeltaTracker;)V", + shift = At.Shift.AFTER)) + private void mcheli$afterCameraUpdate(DeltaTracker deltaTracker, boolean advanceGameTime, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, deltaTracker.getGameTimeDeltaPartialTick(true), GameRendererMixin::setRotation); + } + + @Inject(method = "extractCamera", at = @At("HEAD")) + private void mcheli$beforeExtractCamera(DeltaTracker deltaTracker, float worldPartialTicks, float cameraEntityPartialTicks, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, worldPartialTicks, GameRendererMixin::setRotation); + } + + /** NeoForge turns the camera the other way round its roll axis than Forge does. */ + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((CameraInvoker) camera).mcheli$setRotation(yaw, pitch, -roll); + } +} From 5456f0a2e97adbf0555887fd984fd13ee34d82c3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:45 +0800 Subject: [PATCH 0630/1461] Drop the placeholder wording from the NeoForge 26.1 metadata --- .../src/main/resources/META-INF/neoforge.mods.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/neoforge-26.1/src/main/resources/META-INF/neoforge.mods.toml b/neoforge-26.1/src/main/resources/META-INF/neoforge.mods.toml index 2585f0b9..c8095914 100644 --- a/neoforge-26.1/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge-26.1/src/main/resources/META-INF/neoforge.mods.toml @@ -8,17 +8,19 @@ modId="${mod_id}" version="${mod_version}" displayName="${mod_name}" displayURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge" -logoFile="mcheli_logo.png" +iconFile="mcheli_logo.png" authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. ''' +[[mixins]] +config="mcheli.mixins.json" + [[dependencies.${mod_id}]] modId="neoforge" type="required" From 1d1b99e1188a3756fe40f58581c3547753923754 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:46 +0800 Subject: [PATCH 0631/1461] List the NeoForge 26.1 mixins --- .../src/main/resources/mcheli.mixins.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 neoforge-26.1/src/main/resources/mcheli.mixins.json diff --git a/neoforge-26.1/src/main/resources/mcheli.mixins.json b/neoforge-26.1/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..77522ff3 --- /dev/null +++ b/neoforge-26.1/src/main/resources/mcheli.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.neoforge.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "CameraInvoker", + "GameRendererInvoker", + "GameRendererMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From 425a4a32cb888f1c12ffd7780d069eeee6f47ad7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:46 +0800 Subject: [PATCH 0632/1461] Set up the NeoForge 26.1 runs --- neoforge-26.1/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neoforge-26.1/build.gradle b/neoforge-26.1/build.gradle index 6dded114..60829c34 100644 --- a/neoforge-26.1/build.gradle +++ b/neoforge-26.1/build.gradle @@ -7,12 +7,17 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + neoForge { version = neoforge_version runs { configureEach { gameDirectory = rootProject.file("run-${minecraft_version}") + systemProperty 'mcheli.legacyZip', legacyZip } client { client() From 32032cca27cae6f32ecacde92e0e20bf37e3ee60 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:47 +0800 Subject: [PATCH 0633/1461] Stop treating NeoForge 26.1 as a placeholder and use the jspecify it pins --- neoforge-26.1/gradle.properties | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neoforge-26.1/gradle.properties b/neoforge-26.1/gradle.properties index 265dfcf9..cbf34ea0 100644 --- a/neoforge-26.1/gradle.properties +++ b/neoforge-26.1/gradle.properties @@ -1,8 +1,9 @@ -# Minecraft 26.1 on NeoForge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 26.1 on NeoForge loader=neoforge minecraft_version=26.1 minecraft_version_range=[26.1,26.2) neoforge_version=26.1.2.126 loader_version_range=[26.1,) java_version=25 -scaffold=true +# NeoForge 26.1 pins this version strictly. +jspecify_version=1.0.0 From 8a479175dd3a276c0c11242f15b6f48bf84b4c74 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:47 +0800 Subject: [PATCH 0634/1461] Build block entity types with Fabric API on 26.1 --- .../mcheli/fabric/FabricPlatform.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java new file mode 100644 index 00000000..58861da2 --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.creativetab.v1.FabricCreativeModeTab; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.nio.file.Path; +import java.util.function.BiFunction; + +final class FabricPlatform implements Platform { + @Override + public String loaderName() { + return "fabric"; + } + + @Override + public Path gameDir() { + return FabricLoader.getInstance().getGameDir(); + } + + @Override + public Path configDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + public String modVersion() { + return FabricLoader.getInstance().getModContainer(MCHeli.MOD_ID) + .map(c -> c.getMetadata().getVersion().getFriendlyString()).orElse(""); + } + + @Override + public boolean isClient() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPlayNetworking.send(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ServerPlayNetworking.send(player, payload); + } + + /** Fabric orders tabs by registration, which already puts ours one after another. */ + @Override + public BlockEntityType blockEntityType(BiFunction factory, Block block) { + return FabricBlockEntityTypeBuilder.create(factory::apply, block).build(); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return FabricCreativeModeTab.builder(); + } +} From 2e504a55f1312b1bdc89a64e7022c9f3bdb09cd5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:48 +0800 Subject: [PATCH 0635/1461] Add FabricVehicleIngredient.java to the Fabric 26.1 glue --- .../fabric/FabricVehicleIngredient.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java new file mode 100644 index 00000000..ecc8b270 --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Holder; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a Fabric ingredient, read from the {@code fabric:type} key. */ +record FabricVehicleIngredient(String vehicle) implements CustomIngredient { + static final CustomIngredientSerializer SERIALIZER = new CustomIngredientSerializer<>() { + private final MapCodec codec = + VehicleIngredient.codec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + private final StreamCodec streamCodec = + VehicleIngredient.streamCodec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + + @Override + public Identifier getIdentifier() { + return VehicleIngredient.ID; + } + + @Override + public MapCodec getCodec() { + return codec; + } + + @Override + public StreamCodec getStreamCodec() { + return streamCodec; + } + }; + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> items() { + return VehicleIngredient.items(); + } + + @Override + public boolean requiresTesting() { + return true; + } + + @Override + public CustomIngredientSerializer getSerializer() { + return SERIALIZER; + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } +} From 71c88e3bdd425ee500bf1072261d31b21d036658 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:48 +0800 Subject: [PATCH 0636/1461] Replace the Fabric 26.1 placeholder entry point with the real one --- .../mcheli/fabric/McheliFabric.java | 69 +++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java index 4547c6a5..d7503501 100644 --- a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java @@ -1,19 +1,74 @@ package com.jnhprojects.mcheli.fabric; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; import net.fabricmc.api.ModInitializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; /** - * Placeholder entry point for Minecraft 26.1. It only proves that the module builds and loads; - * the Fabric glue from fabric-26.3 replaces it when this version is ported for a final release. + * The Fabric entry point: everything here only connects Fabric to the shared code. The imported + * content pack is added by {@code PackRepositoryMixin}, since Fabric has no pack finder event. */ public final class McheliFabric implements ModInitializer { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); - @Override public void onInitialize() { - LOGGER.info("MCHeli Rework placeholder loaded on Fabric for Minecraft 26.1; no content yet"); + Platform.install(new FabricPlatform()); + MCHeli.init(); + + for (ResourceKey> registry : Registration.registries()) { + register(Registration.typed(registry)); + } + CustomIngredientSerializer.register(FabricVehicleIngredient.SERIALIZER); + registerPackets(); + CommonHooks.commonSetup(); + + ServerLifecycleEvents.SERVER_STARTING.register(CommonHooks::serverAboutToStart); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> CommonHooks.serverStopped()); + ServerTickEvents.END_SERVER_TICK.register(CommonHooks::serverTickEnd); + CommandRegistrationCallback.EVENT.register((dispatcher, context, selection) -> CommonHooks.registerCommands(dispatcher)); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> CommonHooks.playerLoggedIn(handler.getPlayer())); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> CommonHooks.playerLoggedOut(handler.getPlayer())); + } + + @SuppressWarnings("unchecked") + private static void register(ResourceKey> key) { + Registry registry = (Registry) BuiltInRegistries.REGISTRY.getValue(key.identifier()); + if (registry == null) { + throw new IllegalStateException("No registry " + key.identifier()); + } + Registration.registerAll(key, (id, value) -> Registry.register(registry, id, value)); + } + + /** Client handlers are registered by {@link McheliFabricClient}; handlers run on the main thread. */ + private static void registerPackets() { + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + } + + private static void clientbound(ModNetwork.Clientbound packet) { + PayloadTypeRegistry.clientboundPlay().register(packet.type(), packet.codec()); + } + + private static void serverbound(ModNetwork.Serverbound packet) { + PayloadTypeRegistry.serverboundPlay().register(packet.type(), packet.codec()); + ServerPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg, context.player())); } } From 443cd35257f37c583005ff344a71166f25843e58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:48 +0800 Subject: [PATCH 0637/1461] Add McheliFabricClient.java to the Fabric 26.1 glue --- .../mcheli/fabric/McheliFabricClient.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java new file mode 100644 index 00000000..edaeb0b4 --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java @@ -0,0 +1,56 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.fabric.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.particle.v1.ParticleProviderRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.packs.PackType; + +/** + * Client side of the Fabric entry point. The camera, hand, key and rider hooks are mixins (see + * {@code com.jnhprojects.mcheli.fabric.mixin}), since Fabric API has no events for them. + */ +public final class McheliFabricClient implements ClientModInitializer { + + // EntityRendererRegistry is deprecated in favour of a vanilla method that is not public yet. + @SuppressWarnings("deprecation") + @Override + public void onInitializeClient() { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$setPostEffect(effect)); + ClientHooks.registerEntityRenderers(EntityRendererRegistry::register); + ClientHooks.registerBlockEntityRenderers(BlockEntityRenderers::register); + ClientHooks.registerMenuScreens(MenuScreens::register); + ParticleProviderRegistry.getInstance().register(ModParticles.GLOW.get(), GlowParticle.Provider::new); + ParticleProviderRegistry.getInstance().register(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + ClientHooks.keyMappings().forEach(KeyMappingHelper::registerKeyMapping); + ClientHooks.reloadListeners().forEach(ResourceLoader.get(PackType.CLIENT_RESOURCES)::registerReloadListener); + HudElementRegistry.attachElementAfter(VanillaHudElements.SLEEP, ClientHooks.HUD_LAYER, ClientHooks::drawHud); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + + ClientTickEvents.END_CLIENT_TICK.register(client -> ClientHooks.clientTickEnd()); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> ClientHooks.loggedIn(client.player)); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ClientHooks.loggedOut()); + } + + private static void clientbound(ModNetwork.Clientbound packet) { + ClientPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg)); + } +} From a031b36023e14cdeae9ba72ba5778d6eb27683b9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:49 +0800 Subject: [PATCH 0638/1461] Add RollableCamera.java to the Fabric 26.1 glue --- .../java/com/jnhprojects/mcheli/fabric/RollableCamera.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java new file mode 100644 index 00000000..ff30f0ff --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java @@ -0,0 +1,7 @@ +package com.jnhprojects.mcheli.fabric; + +/** Implemented on {@code Camera} by {@code CameraMixin}. Outside the mixin package, which Mixin refuses to load classes from. */ +public interface RollableCamera { + /** Like vanilla's {@code setRotation}, with a roll in degrees as Forge applies it. */ + void mcheli$setRotation(float yRot, float xRot, float roll); +} From 6ea6a5aef889b2a1cbd7d28e702b9d3884f390bc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:49 +0800 Subject: [PATCH 0639/1461] Add the Fabric camera roll mixin for 26.1, whose vectors are not final interfaces yet --- .../mcheli/fabric/mixin/CameraMixin.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java new file mode 100644 index 00000000..1e8a57a7 --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -0,0 +1,67 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Vanilla's camera has no roll. This is {@code Camera#setRotation} with the roll Forge patches in, + * so a banked aircraft tilts the view the same on every loader. + */ +@Mixin(Camera.class) +abstract class CameraMixin implements RollableCamera { + @Shadow + @Final + private static Vector3f FORWARDS; + @Shadow + @Final + private static Vector3f UP; + @Shadow + @Final + private static Vector3f LEFT; + + @Shadow + private float xRot; + @Shadow + private float yRot; + @Shadow + @Final + private Vector3f forwards; + @Shadow + @Final + private Vector3f up; + @Shadow + @Final + private Vector3f left; + @Shadow + @Final + private Quaternionf rotation; + @Shadow + private int matrixPropertiesDirty; + + @Override + public void mcheli$setRotation(float yRot, float xRot, float roll) { + this.xRot = xRot; + this.yRot = yRot; + this.rotation.rotationYXZ((float) Math.PI - yRot * (float) (Math.PI / 180.0), -xRot * (float) (Math.PI / 180.0), + roll * (float) (Math.PI / 180.0)); + FORWARDS.rotate(this.rotation, this.forwards); + UP.rotate(this.rotation, this.up); + LEFT.rotate(this.rotation, this.left); + this.matrixPropertiesDirty |= 3; + } + + /** Zoom through a sight, where Forge fires its field of view event. */ + @Inject(method = "calculateFov", at = @At("RETURN"), cancellable = true) + private void mcheli$calculateFov(float partialTicks, CallbackInfoReturnable cir) { + cir.setReturnValue(ClientHooks.fov(cir.getReturnValueF())); + } +} From cb298aacf31988c0d5fdfd287b195b8b4d13cd42 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:50 +0800 Subject: [PATCH 0640/1461] Invoke the private post effect setter on Fabric 26.1 --- .../mcheli/fabric/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..b7afee17 --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#setPostEffect} private on 26.1. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("setPostEffect") + void mcheli$setPostEffect(Identifier effect); +} From 3155c3d40ac2a050eb7e0d0524255d6f1e7dbfa2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:50 +0800 Subject: [PATCH 0641/1461] Add the Fabric camera and hand mixin for 26.1, whose update takes a second argument --- .../fabric/mixin/GameRendererMixin.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java new file mode 100644 index 00000000..ac10d9b0 --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, at the two places Forge fires its camera angle event (once vanilla has placed + * the camera for the frame, and again before the camera is copied into the render state), and + * the first person hand, which is hidden in some seats. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;update(Lnet/minecraft/client/DeltaTracker;)V", + shift = At.Shift.AFTER)) + private void mcheli$afterCameraUpdate(DeltaTracker deltaTracker, boolean advanceGameTime, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, deltaTracker.getGameTimeDeltaPartialTick(true), GameRendererMixin::setRotation); + } + + @Inject(method = "extractCamera", at = @At("HEAD")) + private void mcheli$beforeExtractCamera(DeltaTracker deltaTracker, float worldPartialTicks, float cameraEntityPartialTicks, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, worldPartialTicks, GameRendererMixin::setRotation); + } + + @Inject(method = "renderItemInHand", at = @At("HEAD"), cancellable = true) + private void mcheli$renderItemInHand(CallbackInfo ci) { + if (ClientHooks.hidesHand()) { + ci.cancel(); + } + } + + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((RollableCamera) camera).mcheli$setRotation(yaw, pitch, roll); + } +} From 8d56609aad0d78bac93ff2825159c56979887275 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:51 +0800 Subject: [PATCH 0642/1461] Add LivingEntityRendererMixin.java to the Fabric 26.1 glue --- .../mixin/LivingEntityRendererMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java new file mode 100644 index 00000000..524f5481 --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; +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.callback.CallbackInfo; + +/** Tilts riders with their vehicle, where Forge fires its living entity render event. */ +@Mixin(LivingEntityRenderer.class) +abstract class LivingEntityRendererMixin { + + @Inject(method = "submit(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;" + + "Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/level/CameraRenderState;)V", + at = @At("HEAD")) + private void mcheli$beforeSubmit(LivingEntityRenderState state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera, + CallbackInfo ci) { + ClientHooks.beforeRenderLiving(state, pose, ClientHooks.cameraPartialTicks()); + } +} From 7d58a6f9516e833f9655715ce189ee2d6017f0b5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:51 +0800 Subject: [PATCH 0643/1461] Add MinecraftMixin.java to the Fabric 26.1 glue --- .../mcheli/fabric/mixin/MinecraftMixin.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java new file mode 100644 index 00000000..b155db7c --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.HitResult; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** The use and pick block keys, which Forge and NeoForge let mods take through an input event. */ +@Mixin(Minecraft.class) +abstract class MinecraftMixin { + @Shadow + public @Nullable LocalPlayer player; + @Shadow + public @Nullable HitResult hitResult; + @Shadow + private int rightClickDelay; + + @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) + private void mcheli$startUseItem(CallbackInfo ci) { + if (player == null) { + return; + } + ClientHooks.UseKey result = ClientHooks.onUseKey(InteractionHand.MAIN_HAND); + if (result != ClientHooks.UseKey.PASS) { + // Vanilla sets the same delay before it looks at the hands. + rightClickDelay = 4; + if (result == ClientHooks.UseKey.CONSUME_AND_SWING) { + player.swing(InteractionHand.MAIN_HAND); + } + ci.cancel(); + } + } + + @Inject(method = "pickBlockOrEntity", at = @At("HEAD"), cancellable = true) + private void mcheli$pickBlockOrEntity(CallbackInfo ci) { + if (hitResult != null && hitResult.getType() != HitResult.Type.MISS && ClientHooks.onPickBlockKey()) { + ci.cancel(); + } + } +} From 1825d99700336dd7a00fc99e771a8e4cc3a870c4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:51 +0800 Subject: [PATCH 0644/1461] Add PackRepositoryMixin.java to the Fabric 26.1 glue --- .../fabric/mixin/PackRepositoryMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java new file mode 100644 index 00000000..965719b9 --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.CommonHooks; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.Arrays; + +/** + * Adds the imported original content to every pack repository, client resources and server data + * alike, which is what Forge and NeoForge do through their pack finder event. + */ +@Mixin(PackRepository.class) +abstract class PackRepositoryMixin { + + @ModifyVariable(method = "", at = @At("HEAD"), argsOnly = true) + private static RepositorySource[] mcheli$addImportedContent(RepositorySource[] sources) { + RepositorySource[] withOurs = Arrays.copyOf(sources, sources.length + 1); + withOurs[sources.length] = CommonHooks.packSource(); + return withOurs; + } +} From 69d1ddba2cf392ce27596f6dac1b88faf7ac269d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:52 +0800 Subject: [PATCH 0645/1461] Add RuntimeSoundInstanceMixin.java to the Fabric 26.1 glue --- .../mixin/RuntimeSoundInstanceMixin.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java diff --git a/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java new file mode 100644 index 00000000..f085c684 --- /dev/null +++ b/fabric-26.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java @@ -0,0 +1,20 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.concurrent.CompletableFuture; + +/** Streams content pack sounds from disk through Fabric's sound instance hook. */ +@Mixin(RuntimeSoundInstance.class) +abstract class RuntimeSoundInstanceMixin implements FabricSoundInstance { + + @Override + public CompletableFuture getAudioStream(SoundBufferLibrary loader, Identifier id, boolean repeatInstantly) { + return ((RuntimeSoundInstance) (Object) this).openStream(loader, id, repeatInstantly); + } +} From 8169bedd862814b76397166c15787ce2d097229c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:52 +0800 Subject: [PATCH 0646/1461] Drop the placeholder wording from the Fabric 26.1 metadata --- fabric-26.1/src/main/resources/fabric.mod.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fabric-26.1/src/main/resources/fabric.mod.json b/fabric-26.1/src/main/resources/fabric.mod.json index 1b87b793..8346f4c2 100644 --- a/fabric-26.1/src/main/resources/fabric.mod.json +++ b/fabric-26.1/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "${mod_id}", "version": "${mod_version}", "name": "${mod_name}", - "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", + "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", "authors": ["JayMar921 (rework)", "EMB4 (original author)", "Fureniku (CurseForge maintainer)"], "contact": { "homepage": "https://github.com/JnH-Projects/MCHeli-Rework-Forge", @@ -14,8 +14,10 @@ "icon": "mcheli_logo.png", "environment": "*", "entrypoints": { - "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"] + "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"], + "client": ["com.jnhprojects.mcheli.fabric.McheliFabricClient"] }, + "mixins": ["mcheli.mixins.json"], "depends": { "fabricloader": "${loader_version_range}", "fabric-api": "*", From 596547d1c955fd967b686ca64a199cccc7d1ed76 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:53 +0800 Subject: [PATCH 0647/1461] List the Fabric 26.1 mixins --- .../src/main/resources/mcheli.mixins.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-26.1/src/main/resources/mcheli.mixins.json diff --git a/fabric-26.1/src/main/resources/mcheli.mixins.json b/fabric-26.1/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..3dcbaf14 --- /dev/null +++ b/fabric-26.1/src/main/resources/mcheli.mixins.json @@ -0,0 +1,20 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "PackRepositoryMixin" + ], + "client": [ + "CameraMixin", + "GameRendererInvoker", + "GameRendererMixin", + "LivingEntityRendererMixin", + "MinecraftMixin", + "RuntimeSoundInstanceMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From c79f9b19c803cce9262e14c233f87f0b7238e492 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:53 +0800 Subject: [PATCH 0648/1461] Set up the Fabric 26.1 runs --- fabric-26.1/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fabric-26.1/build.gradle b/fabric-26.1/build.gradle index dcc4eded..c4cfd3fb 100644 --- a/fabric-26.1/build.gradle +++ b/fabric-26.1/build.gradle @@ -7,10 +7,15 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + loom { runs { configureEach { runDir = "../run-${minecraft_version}" + vmArg "-Dmcheli.legacyZip=${legacyZip}" } server { programArgs '--nogui' From d78e1a820b7fac7faf282e726191ad4427a1bc69 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:54 +0800 Subject: [PATCH 0649/1461] Stop treating Fabric 26.1 as a placeholder --- fabric-26.1/gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fabric-26.1/gradle.properties b/fabric-26.1/gradle.properties index 037b1476..f35e4967 100644 --- a/fabric-26.1/gradle.properties +++ b/fabric-26.1/gradle.properties @@ -1,4 +1,4 @@ -# Minecraft 26.1 on Fabric (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 26.1 on Fabric loader=fabric minecraft_version=26.1 minecraft_version_range=~26.1 @@ -6,4 +6,3 @@ fabric_loader_version=0.20.1 loader_version_range=>=0.20.0 fabric_api_version=0.145.1+26.1 java_version=25 -scaffold=true From 0f258140fa11bee8481361692b39ea223292a8f7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:54 +0800 Subject: [PATCH 0650/1461] Build the 26.1 modules --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9b5034fb..e6390ea9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mod_license=MCHeli Rework Licence 1.0 (see LICENSE) # Platform modules built by this project, one per Minecraft version. Each has its own # gradle.properties with the Minecraft, loader and Java versions. -platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-26.2,neoforge-26.2,fabric-26.2,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 +platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-26.2,neoforge-26.2,fabric-26.2,forge-26.1,neoforge-26.1,fabric-26.1,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 # Libraries the common module compiles against. Every supported Minecraft version ships them. slf4j_version=2.0.17 From ecd8574171a0f257230167b432193ee870845e5f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:54 +0800 Subject: [PATCH 0651/1461] Note the 26.1 port in the changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73e8903c..c6aec401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,12 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). the vehicle screen, the drafting table, team manager, UAV stations and the workshop. On 1.21.1 the PVP button of the team manager switches the server's PVP setting until a restart, since that version has no game rule for it, and target markers ignore view bobbing. -- Minecraft 26.2 on Forge, NeoForge and Fabric, with everything 26.3 has. +- Minecraft 26.2 and 26.1 on Forge, NeoForge and Fabric, with everything 26.3 has. On 26.1 teams pick + from the sixteen chat colours, since that version has no separate team colours. ### Changed -- Forge, NeoForge and Fabric on 26.3, 26.2 and 1.21.1 are all built on every pull request. +- Forge, NeoForge and Fabric on 26.3, 26.2, 26.1 and 1.21.1 are all built on every pull request. - Clients and servers on different loaders cannot play together, which the roadmap used to promise. Each loader refuses the other at its own handshake, before the mod runs. A pack zip still works on every loader when copied by hand. From fee2b9bb0f2ce26d5719018d53c5950628e06c94 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:55 +0800 Subject: [PATCH 0652/1461] List Minecraft 26.1 in the README --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ae64be5e..157fa823 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ MCHeli Rework icon -Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3**, **26.2** and **1.21.1**, on -**Forge**, **NeoForge** and **Fabric**. +Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3**, **26.2**, **26.1** and +**1.21.1**, on **Forge**, **NeoForge** and **Fabric**. MCHeli Rework is a ground-up port of MCHeli 1.0.3, a mod that stopped at Minecraft 1.7.10. Every system is rewritten for the modern game, while the original vehicle definitions, models, @@ -84,16 +84,18 @@ Reserved original, the code is source available rather than open source. ## Requirements -| | Minecraft 26.3 | Minecraft 26.2 | Minecraft 1.21.1 | -|---|---|---|---| -| Forge | 66.1.0 or newer in the 66.x line | 65.1.5 or newer in the 65.x line | 52.1.0 or newer | -| NeoForge | 26.3.0.99 or newer | 26.2.0.93 or newer | 21.1.200 or newer | -| Fabric | Loader 0.20 and Fabric API | Loader 0.20 and Fabric API | Loader 0.16 and Fabric API | -| Java | 25 (bundled with the launcher) | 25 (bundled with the launcher) | 21 (bundled with the launcher) | +| Minecraft | Forge | NeoForge | Fabric | Java | +|---|---|---|---|---| +| 26.3 | 66.1.0 or newer in 66.x | 26.3.0.99 or newer | Loader 0.20 and Fabric API | 25 | +| 26.2 | 65.1.5 or newer in 65.x | 26.2.0.93 or newer | Loader 0.20 and Fabric API | 25 | +| 26.1 | 62.0.9 or newer in 62.x | 26.1.2.126 or newer | Loader 0.20 and Fabric API | 25 | +| 1.21.1 | 52.1.0 or newer | 21.1.200 or newer | Loader 0.16 and Fabric API | 21 | + +The launcher bundles the right Java for each version. ## Installing -1. Install Forge, NeoForge or Fabric for Minecraft 26.3, 26.2 or 1.21.1. On Fabric, also install +1. Install Forge, NeoForge or Fabric for Minecraft 26.3, 26.2, 26.1 or 1.21.1. On Fabric, also install [Fabric API](https://modrinth.com/mod/fabric-api). 2. Download the jar for your loader from the [releases](https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases): From 87a44ba1a2aed6ef0d5368427b0b768eaa50de33 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:55 +0800 Subject: [PATCH 0653/1461] Tick the 26.1 port on the roadmap --- docs/ROADMAP.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8fb5c6fb..ac9fe2b0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -309,9 +309,10 @@ added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest - [x] Every resource the repository holds ships in the jar (betas 4 to 8 lacked recipes, the thermal shader, the smoke particle and the drafting table model) - [x] Phases 8 to 12 ported to all three loaders on 1.21.1 -- [ ] Every module in `platforms` (26.3, 26.2 and 1.21.1 are in) +- [ ] Every module in `platforms` (26.3, 26.2, 26.1 and 1.21.1 are in) - [x] `minecraft-26.2` with its Forge, NeoForge and Fabric glue -- [ ] `minecraft-26.1`, `minecraft-1.21.11`, `minecraft-1.21.5`, +- [x] `minecraft-26.1` with its Forge, NeoForge and Fabric glue +- [ ] `minecraft-1.21.11`, `minecraft-1.21.5`, `minecraft-1.20.6`, `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its placeholder modules with real glue - [ ] `common` compiles for Java 17 (no pattern matching `switch`, no `SequencedCollection` From 2803a345d3356f85cafdbed1f9a448731b82f2df Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:56 +0800 Subject: [PATCH 0654/1461] Describe what differs on 26.1 --- docs/ARCHITECTURE.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 51c63b8c..3b5ce567 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -51,7 +51,7 @@ loader. The top layer is a thin module per loader and Minecraft version, which c lower layers together with its own glue into one jar. See [DEVELOPMENT.md](DEVELOPMENT.md#three-layers). -Only 26.3, 26.2 and 1.21.1 have the middle layer so far. The other supported versions have placeholder +Only 26.3, 26.2, 26.1 and 1.21.1 have the middle layer so far. The other supported versions have placeholder loader modules (`scaffold=true`) that compile neither lower layer: just an entry point that logs, against that version's loader and toolchain. Porting a version means adding its `minecraft-` folder, copying the loader glue in and dropping the flag. @@ -103,6 +103,22 @@ NeoForge rolls the camera the other way round from Forge, so its mixin negates t - **Rider pose.** `CameraRenderState` does not carry the camera entity's partial tick, so Forge and Fabric ask `ClientHooks.cameraPartialTicks`. NeoForge's event has it. +### Minecraft 26.1 + +`minecraft-26.1` is `minecraft-26.2` with these differences: + +- **Depth.** 26.1 is the last version with a normal depth buffer, so the glow pipelines test with + `LESS_THAN_OR_EQUAL`. The enum exists on both sides of the change, so the wrong one compiles and + only shows as glows drawn through walls. +- **Rendering API.** Pipelines take samplers and a vertex format through `withSampler` and + `withVertexFormat` rather than bind groups and a primitive topology. +- **Screens.** They live on `Minecraft`, not `Gui`, as they do on 1.21.1. +- **Teams.** There is no `TeamColor`. The team manager picks from the `ChatFormatting` colours. +- **Block entity types.** The constructor is private in vanilla, so `Platform.blockEntityType` makes + them: Forge and NeoForge have opened the constructor, Fabric API has a builder. +- **Camera hooks.** `GameRenderer.update` also takes whether game time advances, and Fabric's + camera mixin shadows `Vector3f` constants rather than `Vector3fc`. + ### Minecraft 1.21.1 `minecraft-1.21.1` is a copy of `minecraft-26.3` ported back, so the two read side by side. During From a3762439c627e1af2d0e00fbb593b64051b1cf43 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:56 +0800 Subject: [PATCH 0655/1461] List the 26.1 modules --- docs/DEVELOPMENT.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 481cf89b..90746ae8 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -15,6 +15,9 @@ | Forge | 65.1.5 for Minecraft 26.2 | | | NeoForge | 26.2.0.93 for Minecraft 26.2 | | | Fabric | Loader 0.20.1, Fabric API 0.161.0+26.2 | | +| Forge | 62.0.9 for Minecraft 26.1 | | +| NeoForge | 26.1.2.126 for Minecraft 26.1 | | +| Fabric | Loader 0.20.1, Fabric API 0.145.1+26.1 | | | Forge | 52.1.20 for Minecraft 1.21.1 | Built with ForgeGradle 7 and the official mappings. | | NeoForge | 21.1.265 for Minecraft 1.21.1 | | | Fabric | Fabric API 0.116.17+1.21.1 | Loom remaps the jar, which is obfuscated on 1.21.1. | @@ -44,7 +47,7 @@ Shared properties live in the root `gradle.properties`: `mod_version`, the mod m of loader modules (`platforms`) and the library versions `common` compiles against. Each loader module has its own `gradle.properties` with its loader, Minecraft and Java versions. During the betas up to beta.8, `platforms` listed only `forge-26.3`. Roadmap phase 13 adds modules back as each -one catches up, currently the three each on 26.3, 26.2 and 1.21.1; the others stay in the repository +one catches up, currently the three each on 26.3, 26.2, 26.1 and 1.21.1; the others stay in the repository until they are ported. `mods.toml`, `neoforge.mods.toml`, `fabric.mod.json` and `pack.mcmeta` are filled in from them at build time. What every loader module shares (source folders, jar contents, manifest, file @@ -65,6 +68,8 @@ neoforge-26.3/ NeoForge glue, plus a camera mixin fabric-26.3/ Fabric glue, plus mixins for what Fabric API has no event for minecraft-26.2/ the same shared code on Minecraft 26.2 forge-26.2/, neoforge-26.2/, fabric-26.2/ the loader glue for 26.2 +minecraft-26.1/ the same shared code on Minecraft 26.1 +forge-26.1/, neoforge-26.1/, fabric-26.1/ the loader glue for 26.1 minecraft-1.21.1/ the same shared code, backported to Minecraft 1.21.1 forge-1.21.1/, neoforge-1.21.1/, fabric-1.21.1/ the loader glue for 1.21.1 -/ placeholders for the other supported versions (see below) @@ -100,7 +105,7 @@ so each loader and version ships as one jar and runs as one module in developmen A feature that is pure logic goes in `common` with a unit test; the part that touches Minecraft goes in `minecraft-`; only what needs a loader API goes in the loader modules. The run folder `./run` is shared by every 26.3 module, and every other version has its own, such as -`./run-26.2` and `./run-1.21.1`; worlds made by one loader open on the others, but never open a world +`./run-26.1` and `./run-1.21.1`; worlds made by one loader open on the others, but never open a world on an older Minecraft version than the one that last saved it. IDEs import the project as one Gradle project per module. A file under `minecraft-26.3` belongs @@ -110,8 +115,8 @@ of them on the command line. After pulling the split, re-import the Gradle proje ### Placeholder modules -Every [supported version](ROADMAP.md#supported-versions) has a loader module, but only 26.3, 26.2 -and 1.21.1 are ported. The other 16 (`forge-`, `neoforge-` and `fabric-` for 26.1, 1.21.11, +Every [supported version](ROADMAP.md#supported-versions) has a loader module, but only 26.3, 26.2, +26.1 and 1.21.1 are ported. The other 13 (`forge-`, `neoforge-` and `fabric-` for 1.21.11, 1.21.5 and 1.20.6, plus `forge-` and `fabric-` for 1.20.2 and 1.19.4) are placeholders: each pins its loader, mappings and Java version, and holds only an entry point that logs a line at startup and the mod metadata. Their `gradle.properties` sets `scaffold=true`, which tells @@ -134,7 +139,8 @@ A clean compile proves little about a port. Mixin targets, the Forge access tran data files only fail at runtime, so start a client and a dedicated server on every loader of that version, and check a built jar. A loader can also pin a library more strictly than the version the root `gradle.properties` asks for; override it in that module's `gradle.properties`, as NeoForge 26.2 -does with `jspecify_version`. +and 26.1 do with `jspecify_version`. A first run in a new run folder stops at vanilla's accessibility +screen instead of the title screen, which matters only to anything that automates a client. There is no `neoforge-1.20.2`: ModDevGradle needs a "moddev bundle" that NeoForge 20.2 does not publish, so NeoForge support starts at 1.20.6, the next supported version. From 188aab2476648073bb0ef45a73fc50cce4a1593d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:03:57 +0800 Subject: [PATCH 0656/1461] Add 26.1 checks for glow depth and the Fabric block entity builder --- docs/TESTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/TESTING.md b/docs/TESTING.md index daa84264..c50b0e03 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -48,6 +48,14 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi another loader is refused by the loaders themselves, with a message naming the one it needs. - [ ] A `serverconfig/mcheli-server.toml` written by 1.1.0-beta.2 (Forge) keeps its values. +### 26.1 only + +- [ ] From the built jars on a launcher profile: thermal on and off on all three loaders, and the + drafting table places and draws on Fabric (its block entity type comes from Fabric API). +- [ ] Tracers and flares are hidden by a block between them and the camera, and show in front of one. +- [ ] Rider pose and the camera roll look right in a banked aircraft on all three loaders. +- [ ] The team manager lists the chat colours, and a new team takes the one picked. + ### 26.2 only - [ ] From the built jars on a launcher profile, not a dev run: the thermal view turns on and off on From 7ddd18ac5c9494a9da677042d13612096908b928 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:29 +0800 Subject: [PATCH 0657/1461] Define c:gunpowders on Forge 1.21.1, which only has forge:gunpowder --- .../src/main/resources/data/c/tags/item/gunpowders.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 forge-1.21.1/src/main/resources/data/c/tags/item/gunpowders.json diff --git a/forge-1.21.1/src/main/resources/data/c/tags/item/gunpowders.json b/forge-1.21.1/src/main/resources/data/c/tags/item/gunpowders.json new file mode 100644 index 00000000..16c21708 --- /dev/null +++ b/forge-1.21.1/src/main/resources/data/c/tags/item/gunpowders.json @@ -0,0 +1,5 @@ +{ + "values": [ + "#forge:gunpowder" + ] +} From 6f3ce06c9d82f2d19e147fcc38211cb11f523970 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:30 +0800 Subject: [PATCH 0658/1461] Define c:feathers on Forge 1.21.1, which only has forge:feathers --- .../src/main/resources/data/c/tags/item/feathers.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 forge-1.21.1/src/main/resources/data/c/tags/item/feathers.json diff --git a/forge-1.21.1/src/main/resources/data/c/tags/item/feathers.json b/forge-1.21.1/src/main/resources/data/c/tags/item/feathers.json new file mode 100644 index 00000000..4554f87e --- /dev/null +++ b/forge-1.21.1/src/main/resources/data/c/tags/item/feathers.json @@ -0,0 +1,5 @@ +{ + "values": [ + "#forge:feathers" + ] +} From 036ac3ee407206b70019099be4429fb55dbd48f1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:30 +0800 Subject: [PATCH 0659/1461] Add ClientConfig.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/ClientConfig.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ClientConfig.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ClientConfig.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ClientConfig.java new file mode 100644 index 00000000..ff258847 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ClientConfig.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Per player look and feel, in {@code config/mcheli-client.toml}. Nothing here changes how the + * game plays, so each player picks for themselves and the server never sees it. + */ +public final class ClientConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue NIGHT_GLOW; + private static final ConfigSpec.IntValue PACK_CACHE_SIZE; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("effects"); + NIGHT_GLOW = builder + .comment("Whether tracers, rocket motors, flares and explosions glow. The glow is faint by day", + "and strongest at night, in caves and under water.") + .define("nightGlow", true); + builder.pop(); + + builder.push("packs"); + PACK_CACHE_SIZE = builder + .comment("How much the content packs downloaded from servers may take up in mcheli/cache, in megabytes.", + "The packs used longest ago are deleted first; the current server's packs are always kept.", + "A single pack larger than this is not downloaded.") + .defineInRange("cacheSize", 256, 16, 16384); + builder.pop(); + + SPEC = builder.build(); + } + + private ClientConfig() { + } + + /** Reads {@code config/mcheli-client.toml}, writing it with the defaults if it is new. */ + public static void load(Path configDir) { + Path file = configDir.resolve(MCHeli.MOD_ID + "-client.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + public static boolean nightGlow() { + return SPEC.isLoaded() ? NIGHT_GLOW.get() : NIGHT_GLOW.getDefault(); + } + + public static long packCacheBytes() { + return (long) (SPEC.isLoaded() ? PACK_CACHE_SIZE.get() : PACK_CACHE_SIZE.getDefault()) << 20; + } +} From 3001d3cbae96ed7cd8965c7bf203c539050b8cb6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:31 +0800 Subject: [PATCH 0660/1461] Add CommonHooks.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/CommonHooks.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/CommonHooks.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/CommonHooks.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/CommonHooks.java new file mode 100644 index 00000000..2cf2f6a8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/CommonHooks.java @@ -0,0 +1,83 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.network.PackSync; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.repository.RepositorySource; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.level.storage.LevelResource; + +import java.util.List; + +/** + * What the mod does on game events that both sides see. Each loader module calls these from its + * own events; nothing here knows which loader that is. + */ +public final class CommonHooks { + private CommonHooks() { + } + + /** Once the registries are frozen, on the main thread: what vanilla keeps in its own tables. */ + public static void commonSetup() { + DispenserBlock.registerBehavior(ModItems.THROWABLE.get(), new ThrowableItem.DispenseBehavior()); + } + + /** + * Adds the imported original content to the resource packs (client) and data packs (server). + * Without the original zip there is nothing to add. + */ + public static RepositorySource packSource() { + return onLoad -> { + // Fabric builds some pack repositories before our initializer has run. + LegacyContent content = MCHeli.legacyContent(); + if (content != null) { + content.pack().ifPresent(onLoad); + } + }; + } + + /** Before the worlds load: read the world's server config, then load the content packs it allows. */ + public static void serverAboutToStart(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + MCHeli.content().replaceWith(MCHeli.packs().load(true)); + } + + public static void serverStopped() { + ServerConfig.unload(); + } + + public static void serverTickEnd(MinecraftServer server) { + PackSync.pump(server); + } + + public static void registerCommands(CommandDispatcher dispatcher) { + McheliCommand.register(dispatcher); + } + + /** + * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack. Every + * MCHeli recipe goes into a player's recipe book when they join: the drafting table lists them + * from there, and they can be found in the crafting table's book too. + */ + public static void playerLoggedIn(ServerPlayer player) { + PackSync.onLogin(player); + // A remote pilot whose drone did not come back with them is shown and made mortal again. + if (!(player.getVehicle() instanceof AircraftEntity uav && uav.isRemotePilot(player))) { + AircraftEntity.clearRemotePilot(player); + } + List> ours = player.level().getServer().getRecipeManager().getRecipes().stream() + .filter(holder -> holder.id().identifier().getNamespace().equals(MCHeli.MOD_ID)) + .toList(); + player.awardRecipes(ours); + } + + public static void playerLoggedOut(ServerPlayer player) { + PackSync.onLogout(player); + } +} From 41673f93223326d9e1a58187e3e6859a16445ccb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:31 +0800 Subject: [PATCH 0661/1461] Add ContentPacks.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/ContentPacks.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ContentPacks.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ContentPacks.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ContentPacks.java new file mode 100644 index 00000000..d3bfdee5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ContentPacks.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackFolder; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * The content packs in {@code /mcheli/packs}, loaded together with the imported 1.0.3 + * content, which always comes first. + * + *

At startup every pack in the folder is loaded, so the vehicles exist before any world does. + * When a server starts (single player included) and on {@code /mcheli reload}, the folder is + * scanned again and the server config applied: the allow list, the size limit per pack and the + * limit for all packs together. The packs that pass are what players get sent. + * + *

A zip pack is read in place, so packs stay open while their definitions and assets are in + * use. The set a reload replaces is closed on the reload after that, so a client in the same game + * that is still drawing from it never reads from a closed zip. + */ +public final class ContentPacks { + private static final int PROBLEMS_IN_CHAT = 8; + private static final String SOUNDS = "assets/mcheli/sounds/"; + + private final Path folder; + private final PendingPacks pending; + private final LegacyContent legacy; + private volatile List active = List.of(); + private volatile List problems = List.of(); + private volatile Set sounds = Set.of(); + private List retired = List.of(); + + ContentPacks(Path gameDir, LegacyContent legacy) { + this.folder = gameDir.resolve(MCHeli.MOD_ID).resolve("packs"); + this.pending = new PendingPacks(gameDir.resolve(MCHeli.MOD_ID).resolve("pending")); + this.legacy = legacy; + } + + /** {@code /mcheli/packs}. */ + public Path folder() { + return folder; + } + + /** The packs whose definitions are loaded, in load order. The import is not among them. */ + public List active() { + return active; + } + + /** Everything that went wrong in the last load, packs left out included. */ + public List problems() { + return problems; + } + + /** + * Scans the folder and loads the import and the packs. Never throws; whatever goes wrong is + * logged and kept in {@link #problems()}. + * + * @param serverRules whether to apply the server config; off at startup, when no world exists + * @return the new content, for the caller to swap in + */ + public synchronized ContentRegistry load(boolean serverRules) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not create {}", folder, e); + } + + PackFolder.Scan scan = PackFolder.scan(folder); + List found = new ArrayList<>(scan.problems()); + List accepted = new ArrayList<>(); + long total = 0; + for (ContentPack pack : scan.packs()) { + long size; + String refusal; + try { + size = pack.archiveSize(); + refusal = serverRules ? refusal(pack, size, total) : null; + } catch (IOException e) { + size = 0; + refusal = "left out, could not be read: " + e; + } + if (refusal == null) { + accepted.add(pack); + total += size; + } else { + found.add(new PackProblem(pack.id(), refusal)); + close(List.of(pack)); + } + } + for (PackProblem problem : found) { + MCHeli.LOGGER.warn("Content pack {}", problem); + } + + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, legacy.assets())); + for (ContentPack pack : accepted) { + sources.add(pack.contentSource()); + } + ContentRegistry registry = ContentRegistry.load(sources); + found.addAll(registry.problems()); + + close(retired); + PendingPacks.cleanReplaced(folder); + retired = active; + active = List.copyOf(accepted); + problems = List.copyOf(found); + sounds = soundsOf(accepted); + MCHeli.LOGGER.info("Content packs in use: {}", active.isEmpty() ? "none" : active); + return registry; + } + + /** Sound names as {@code RuntimeAssets.soundFile} finds them: {@code heli.ogg} and {@code heli1.ogg} are both {@code heli}. */ + private static Set soundsOf(List packs) { + Set names = new HashSet<>(); + for (ContentPack pack : packs) { + for (String file : pack.files()) { + if (file.startsWith(SOUNDS) && file.endsWith(".ogg")) { + String name = file.substring(SOUNDS.length(), file.length() - ".ogg".length()); + names.add(name); + if (name.endsWith("1")) { + names.add(name.substring(0, name.length() - 1)); + } + } + } + } + return Set.copyOf(names); + } + + /** Whether a pack in use brought this sound, so that the sound manager has not heard of it. */ + public boolean hasSound(String name) { + return sounds.contains(name); + } + + /** What players have sent this server, waiting for an operator. */ + public PendingPacks pending() { + return pending; + } + + private static @Nullable String refusal(ContentPack pack, long size, long total) { + if (!ServerConfig.isPackAllowed(pack.id())) { + return "left out, not on the server's allow list (packs.allowList in mcheli-server.toml)"; + } + if (size > ServerConfig.maxPackBytes()) { + return "left out, %.1f MB is more than the server allows for one pack (packs.maxPackSize, %d MB)" + .formatted(size / 1048576.0, ServerConfig.maxPackBytes() >> 20); + } + if (total + size > ServerConfig.maxTotalPackBytes()) { + return "left out, it would take the packs past the server's total limit (packs.maxTotalSize, %d MB)" + .formatted(ServerConfig.maxTotalPackBytes() >> 20); + } + return null; + } + + /** Sends the problems of the last load as chat lines, the first few of them. */ + public void reportProblems(Consumer out) { + List list = problems; + for (int i = 0; i < Math.min(list.size(), PROBLEMS_IN_CHAT); i++) { + out.accept(Component.translatable("message.mcheli.pack.problem", list.get(i).pack(), list.get(i).message())); + } + if (list.size() > PROBLEMS_IN_CHAT) { + out.accept(Component.translatable("message.mcheli.pack.more_problems", list.size() - PROBLEMS_IN_CHAT)); + } + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } +} From a156354a997d06b731f0050c4779a580abfc1e57 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:31 +0800 Subject: [PATCH 0662/1461] Add ContentReload.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/ContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ContentReload.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ContentReload.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ContentReload.java new file mode 100644 index 00000000..7846b7bf --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.network.PackSync; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +/** + * Reads the vehicle and weapon definitions from disk again while the game runs, for + * {@code /mcheli reload}: the server config, then the imported content and the content packs. + * Vehicles in the world pick up their new definition on their next tick (see + * {@code AircraftEntity#definition}). Every player gets the new pack list, downloads what changed + * and rebuilds their content the same way. + */ +public final class ContentReload { + private ContentReload() { + } + + /** @return the newly loaded content, already in use */ + public static ContentRegistry reload(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + ContentRegistry fresh = MCHeli.packs().load(true); + MCHeli.content().replaceWith(fresh); + MCHeli.LOGGER.info("Content reloaded: {} vehicles, {} weapons", fresh.all().size(), fresh.weapons().size()); + PackSync.sendListToAll(server); + return fresh; + } +} From 95a34e29f3eaa18f92d88311f3ae9ee65b111934 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:32 +0800 Subject: [PATCH 0663/1461] Add LegacyContent.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/LegacyContent.java | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/LegacyContent.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/LegacyContent.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/LegacyContent.java new file mode 100644 index 00000000..495bb502 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/LegacyContent.java @@ -0,0 +1,239 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ImportTarget; +import com.jnhprojects.mcheli.content.LegacyImporter; +import net.minecraft.network.chat.Component; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PathPackResources; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackCompatibility; +import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.world.flag.FeatureFlagSet; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Stream; + +/** + * The original vehicles, models, textures and sounds, imported from the player's own copy of + * MCHeli 1.0.3. + * + *

That content is EMB4's and is not redistributed with this mod. Players download the original + * zip from the MCHeli CurseForge page and put it in {@code /mcheli}. On start the zip is + * converted once by {@link LegacyImporter} into {@code mcheli/imported}, which then serves as a + * required resource pack and data pack. The zip is converted again only when it changes or the + * importer's output format does; once imported it may be deleted. + * + *

Development runs point the {@code mcheli.legacyZip} system property at the zip instead. + */ +public final class LegacyContent { + /** The file name the original download has. Any zip with the same layout works. */ + public static final String ZIP_NAME = "MC1.7.10_mcheli_1.0.3.zip"; + + private static final String PACK_ID = "mcheli_content"; + private static final String MARKER = "import.properties"; + /** The pack layout this Minecraft version reads. */ + private static final ImportTarget TARGET = ImportTarget.MODERN; + + private final Path folder; + private final Path packRoot; + private @Nullable Component problem; + + private LegacyContent(Path folder) { + this.folder = folder; + this.packRoot = folder.resolve("imported"); + } + + /** Imports the zip if needed. Never throws; a missing or broken zip leaves the mod without content. */ + static LegacyContent prepare(Path gameDir) { + LegacyContent content = new LegacyContent(gameDir.resolve(MCHeli.MOD_ID)); + try { + content.importIfNeeded(); + } catch (IOException | UncheckedIOException e) { + MCHeli.LOGGER.error("Could not import the original MCHeli content", e); + content.problem = Component.translatable("message.mcheli.content.import_failed", e.getMessage()); + } + return content; + } + + /** The imported {@code assets/mcheli} folder. It does not exist when nothing was imported. */ + public Path assets() { + return packRoot.resolve("assets").resolve(MCHeli.MOD_ID); + } + + /** Why the vehicles are missing, for the player; empty when the content is in place. */ + public Optional problem() { + return Optional.ofNullable(problem); + } + + /** + * The imported folder as a pack, for both the client resource packs and the server data packs. + * Empty when nothing was imported. + */ + public Optional pack() { + if (!Files.isDirectory(packRoot)) { + return Optional.empty(); + } + PackLocationInfo location = new PackLocationInfo(PACK_ID, + Component.translatable("pack.mcheli.content"), PackSource.BUILT_IN, Optional.empty()); + Pack.Metadata metadata = new Pack.Metadata(Component.translatable("pack.mcheli.content.description"), + PackCompatibility.COMPATIBLE, FeatureFlagSet.of(), List.of()); + // Required, so it is enabled in every world and cannot be switched off. It sits at the + // bottom so that regular resource packs can still replace its textures and models. + return Optional.of(new Pack(location, new PathPackResources.PathResourcesSupplier(packRoot), metadata, + new PackSelectionConfig(true, Pack.Position.BOTTOM, false))); + } + + private void importIfNeeded() throws IOException { + Path zip = findZip(); + if (zip == null) { + if (Files.isDirectory(packRoot)) { + MCHeli.LOGGER.info("Using the MCHeli content imported earlier into {}", packRoot); + } else { + MCHeli.LOGGER.error("MCHeli content not found. Download {} from the MCHeli CurseForge page and put it in {}", + ZIP_NAME, folder.toAbsolutePath()); + problem = Component.translatable("message.mcheli.content.missing", ZIP_NAME, folder.toAbsolutePath().toString()); + } + return; + } + + Properties source = describe(zip); + if (source.equals(readMarker())) { + return; + } + + MCHeli.LOGGER.info("Importing MCHeli content from {}", zip); + Path staging = folder.resolve("imported.tmp"); + deleteTree(staging); + LegacyImporter.Summary summary = LegacyImporter.run(zip, staging, TARGET); + try (OutputStream out = Files.newOutputStream(staging.resolve(MARKER))) { + source.store(out, "Written by MCHeli Rework. Delete this folder to import again."); + } + swapIn(staging); + + MCHeli.LOGGER.info("Imported {} files, {} sounds, {} item models and {} recipes into {}", + summary.files(), summary.sounds(), summary.itemModels(), summary.recipes(), packRoot); + for (String skipped : summary.skippedRecipes()) { + MCHeli.LOGGER.info("Recipe left out: {}", skipped); + } + } + + /** + * Replaces the previous import with the fresh one in {@code staging}. + * + *

On Windows a folder that was just deleted keeps its name for a moment while another + * program (a file watcher, a virus scanner) still has a handle in it, and renaming onto that + * name fails with "access denied". So the old import is moved out of the way rather than + * deleted, the renames are retried for a few seconds, and the old copy is deleted last, on a + * best effort basis; a copy left behind is removed on the next import. + */ + private void swapIn(Path staging) throws IOException { + Path old = folder.resolve("imported.old"); + deleteQuietly(old); + if (Files.exists(packRoot)) { + try { + moveWithRetry(packRoot, old); + } catch (IOException e) { + deleteTree(packRoot); + } + } + moveWithRetry(staging, packRoot); + deleteQuietly(old); + } + + private static void moveWithRetry(Path from, Path to) throws IOException { + for (int attempt = 1; ; attempt++) { + try { + Files.move(from, to); + return; + } catch (FileSystemException e) { + if (attempt >= 15) { + throw e; + } + try { + Thread.sleep(50L * attempt); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw e; + } + } + } + } + + private static void deleteQuietly(Path root) { + try { + deleteTree(root); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not delete {}; it is removed on the next import", root, e); + } + } + + private @Nullable Path findZip() throws IOException { + String configured = System.getProperty("mcheli.legacyZip"); + if (configured != null && !configured.isBlank()) { + Path zip = Path.of(configured); + if (Files.isRegularFile(zip)) { + return zip; + } + MCHeli.LOGGER.warn("mcheli.legacyZip points to {}, which does not exist", zip); + } + + Files.createDirectories(folder); + if (Files.isRegularFile(folder.resolve(ZIP_NAME))) { + return folder.resolve(ZIP_NAME); + } + // Browsers rename repeated downloads, so take any zip, the same way every start. + try (Stream files = Files.list(folder)) { + return files.filter(p -> p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) + .filter(Files::isRegularFile) + .sorted() + .findFirst() + .orElse(null); + } + } + + /** What the current import was made from. Comparing size and time is enough to notice a new zip. */ + private static Properties describe(Path zip) throws IOException { + Properties p = new Properties(); + p.setProperty("format", Integer.toString(LegacyImporter.FORMAT)); + p.setProperty("target", TARGET.name()); + p.setProperty("source", zip.toAbsolutePath().normalize().toString()); + p.setProperty("size", Long.toString(Files.size(zip))); + p.setProperty("modified", Long.toString(Files.getLastModifiedTime(zip).toMillis())); + return p; + } + + private Properties readMarker() throws IOException { + Properties p = new Properties(); + Path marker = packRoot.resolve(MARKER); + if (Files.isRegularFile(marker)) { + try (InputStream in = Files.newInputStream(marker)) { + p.load(in); + } + } + return p; + } + + private static void deleteTree(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + try (Stream paths = Files.walk(root)) { + for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) { + Files.delete(p); + } + } + } +} From c3513ad5a9030826ba5a03dece85d558179bd55d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:32 +0800 Subject: [PATCH 0664/1461] Add MCHeli.java to the 1.21.11 shared sources --- .../java/com/jnhprojects/mcheli/MCHeli.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/MCHeli.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/MCHeli.java new file mode 100644 index 00000000..1a890651 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/MCHeli.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.registry.ModParticles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The mod, minus the loader. Each loader module's entry point installs its {@link Platform}, calls + * {@link #init()}, registers what {@code Registration} collected and wires its events to + * {@link CommonHooks} (and, on a client, {@code ClientHooks}). + */ +public final class MCHeli { + public static final String MOD_ID = "mcheli"; + public static final Logger LOGGER = LoggerFactory.getLogger(MCHeli.class); + + private static LegacyContent legacyContent; + private static ContentPacks packs; + private static ContentRegistry content; + + private MCHeli() { + } + + /** + * Imports the original content if needed, loads the definitions and declares the registry + * entries. The vehicle list must be known here: 1.0.0 had an item per vehicle, and those items + * are still registered so old worlds keep them. + */ + public static void init() { + Platform platform = Platform.get(); + LOGGER.info("MCHeli Rework {} on {}", platform.modVersion(), platform.loaderName()); + legacyContent = LegacyContent.prepare(platform.gameDir()); + packs = new ContentPacks(platform.gameDir(), legacyContent); + content = packs.load(false); + ModBlocks.init(); + ModBlockEntities.init(); + ModDataComponents.init(); + ModEntities.init(); + ModParticles.init(); + ModMenus.init(); + ModItems.init(content); + ModCreativeTabs.init(); + } + + /** + * Vehicle and weapon definitions, from the imported original content and the content packs. + * Available from mod construction onwards. Always the same object; a reload changes what it + * holds. + */ + public static ContentRegistry content() { + return content; + } + + /** Where the original vehicle content came from, and why it is missing if it is. */ + public static LegacyContent legacyContent() { + return legacyContent; + } + + /** The content packs in {@code mcheli/packs} and the ones in use. */ + public static ContentPacks packs() { + return packs; + } +} From 644aa5c7829add6228070d56e0dcca7b5e066cb6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:33 +0800 Subject: [PATCH 0665/1461] Add McheliCommand.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/McheliCommand.java | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/McheliCommand.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/McheliCommand.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/McheliCommand.java new file mode 100644 index 00000000..25a3aea8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/McheliCommand.java @@ -0,0 +1,262 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.Definition; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.network.PackSubmissions; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * {@code /mcheli}: what content is loaded, and operator tools to hand out vehicles and reload the + * definitions and content packs. + *

+ *   /mcheli                              content source and counts
+ *   /mcheli list [helicopter|plane|...]  loaded vehicles
+ *   /mcheli packs                        content packs in use, and their problems
+ *   /mcheli give <players> <vehicle> [count]   (operators)
+ *   /mcheli reload                       re-read the definitions and packs (operators)
+ *   /mcheli pending                      packs players have sent, waiting (operators)
+ *   /mcheli approve <pack>               put a waiting pack in use (operators)
+ *   /mcheli reject <pack>                throw a waiting pack away (operators)
+ * 
+ */ +public final class McheliCommand { + private static final List KINDS = List.of("helicopter", "plane", "tank", "vehicle"); + private static final DynamicCommandExceptionType UNKNOWN_VEHICLE = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.unknown_vehicle", name)); + private static final DynamicCommandExceptionType UNKNOWN_PENDING = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.pending.unknown", name)); + + private McheliCommand() { + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal(MCHeli.MOD_ID) + .executes(McheliCommand::status) + .then(Commands.literal("list") + .executes(ctx -> list(ctx, null)) + .then(Commands.argument("kind", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest(KINDS, builder)) + .executes(ctx -> list(ctx, StringArgumentType.getString(ctx, "kind"))))) + .then(Commands.literal("packs") + .executes(McheliCommand::packs)) + .then(Commands.literal("give") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("targets", EntityArgument.players()) + .then(Commands.argument("vehicle", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest( + MCHeli.content().all().stream().map(AircraftDefinition::name), builder)) + .executes(ctx -> give(ctx, 1)) + .then(Commands.argument("count", IntegerArgumentType.integer(1, 64)) + .executes(ctx -> give(ctx, IntegerArgumentType.getInteger(ctx, "count"))))))) + .then(Commands.literal("reload") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::reload)) + .then(Commands.literal("pending") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::pending)) + .then(Commands.literal("approve") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::approve))) + .then(Commands.literal("reject") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::reject)))); + } + + private static CompletableFuture suggestPending(CommandContext ctx, SuggestionsBuilder builder) { + try { + return SharedSuggestionProvider.suggest(MCHeli.packs().pending().list().stream().map(PendingPacks.Submission::id), builder); + } catch (IOException e) { + return builder.buildFuture(); + } + } + + private static int pending(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List waiting; + try { + waiting = MCHeli.packs().pending().list(); + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + if (waiting.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.none"), false); + } + for (PendingPacks.Submission submission : waiting) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.entry", submission.id(), submission.manifest().version(), + submission.manifest().author(), submission.submitter(), "%.1f".formatted(submission.size() / 1048576.0), + String.join(", ", submission.definitions())), false); + List replaced = replaced(submission); + if (!replaced.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.replaces", String.join(", ", replaced)), false); + } + } + return waiting.size(); + } + + /** + * What going live would take the place of: a pack with the same id, and vehicles or weapons of + * another pack with the same file name. That is allowed, so it is the operator's call to make, + * but it should not come as a surprise. + */ + private static List replaced(PendingPacks.Submission submission) { + List names = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.id().equals(submission.id())) { + names.add("pack " + pack.id()); + } + } + ContentRegistry content = MCHeli.content(); + for (String path : submission.definitions()) { + String name = path.substring(path.lastIndexOf('/') + 1, path.length() - ".txt".length()); + Definition existing = path.startsWith("weapons/") ? content.weapon(name) : content.any(name); + if (existing != null && !existing.pack().equals(submission.id())) { + names.add(name + " (" + existing.pack() + ")"); + } + } + return names; + } + + private static PendingPacks.Submission waiting(CommandContext ctx) throws CommandSyntaxException { + String id = StringArgumentType.getString(ctx, "pack"); + PendingPacks.Submission submission; + try { + submission = MCHeli.packs().pending().find(id); + } catch (IOException e) { + submission = null; + } + if (submission == null) { + throw UNKNOWN_PENDING.create(id); + } + return submission; + } + + private static int approve(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + if (PackSubmissions.install(source.getServer(), submission)) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.approve.success", submission.id(), submission.submitter()), true); + return 1; + } + source.sendFailure(Component.translatable("commands.mcheli.approve.left_out", submission.id())); + MCHeli.packs().reportProblems(source::sendFailure); + } catch (IOException | PackException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.getMessage())); + } + return 0; + } + + private static int reject(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + PackSubmissions.reject(source.getServer(), submission); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reject.success", submission.id(), submission.submitter()), true); + return 1; + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + } + + private static int status(CommandContext ctx) { + ContentRegistry content = MCHeli.content(); + CommandSourceStack source = ctx.getSource(); + MCHeli.legacyContent().problem().ifPresent(source::sendFailure); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status", + content.helicopters().size(), content.planes().size(), content.tanks().size(), content.vehicles().size(), + content.weapons().size(), MCHeli.legacyContent().assets().toString()), false); + List packs = MCHeli.packs().active(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status.packs", packs.size(), + packs.stream().map(ContentPack::id).collect(Collectors.joining(", "))), false); + return content.all().size(); + } + + private static int packs(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List packs = MCHeli.packs().active(); + if (packs.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.none", MCHeli.packs().folder().toString()), false); + } else { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.header", packs.size(), MCHeli.packs().folder().toString()), false); + } + ContentRegistry content = MCHeli.content(); + for (ContentPack pack : packs) { + long vehicles = content.all().stream().filter(def -> def.pack().equals(pack.id())).count(); + long weapons = content.weapons().stream().filter(def -> def.pack().equals(pack.id())).count(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.entry", pack.id(), pack.manifest().version(), + pack.manifest().name(), vehicles, weapons, ContentPack.shortHash(pack.hash())), false); + if (!pack.manifest().author().isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.author", pack.manifest().author()), false); + } + } + MCHeli.packs().reportProblems(source::sendFailure); + return packs.size(); + } + + private static int list(CommandContext ctx, String kind) { + List defs = MCHeli.content().all().stream() + .filter(def -> kind == null || def.kind().equals(kind)) + .toList(); + String names = defs.stream().map(AircraftDefinition::name).collect(Collectors.joining(", ")); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.list", defs.size(), names), false); + return defs.size(); + } + + private static int give(CommandContext ctx, int count) throws CommandSyntaxException { + String name = StringArgumentType.getString(ctx, "vehicle"); + AircraftDefinition def = MCHeli.content().any(name); + if (def == null) { + throw UNKNOWN_VEHICLE.create(name); + } + Collection targets = EntityArgument.getPlayers(ctx, "targets"); + for (ServerPlayer player : targets) { + // Vehicle items do not stack, so each one is a separate stack. + for (int i = 0; i < count; i++) { + player.getInventory().placeItemBackInInventory(AircraftItem.stackOf(def)); + } + } + Component vehicle = AircraftItem.stackOf(def).getHoverName(); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.give", count, vehicle, targets.size()), true); + return targets.size(); + } + + private static int reload(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + ContentRegistry fresh = ContentReload.reload(source.getServer()); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reload.success", + fresh.all().size(), fresh.weapons().size(), MCHeli.packs().active().size()), true); + MCHeli.packs().reportProblems(source::sendFailure); + return fresh.all().size(); + } +} From 086efc937f1e1d3d70e1938fca673937e2ec141c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:33 +0800 Subject: [PATCH 0666/1461] Add ModSounds.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/ModSounds.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ModSounds.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ModSounds.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ModSounds.java new file mode 100644 index 00000000..ed6eeb7e --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ModSounds.java @@ -0,0 +1,64 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.core.Holder; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Sound events from {@code assets/mcheli/sounds.json}. + * + *

The events are not put in the registry. Content files name their sounds freely and a content + * pack may bring its own, so they are created on demand as direct holders, which the sound packet + * and the sound manager both accept. + * + *

A sound a pack brought is in no {@code sounds.json}, so the sound manager would drop it as + * unknown. For those the server sends our own packet, and the client streams the file. + */ +public final class ModSounds { + /** How far a sound carries at a volume of one, and per volume above that, as in vanilla. */ + private static final float RANGE = 16.0F; + + private static final Map EVENTS = new ConcurrentHashMap<>(); + + private ModSounds() { + } + + public static SoundEvent get(String name) { + return EVENTS.computeIfAbsent(name, n -> SoundEvent.createVariableRangeEvent(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, n))); + } + + /** Plays {@code name} at the entity for everyone nearby. Call on the server. */ + public static void playAt(Entity entity, String name, float volume, float pitch) { + playAt(entity.level(), entity.getX(), entity.getY(), entity.getZ(), name, volume, pitch); + } + + /** Plays {@code name} at a position for everyone nearby. Call on the server. */ + public static void playAt(Level level, double x, double y, double z, String name, float volume, float pitch) { + if (level instanceof ServerLevel server && MCHeli.packs().hasSound(name)) { + ModNetwork.sendNear(server, x, y, z, RANGE * Math.max(volume, 1.0F), new PackSoundPacket(name, x, y, z, volume, pitch)); + } else { + level.playSound(null, x, y, z, get(name), SoundSource.NEUTRAL, volume, pitch); + } + } + + /** Plays {@code name} for one player only, such as a hit confirmation. */ + public static void playTo(ServerPlayer player, String name, float volume, float pitch) { + if (MCHeli.packs().hasSound(name)) { + ModNetwork.sendToPlayer(player, new PackSoundPacket(name, player.getX(), player.getY(), player.getZ(), volume, pitch)); + } else { + player.connection.send(new ClientboundSoundPacket(Holder.direct(get(name)), SoundSource.PLAYERS, + player.getX(), player.getY(), player.getZ(), volume, pitch, player.getRandom().nextLong())); + } + } +} From 4b2ab22e82a12afd7f165efb8c63b5da423111e5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:34 +0800 Subject: [PATCH 0667/1461] Add ServerConfig.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/ServerConfig.java | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ServerConfig.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ServerConfig.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ServerConfig.java new file mode 100644 index 00000000..9db31738 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/ServerConfig.java @@ -0,0 +1,172 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Server rules, the part of the original {@code mcheli.cfg} that decides how the game plays. + * The file lives per world in {@code serverconfig/mcheli-server.toml}, where Forge kept it, and is + * never sent to clients (only the server applies these rules). It is read when the world starts + * and again on {@code /mcheli reload}. + * + *

The values only exist while a world is loaded. Before that, and on clients, the getters return + * the defaults, which match 1.0.0. + */ +public final class ServerConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue EXPLOSIONS_BREAK_BLOCKS; + private static final ConfigSpec.BooleanValue EXPLOSIONS_START_FIRES; + private static final ConfigSpec.DoubleValue VEHICLE_DAMAGE_FACTOR; + private static final ConfigSpec.BooleanValue INFINITE_AMMO; + private static final ConfigSpec.BooleanValue INFINITE_FUEL; + private static final ConfigSpec.StringListValue DISABLED_VEHICLES; + private static final ConfigSpec.StringListValue PACK_ALLOW_LIST; + private static final ConfigSpec.IntValue MAX_PACK_SIZE; + private static final ConfigSpec.IntValue MAX_TOTAL_SIZE; + private static final ConfigSpec.BooleanValue ALLOW_SUBMISSIONS; + private static final ConfigSpec.BooleanValue REQUIRE_APPROVAL; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("explosions"); + EXPLOSIONS_BREAK_BLOCKS = builder + .comment("Whether weapons and crashing vehicles break blocks. The mobGriefing game rule also applies.", + "Original: Explosion_DestroyBlock") + .define("breakBlocks", true); + EXPLOSIONS_START_FIRES = builder + .comment("Whether incendiary weapons and crashes set blocks on fire. Original: Explosion_FlamingBlock") + .define("startFires", true); + builder.pop(); + + builder.push("vehicles"); + VEHICLE_DAMAGE_FACTOR = builder + .comment("Multiplies the damage every vehicle takes, after armour. 0.5 makes them twice as tough.") + .defineInRange("damageFactor", 1.0, 0.0, 100.0); + DISABLED_VEHICLES = builder + .comment("Vehicles that cannot be placed, by definition name (the file name, such as \"ah-64\").", + "Vehicles already in the world stay.") + .defineList("disabled", List.of()); + INFINITE_FUEL = builder + .comment("Whether vehicles never run out of fuel. Original: InfinityFuel") + .define("infiniteFuel", false); + builder.pop(); + + builder.push("weapons"); + INFINITE_AMMO = builder + .comment("Whether vehicle weapons never run out of ammunition. Original: InfinityAmmo") + .define("infiniteAmmo", false); + builder.pop(); + + builder.comment("Content packs from /mcheli/packs. Players get the packs from the server when they join.", + "Changes apply when the world starts or after /mcheli reload.") + .push("packs"); + PACK_ALLOW_LIST = builder + .comment("Pack ids the server loads, such as \"rework-example\". Empty loads every pack in the folder.") + .defineList("allowList", List.of()); + MAX_PACK_SIZE = builder + .comment("Largest pack the server loads and sends to players, in megabytes.") + .defineInRange("maxPackSize", 8, 1, 512); + MAX_TOTAL_SIZE = builder + .comment("Most the server loads and sends in all, in megabytes. Packs are taken in id order until", + "the next one would go over.") + .defineInRange("maxTotalSize", 64, 1, 4096); + ALLOW_SUBMISSIONS = builder + .comment("Whether players may send this server packs they made in the workshop. They wait in mcheli/pending", + "until an operator runs /mcheli approve. Off, players can only get packs from the server's owner.") + .define("allowSubmissions", false); + REQUIRE_APPROVAL = builder + .comment("Whether a submitted pack waits for an operator. Off, any player can add vehicles and weapons for", + "everyone, and even replace existing ones, so only turn it off among people you trust. A pack taking", + "the id of a pack already in use always waits.") + .define("requireApproval", true); + builder.pop(); + + SPEC = builder.build(); + } + + private ServerConfig() { + } + + /** Reads the world's file, writing it with the defaults if it is new. */ + public static void load(Path worldDir) { + Path file = worldDir.resolve("serverconfig").resolve(MCHeli.MOD_ID + "-server.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + /** The world closed: back to the defaults. */ + public static void unload() { + SPEC.unload(); + } + + public static boolean explosionsBreakBlocks() { + return SPEC.isLoaded() ? EXPLOSIONS_BREAK_BLOCKS.get() : EXPLOSIONS_BREAK_BLOCKS.getDefault(); + } + + public static boolean explosionsStartFires() { + return SPEC.isLoaded() ? EXPLOSIONS_START_FIRES.get() : EXPLOSIONS_START_FIRES.getDefault(); + } + + public static float vehicleDamageFactor() { + double factor = SPEC.isLoaded() ? VEHICLE_DAMAGE_FACTOR.get() : VEHICLE_DAMAGE_FACTOR.getDefault(); + return (float) factor; + } + + public static boolean infiniteAmmo() { + return SPEC.isLoaded() ? INFINITE_AMMO.get() : INFINITE_AMMO.getDefault(); + } + + public static boolean infiniteFuel() { + return SPEC.isLoaded() ? INFINITE_FUEL.get() : INFINITE_FUEL.getDefault(); + } + + /** Whether the allow list lets the server load this pack. An empty list allows every pack. */ + public static boolean isPackAllowed(String id) { + if (!SPEC.isLoaded() || PACK_ALLOW_LIST.get().isEmpty()) { + return true; + } + for (String allowed : PACK_ALLOW_LIST.get()) { + if (allowed.trim().toLowerCase(Locale.ROOT).equals(id)) { + return true; + } + } + return false; + } + + public static long maxPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_PACK_SIZE.get() : MAX_PACK_SIZE.getDefault()) << 20; + } + + public static long maxTotalPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_TOTAL_SIZE.get() : MAX_TOTAL_SIZE.getDefault()) << 20; + } + + public static boolean allowSubmissions() { + return SPEC.isLoaded() ? ALLOW_SUBMISSIONS.get() : ALLOW_SUBMISSIONS.getDefault(); + } + + public static boolean requireApproval() { + return SPEC.isLoaded() ? REQUIRE_APPROVAL.get() : REQUIRE_APPROVAL.getDefault(); + } + + public static boolean isDisabled(String vehicle) { + if (!SPEC.isLoaded()) { + return false; + } + for (String name : DISABLED_VEHICLES.get()) { + if (name.trim().toLowerCase(Locale.ROOT).equals(vehicle)) { + return true; + } + } + return false; + } +} From 977c18f5fba5cd3135040f47a813e1c79702b006 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:34 +0800 Subject: [PATCH 0668/1461] Add block/DraftingTableBlock.java to the 1.21.11 shared sources --- .../mcheli/block/DraftingTableBlock.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java new file mode 100644 index 00000000..8545d2b5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java @@ -0,0 +1,100 @@ +package com.jnhprojects.mcheli.block; + +import com.mojang.serialization.MapCodec; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +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.block.state.properties.IntegerProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jspecify.annotations.Nullable; + +/** + * The drafting table, {@code MCH_DraftingTableBlock}: right-click opens the recipe browser, and + * Shift and right-click switches its lamp. It turns in eighths of a circle to face whoever placed + * it, and its block entity draws the original model. + */ +public class DraftingTableBlock extends BaseEntityBlock { + private static final MapCodec CODEC = simpleCodec(DraftingTableBlock::new); + public static final IntegerProperty ROTATION = IntegerProperty.create("rotation", 0, 7); + public static final BooleanProperty LIT = BooleanProperty.create("lit"); + private static final VoxelShape SHAPE = Block.column(16.0, 0.0, 15.0); + private static final Component TITLE = Component.translatable("block.mcheli.drafting_table"); + + public DraftingTableBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(ROTATION, 0).setValue(LIT, false)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ROTATION, LIT); + } + + /** {@code onBlockPlacedBy}: the nearest eighth of the placer's heading. */ + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + float yaw = context.getPlayer() != null ? context.getPlayer().getYRot() : 0.0F; + int rotation = (int) ((Mth.positiveModulo(yaw, 360.0F) + 22.5F) / 45.0F) % 8; + return defaultBlockState().setValue(ROTATION, rotation); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive()) { + level.setBlock(pos, state.cycle(LIT), Block.UPDATE_ALL); + level.playSound(null, pos, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.BLOCKS, 0.3F, 0.5F); + } else { + player.openMenu(state.getMenuProvider(level, pos)); + } + return InteractionResult.SUCCESS; + } + + @Override + protected @Nullable MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { + return new SimpleMenuProvider((id, inventory, player) -> new DraftingTableMenu(id, inventory, ContainerLevelAccess.create(level, pos)), TITLE); + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + /** The block entity renderer draws the table. */ + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new DraftingTableBlockEntity(pos, state); + } +} From 083d70cae52fd4722578e9c1dedd9e4c70cf620c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:35 +0800 Subject: [PATCH 0669/1461] Add block/DraftingTableBlockEntity.java to the 1.21.11 shared sources --- .../mcheli/block/DraftingTableBlockEntity.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java new file mode 100644 index 00000000..56238f3f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.block; + +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** {@code MCH_DraftingTableTileEntity}: holds nothing; it is there so the table's model can be drawn. */ +public class DraftingTableBlockEntity extends BlockEntity { + public DraftingTableBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.DRAFTING_TABLE.get(), pos, state); + } +} From bbfa478a45fe2e68cf31fa314f9b49fa840e8a83 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:35 +0800 Subject: [PATCH 0670/1461] Add client/CameraAngles.java to the 1.21.11 shared sources --- .../mcheli/client/CameraAngles.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java new file mode 100644 index 00000000..ab62e120 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.client; + +import net.minecraft.client.Camera; + +/** + * The camera's angles for this frame, after vanilla placed the camera and before it renders, as + * the loader module hands them to {@link ClientHooks#computeCameraAngles}. Forge and NeoForge + * back this with their viewport event; Fabric with a mixin at the same spot. + */ +public interface CameraAngles { + Camera camera(); + + float partialTick(); + + float yaw(); + + float pitch(); + + float roll(); + + void setYaw(float yaw); + + void setPitch(float pitch); + + void setRoll(float roll); +} From 165fa0da4bdb22c6341f07951d3b9e53594f5210 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:36 +0800 Subject: [PATCH 0671/1461] Add client/ClientContentReload.java to the 1.21.11 shared sources --- .../mcheli/client/ClientContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java new file mode 100644 index 00000000..70bcc23e --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.CreativeModeTab; + +/** + * Makes the client show the content it now holds: after the server's pack list was applied, which + * also happens after {@code /mcheli reload}. + */ +final class ClientContentReload { + private ClientContentReload() { + } + + /** Drops cached models and textures so edited and new files load, and rebuilds the creative tabs. */ + static void refresh() { + Minecraft mc = Minecraft.getInstance(); + MeshCache.INSTANCE.clear(); + RuntimeAssets.refreshTextures(); + if (mc.level != null && mc.player != null) { + boolean operator = mc.options.operatorItemsTab().get() && mc.player.canUseGameMasterBlocks(); + CreativeModeTab.ItemDisplayParameters params = + new CreativeModeTab.ItemDisplayParameters(mc.level.enabledFeatures(), operator, mc.level.registryAccess()); + ModCreativeTabs.all().forEach(tab -> tab.buildContents(params)); + } + } +} From 87fa2270142e14646a7394ba923c16fb356e9c91 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:36 +0800 Subject: [PATCH 0672/1461] Let the loaders load post effects and read the camera partial tick on 1.21.11 --- .../mcheli/client/ClientHooks.java | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java new file mode 100644 index 00000000..4f140cc5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -0,0 +1,332 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.client.hud.HudScripts; +import com.jnhprojects.mcheli.client.render.A10Renderer; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import com.jnhprojects.mcheli.client.render.ChainRenderer; +import com.jnhprojects.mcheli.client.render.DraftingTableRenderer; +import com.jnhprojects.mcheli.client.render.EquipmentRenderer; +import com.jnhprojects.mcheli.client.render.FlareRenderer; +import com.jnhprojects.mcheli.client.render.GltdRenderer; +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.render.ParachuteRenderer; +import com.jnhprojects.mcheli.client.render.ProjectileRenderer; +import com.jnhprojects.mcheli.client.render.RiderPose; +import com.jnhprojects.mcheli.client.render.ThrowableRenderer; +import com.jnhprojects.mcheli.client.render.UavStationRenderer; +import com.jnhprojects.mcheli.client.screen.DraftingTableScreen; +import com.jnhprojects.mcheli.client.screen.TeamManagerScreen; +import com.jnhprojects.mcheli.client.screen.UavStationScreen; +import com.jnhprojects.mcheli.client.screen.VehicleScreen; +import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; +import com.jnhprojects.mcheli.client.workshop.WorkshopClient; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.team.Markers; +import com.jnhprojects.mcheli.team.TeamManager; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.NoopRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import java.util.List; +import java.util.Map; + +/** + * Client side counterpart of {@code CommonHooks}: what the mod does on client events. The loader + * module calls these from its own events. Kept out of the shared entry point so dedicated servers + * never load these classes. + */ +public final class ClientHooks { + /** The weapon HUD's layer, drawn above the vanilla HUD. */ + public static final Identifier HUD_LAYER = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "weapons"); + + private static boolean shaderProgramsAssigned; + + private ClientHooks() { + } + + /** Loads a post effect into the game renderer, whose own method is private on 1.21.11. */ + public interface PostEffects { + void load(Identifier effect); + } + + private static PostEffects postEffects = effect -> { + }; + + static PostEffects postEffects() { + return postEffects; + } + + /** During mod construction, on a client only. */ + public static void init(PostEffects effects) { + postEffects = effects; + ClientConfig.load(Platform.get().configDir()); + ClientAccess.install(() -> Minecraft.getInstance().player, ClientPacks.INSTANCE); + } + + /** How a loader registers an entity renderer. */ + public interface EntityRenderers { + void register(EntityType type, EntityRendererProvider provider); + } + + public static void registerEntityRenderers(EntityRenderers renderers) { + renderers.register(ModEntities.HELICOPTER.get(), AircraftRenderer::new); + renderers.register(ModEntities.PLANE.get(), AircraftRenderer::new); + renderers.register(ModEntities.TANK.get(), AircraftRenderer::new); + renderers.register(ModEntities.VEHICLE.get(), AircraftRenderer::new); + renderers.register(ModEntities.BULLET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.BOMB.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AA_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AS_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AT_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TV_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TORPEDO.get(), ProjectileRenderer::new); + renderers.register(ModEntities.MARKER_ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.DISPENSED_ITEM.get(), ProjectileRenderer::new); + renderers.register(ModEntities.FLARE.get(), FlareRenderer::new); + renderers.register(ModEntities.A10.get(), A10Renderer::new); + renderers.register(ModEntities.PARACHUTE.get(), ParachuteRenderer::new); + renderers.register(ModEntities.THROWABLE.get(), ThrowableRenderer::new); + renderers.register(ModEntities.GLTD.get(), GltdRenderer::new); + renderers.register(ModEntities.CHAIN.get(), ChainRenderer::new); + renderers.register(ModEntities.CONTAINER.get(), context -> new EquipmentRenderer<>(context, "container", 0.2F, 0xFFBFBFBF, 1.0F)); + renderers.register(ModEntities.UAV_STATION.get(), UavStationRenderer::new); + renderers.register(ModEntities.RAPPEL.get(), NoopRenderer::new); + } + + /** How a loader registers a block entity renderer. */ + public interface BlockEntityRenderers { + void register(BlockEntityType type, + BlockEntityRendererProvider provider); + } + + public static void registerBlockEntityRenderers(BlockEntityRenderers renderers) { + renderers.register(ModBlockEntities.DRAFTING_TABLE.get(), DraftingTableRenderer::new); + } + + /** How a loader registers the screen of a menu: vanilla's own method is not public on all of them. */ + public interface MenuScreenRegistrar { + > void register(MenuType type, + MenuScreens.ScreenConstructor screen); + } + + /** On the main thread during client setup, once our menu types are registered. */ + public static void registerMenuScreens(MenuScreenRegistrar screens) { + screens.register(ModMenus.VEHICLE.get(), VehicleScreen::new); + screens.register(ModMenus.UAV_STATION.get(), UavStationScreen::new); + screens.register(ModMenus.DRAFTING_TABLE.get(), DraftingTableScreen::new); + } + + public static List keyMappings() { + return KeyBindings.ALL; + } + + /** Client resource reload listeners, by the id Fabric wants for each. */ + public static Map reloadListeners() { + return Map.of(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "meshes"), MeshCache.INSTANCE, + Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "hud_scripts"), HudScripts.INSTANCE); + } + + public static void clientTickEnd() { + if (!shaderProgramsAssigned) { + // On the first tick rather than at construction, so Iris has surely loaded by now. + shaderProgramsAssigned = true; + Glow.assignShaderPrograms(); + } + CrewController.onClientTick(); + GltdController.onClientTick(); + RangefinderController.onClientTick(); + LauncherController.onClientTick(); + VehicleCamera.tick(); + WeaponController.onClientTick(); + HudRenderer.tick(); + EngineSoundPlayer.tick(); + TargetMarkers.tick(); + WorkshopClient.tick(); + while (KeyBindings.TEAM_MANAGER.consumeClick()) { + openTeamManager(); + } + } + + /** Operators get the team manager; anyone else is told it is theirs to open. */ + private static void openTeamManager() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.screen != null) { + return; + } + if (TeamManager.mayManage(mc.player)) { + mc.setScreen(new TeamManagerScreen()); + } else { + mc.player.displayClientMessage(Component.translatable("message.mcheli.team.operators_only"), true); + } + } + + /** Tilts the view with the hull and moves the eye into the cockpit or onto a TV missile. */ + public static void computeCameraAngles(CameraAngles angles) { + CrewController.onCameraAngles(angles); + GltdController.onCameraAngles(angles); + } + + /** Sets the camera's rotation, roll included, the way Forge's {@code Camera#setRotation} does. */ + public interface CameraRotation { + void set(Camera camera, float yaw, float pitch, float roll); + } + + /** + * For loaders without Forge's camera angle event: called where Forge fires it, right after + * vanilla placed the camera for this frame. Starts from the camera's own angles and no roll, + * and only touches the rotation when something changed it. + */ + public static void computeCameraAngles(Camera camera, float partialTick, CameraRotation rotation) { + Angles angles = new Angles(camera, partialTick, camera.yRot(), camera.xRot()); + computeCameraAngles(angles); + if (angles.yaw != camera.yRot() || angles.pitch != camera.xRot() || angles.roll != 0.0F) { + rotation.set(camera, angles.yaw, angles.pitch, angles.roll); + } + } + + private static final class Angles implements CameraAngles { + private final Camera camera; + private final float partialTick; + private float yaw; + private float pitch; + private float roll; + + Angles(Camera camera, float partialTick, float yaw, float pitch) { + this.camera = camera; + this.partialTick = partialTick; + this.yaw = yaw; + this.pitch = pitch; + } + + @Override + public Camera camera() { + return camera; + } + + @Override + public float partialTick() { + return partialTick; + } + + @Override + public float yaw() { + return yaw; + } + + @Override + public float pitch() { + return pitch; + } + + @Override + public float roll() { + return roll; + } + + @Override + public void setYaw(float yaw) { + this.yaw = yaw; + } + + @Override + public void setPitch(float pitch) { + this.pitch = pitch; + } + + @Override + public void setRoll(float roll) { + this.roll = roll; + } + } + + /** The field of view for this frame, narrowed while the player zooms through a sight. */ + public static float fov(float fov) { + return LauncherController.fov(RangefinderController.fov(VehicleCamera.fov(fov))); + } + + /** Whether to skip drawing the first person hand this frame. */ + public static boolean hidesHand() { + return CrewController.hidesHand() || GltdController.hidesHand() || RangefinderController.isScoped() || LauncherController.isRaised(); + } + + /** What happens to a press of the use key. */ + public enum UseKey { + /** Vanilla handles it. */ + PASS, + /** Taken, and the hand does not swing: it is a weapon trigger. */ + CONSUME, + /** Taken, and the hand swings as for a vanilla click: the player boards a vehicle. */ + CONSUME_AND_SWING + } + + public static UseKey onUseKey(InteractionHand hand) { + if (WeaponController.onUseItem() || GltdController.onUseItem()) { + return UseKey.CONSUME; + } + return VehiclePicker.onUseItem(hand) ? UseKey.CONSUME_AND_SWING : UseKey.PASS; + } + + /** The pick block key switches weapons in a seat that has any; true when it was taken. */ + public static boolean onPickBlockKey() { + return WeaponController.onPickBlock(); + } + + /** The partial tick the camera entity is drawn at, for the loaders whose event does not carry it. */ + public static float cameraPartialTicks() { + return Minecraft.getInstance().gameRenderer.getMainCamera().getPartialTickTime(); + } + + public static void beforeRenderLiving(LivingEntityRenderState state, PoseStack pose, float partialTicks) { + RiderPose.beforeRender(state, pose, partialTicks); + } + + public static void drawHud(GuiGraphics graphics, DeltaTracker delta) { + WeaponHud.draw(graphics, delta); + GltdController.drawHud(graphics, delta); + RangefinderController.drawHud(graphics, delta); + LauncherController.drawHud(graphics, delta); + TargetMarkers.draw(graphics, delta); + } + + /** Without the original zip there are no vehicles, so say so where the player will see it. */ + public static void loggedIn(LocalPlayer player) { + MCHeli.legacyContent().problem().ifPresent(message -> player.displayClientMessage(message, false)); + } + + public static void loggedOut() { + ClientPacks.INSTANCE.loggedOut(); + WorkshopClient.loggedOut(); + Markers.clear(); + } +} From 6451cc8d7560ee83e1fda6ae42404633d638f6ba Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:36 +0800 Subject: [PATCH 0673/1461] Send pack messages through displayClientMessage on 1.21.11 --- .../mcheli/client/ClientPacks.java | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java new file mode 100644 index 00000000..7e7d61b1 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java @@ -0,0 +1,338 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackRequestPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import com.jnhprojects.mcheli.platform.Platform; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * The client half of the content pack sync (the server half is {@code PackSync}). + * + *

When the server sends its pack list, every pack this client has neither in its own + * {@code mcheli/packs} nor in {@code mcheli/cache} is requested. Each arrives in chunks, is checked + * against its hash and kept as {@code mcheli/cache/.zip}. Once all are there, the content is + * rebuilt from the client's own import plus exactly the server's packs, in the server's order, so + * the vehicle a player flies (which their client simulates) has the server's definition. + * + *

In single player, and for the host of a LAN game, client and server share one registry, so + * only the client's view of the server's packs is refreshed. + * + *

The cache is capped by {@code packs.cacheSize} in the client config: after a download, the + * packs used longest ago go first. The current server's packs are always kept. + */ +public final class ClientPacks implements ClientAccess.PackReceiver { + public static final ClientPacks INSTANCE = new ClientPacks(); + + private static final Pattern CACHED = Pattern.compile("[0-9a-f]{64}\\.zip"); + + private final Path cache = Platform.get().gameDir().resolve(MCHeli.MOD_ID).resolve("cache"); + private List wanted = List.of(); + private final Map downloads = new LinkedHashMap<>(); + /** Packs this class opened for the current server; closed when the next list replaces them. */ + private List mounted = List.of(); + private int downloaded; + + private ClientPacks() { + } + + @Override + public void onPackList(PackListPacket packet) { + if (Minecraft.getInstance().hasSingleplayerServer()) { + useRoots(MCHeli.packs().active()); + ClientContentReload.refresh(); + return; + } + + wanted = packet.packs(); + downloads.clear(); + downloaded = 0; + long cap = ClientConfig.packCacheBytes(); + List request = new ArrayList<>(); + long bytes = 0; + for (PackListPacket.Entry entry : wanted) { + if (find(entry) != null) { + continue; + } + if (entry.size() > cap || entry.size() <= 0) { + say(Component.translatable("message.mcheli.pack.too_large", entry.id(), mb(entry.size()), cap >> 20)); + continue; + } + downloads.put(entry.hash(), new Download(entry)); + request.add(entry.hash()); + bytes += entry.size(); + } + + if (request.isEmpty()) { + apply(); + } else { + say(Component.translatable("message.mcheli.pack.downloading", request.size(), mb(bytes))); + ModNetwork.sendToServer(new PackRequestPacket(request)); + } + } + + @Override + public void onPackData(PackDataPacket packet) { + Download download = downloads.get(packet.hash()); + if (download == null) { + return; + } + if (!download.accept(packet)) { + fail(download, "the server sent a chunk out of order"); + return; + } + if (!download.complete()) { + return; + } + downloads.remove(packet.hash()); + try { + store(download); + downloaded++; + } catch (IOException | PackException e) { + fail(download, e.getMessage()); + return; + } + if (downloads.isEmpty()) { + apply(); + } + } + + @Override + public void onPackSound(PackSoundPacket packet) { + Minecraft.getInstance().getSoundManager().play( + new OneShotSound(packet.sound(), packet.x(), packet.y(), packet.z(), packet.volume(), packet.pitch())); + } + + /** The packs the server sent or named, for the package browser. Empty when hosting the game. */ + public List serverPacks() { + return mounted; + } + + /** Leaving a server: its packs are no longer needed. */ + void loggedOut() { + downloads.clear(); + wanted = List.of(); + RuntimeAssets.setRoots(List.of()); + close(mounted); + mounted = List.of(); + } + + private void fail(Download download, String reason) { + downloads.remove(download.entry.hash()); + MCHeli.LOGGER.error("Content pack {} could not be downloaded: {}", download.entry.id(), reason); + say(Component.translatable("message.mcheli.pack.failed", download.entry.id(), reason)); + if (downloads.isEmpty()) { + apply(); + } + } + + /** Writes a finished download to the cache once its content matches the hash it was listed with. */ + private void store(Download download) throws IOException, PackException { + String hash = download.entry.hash(); + Files.createDirectories(cache); + Path part = cache.resolve(hash + ".download.zip"); + Files.write(part, download.data); + try (ContentPack pack = ContentPack.open(part)) { + if (!pack.hash().equals(hash)) { + throw new PackException("its files do not match the hash the server listed"); + } + } catch (IOException | PackException e) { + Files.deleteIfExists(part); + throw e; + } + Files.move(part, cache.resolve(hash + ".zip"), StandardCopyOption.REPLACE_EXISTING); + } + + /** Rebuilds the content from the import and the server's packs that are now on disk. */ + private void apply() { + List opened = new ArrayList<>(); + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, MCHeli.legacyContent().assets())); + for (PackListPacket.Entry entry : wanted) { + Path file = find(entry); + if (file == null) { + continue; + } + try { + ContentPack pack = ContentPack.open(file); + if (!pack.hash().equals(entry.hash())) { + pack.close(); + MCHeli.LOGGER.warn("Content pack {} changed on disk and no longer matches the server's", file); + continue; + } + opened.add(pack); + sources.add(pack.contentSource()); + if (file.startsWith(cache)) { + Files.setLastModifiedTime(file, FileTime.from(Instant.now())); + } + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not open content pack {}", file, e); + } + } + + ContentRegistry registry = ContentRegistry.load(sources); + MCHeli.content().replaceWith(registry); + List old = mounted; + mounted = List.copyOf(opened); + useRoots(mounted); + close(old); + ClientContentReload.refresh(); + trimCache(); + if (downloaded > 0) { + say(Component.translatable("message.mcheli.pack.ready", downloaded)); + downloaded = 0; + } + } + + /** The pack on disk with the entry's content: one of the player's own packs, or a cached download. */ + private @Nullable Path find(PackListPacket.Entry entry) { + for (ContentPack own : MCHeli.packs().active()) { + if (own.hash().equals(entry.hash())) { + return own.source(); + } + } + Path cached = cache.resolve(entry.hash() + ".zip"); + return Files.isRegularFile(cached) ? cached : null; + } + + /** Deletes the cached packs used longest ago until the cache fits its cap. */ + private void trimCache() { + Set keep = new HashSet<>(); + for (PackListPacket.Entry entry : wanted) { + keep.add(entry.hash() + ".zip"); + } + List files; + try (Stream list = Files.list(cache)) { + files = list.filter(p -> CACHED.matcher(p.getFileName().toString()).matches()) + .sorted(Comparator.comparing(ClientPacks::lastUsed)) + .toList(); + } catch (IOException e) { + return; + } + long total = 0; + for (Path file : files) { + total += size(file); + } + long cap = ClientConfig.packCacheBytes(); + for (Path file : files) { + if (total <= cap) { + break; + } + if (keep.contains(file.getFileName().toString())) { + continue; + } + try { + long size = size(file); + Files.delete(file); + total -= size; + MCHeli.LOGGER.info("Removed {} from the content pack cache", file.getFileName()); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not remove {} from the content pack cache", file, e); + } + } + } + + private static FileTime lastUsed(Path file) { + try { + return Files.getLastModifiedTime(file); + } catch (IOException e) { + return FileTime.fromMillis(0); + } + } + + private static long size(Path file) { + try { + return Files.size(file); + } catch (IOException e) { + return 0; + } + } + + /** The pack loaded last overrides the others, so it is searched first. */ + private static void useRoots(List packs) { + List roots = new ArrayList<>(); + for (ContentPack pack : packs) { + roots.addFirst(pack.assets()); + } + RuntimeAssets.setRoots(roots); + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } + + private static void say(Component message) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null) { + mc.player.displayClientMessage(message, false); + } else { + MCHeli.LOGGER.info(message.getString()); + } + } + + private static String mb(long bytes) { + return "%.1f".formatted(bytes / 1048576.0); + } + + /** One pack arriving from the server. */ + private static final class Download { + private final PackListPacket.Entry entry; + private byte @Nullable [] data; + private int received; + + Download(PackListPacket.Entry entry) { + this.entry = entry; + } + + boolean accept(PackDataPacket packet) { + if (packet.total() != entry.size() || packet.offset() != received + || received + packet.data().length > entry.size()) { + return false; + } + if (data == null) { + data = new byte[entry.size()]; + } + System.arraycopy(packet.data(), 0, data, received, packet.data().length); + received += packet.data().length; + return true; + } + + boolean complete() { + return data != null && received == data.length; + } + } +} From 68bf2cd79f555f1ba02266dafff2a19ea3933f7d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:37 +0800 Subject: [PATCH 0674/1461] Add client/CrewController.java to the 1.21.11 shared sources --- .../mcheli/client/CrewController.java | 482 ++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/CrewController.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/CrewController.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/CrewController.java new file mode 100644 index 00000000..32af6770 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/CrewController.java @@ -0,0 +1,482 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.CameraPosition; +import com.jnhprojects.mcheli.content.aircraft.SeatDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PilotStatePacket; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Input; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +/** + * Turns local player input into vehicle control, for whichever seat the player occupies. + * + *

Keys are sampled once per client tick. The mouse is handled every frame, the way the + * original did it: for helicopters and planes, vanilla first turns the player's view, then the + * change is fed to the aircraft and the view is snapped back onto the aircraft's nose. Everyone + * else looks around freely, within the limits of their seat, and turrets follow their view. + * + *

Gunner seats and camera positions move the first person eye to the point the definition + * gives, which is how the original's camera entity behaved. Every other seat keeps its eye above + * the seat along the hull's up axis, so it tilts with the vehicle. + */ +final class CrewController { + /** Two presses of jump within this many ticks eject the player. */ + private static final int EJECT_WINDOW = 10; + + private static boolean jumpWasDown; + private static int lastJumpPress = -100; + private static int ticks; + private static boolean tracking; + private static float lastYaw; + private static float lastPitch; + + private static @Nullable AircraftEntity viewVehicle; + private static int viewSeat = -1; + private static int cameraIndex; + private static boolean gunnerSight; + + private static @Nullable MethodHandle setCameraPosition; + private static boolean cameraLookupFailed; + + private CrewController() { + } + + /** + * Like the original, the first person hand stays hidden in a vehicle: always for the pilot, + * and for anyone else who looks through a camera or has a weapon to aim. + */ + static boolean hidesHand() { + LocalPlayer player = Minecraft.getInstance().player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + return false; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return false; + } + VehicleWeapons weapons = vehicle.weapons(); + boolean armed = weapons != null && weapons.selected(seat) >= 0; + return seat == 0 || armed || tvCamera(vehicle, seat) != null || eyePosition(vehicle, def, seat) != null; + } + + private static @Nullable AircraftEntity vehicleOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof AircraftEntity vehicle && vehicle.definition() != null ? vehicle : null; + } + + static void onClientTick() { + ticks++; + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + jumpWasDown = false; + tracking = false; + viewVehicle = null; + viewSeat = -1; + // The GLTD's sight shares the zoom and camera mode; only leaving it resets them. + if (GltdController.gltdOf(player) == null) { + VehicleCamera.reset(); + } + return; + } + int seat = vehicle.seatOf(player); + if (vehicle != viewVehicle || seat != viewSeat) { + // New vehicle or seat: start from the default view. + viewVehicle = vehicle; + viewSeat = seat; + cameraIndex = 0; + gunnerSight = false; + tracking = false; + VehicleCamera.reset(); + } + + Input keys = player.input.keyPresses; + boolean jumpPressed = keys.jump() && !jumpWasDown; + jumpWasDown = keys.jump(); + boolean pilot = seat == 0 && vehicle.getControllingPassenger() == player; + + if (pilot) { + AircraftEntity.ControlInput input = vehicle.input(); + input.throttleUp = keys.forward(); + input.throttleDown = keys.backward(); + input.left = keys.left(); + input.right = keys.right(); + input.freeLook = isFreeLook(vehicle); + input.brake = vehicle instanceof TankEntity && keys.jump(); + if (input.brake) { + // The hand brake also stops pivot turns once the vehicle has come to rest. + input.throttleUp = false; + input.throttleDown = false; + double dx = vehicle.getX() - vehicle.xo; + double dz = vehicle.getZ() - vehicle.zo; + if (vehicle.visibleThrottle() <= 0.03F && dx * dx + dz * dz < 0.01) { + input.left = false; + input.right = false; + } + } + if (jumpPressed && vehicle instanceof HelicopterEntity heli) { + heli.toggleHovering(); + } + ModNetwork.sendToServer(new PilotStatePacket(vehicle.getId(), vehicle.visibleThrottle(), vehicle.reverse(), + vehicle.roll(), vehicle.isHoveringMode(), input.write())); + } + + // Eject with a double tap, except for helicopter pilots whose jump key toggles hover. + if (jumpPressed && vehicle.canEject(player) && !(pilot && vehicle instanceof HelicopterEntity)) { + if (ticks - lastJumpPress <= EJECT_WINDOW) { + send(vehicle, CrewActionPacket.Action.EJECT); + lastJumpPress = -100; + } else { + lastJumpPress = ticks; + } + } + + while (KeyBindings.SWITCH_SEAT.consumeClick()) { + send(vehicle, CrewActionPacket.Action.SWITCH_SEAT); + } + while (KeyBindings.DROP_CREW.consumeClick()) { + send(vehicle, pilot ? CrewActionPacket.Action.DROP_CREW : CrewActionPacket.Action.JUMP_OUT); + } + while (KeyBindings.SWITCH_CAMERA.consumeClick()) { + switchCamera(vehicle, seat); + } + while (KeyBindings.LANDING_GEAR.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.LANDING_GEAR); + } + } + while (KeyBindings.FOLD.consumeClick()) { + if (looksThroughSight(vehicle, seat)) { + VehicleCamera.onZoomKey(player, vehicle); + } else if (pilot) { + send(vehicle, CrewActionPacket.Action.FOLD); + } + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + // As in the original, the key works the search light in vehicles that have one. + if (vehicle.canSwitchSearchLight(player)) { + send(vehicle, CrewActionPacket.Action.SEARCH_LIGHT); + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 1.0F); + } else { + VehicleCamera.onCameraModeKey(player, vehicle); + } + } + while (KeyBindings.VEHICLE_SCREEN.consumeClick()) { + if (!vehicle.isDestroyed()) { + send(vehicle, CrewActionPacket.Action.VEHICLE_SCREEN); + } + } + while (KeyBindings.VTOL.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.VTOL); + } + } + while (KeyBindings.RACK_LOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_LOAD); + } + } + while (KeyBindings.RACK_UNLOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_UNLOAD); + } + } + } + + /** + * Whether this seat sees through a sight that can zoom: a camera position or gunner sight, the + * pilot in gunner mode, or the pilot of a UAV. The zoom key folds the wings everywhere else. + */ + private static boolean looksThroughSight(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount() || def.cameraZoom <= 1) { + return false; + } + return eyePosition(vehicle, def, seat) != null || vehicle.isGunnerMode() || def.uav; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + /** Pilot cycles the camera positions; gunners that may leave their sight toggle it. */ + private static void switchCamera(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount()) { + return; + } + SeatDefinition s = def.seats.get(seat); + if (seat == 0) { + switchPilotView(vehicle, def); + } else if (s.gunner() && s.switchGunner()) { + gunnerSight = !gunnerSight; + } + } + + /** + * The pilot's switch mode key, after {@code KeySwitchMode}: leave gunner mode, otherwise step + * through the extra camera positions, otherwise enter gunner mode where the vehicle allows it, + * otherwise go to the first extra camera. Unlike the original, leaving gunner mode never + * depends on the plane still meeting the conditions for entering it. + */ + private static void switchPilotView(AircraftEntity vehicle, AircraftDefinition def) { + int cameras = def.cameraPositions.size(); + if (vehicle.isGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + cameraIndex = 0; + } else if (cameraIndex > 0) { + cameraIndex = (cameraIndex + 1) % cameras; + } else if (vehicle.canSwitchGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + } else if (cameras >= 2) { + cameraIndex = 1; + } + } + + /** + * Free look for the pilot. Helicopters and planes look around while the key is held; tanks + * start from their {@code DefaultFreelook} setting and the key swaps it while held. + */ + private static boolean isFreeLook(AircraftEntity vehicle) { + boolean held = KeyBindings.FREE_LOOK.isDown(); + if (vehicle instanceof TankEntity) { + AircraftDefinition def = vehicle.definition(); + return def != null && def.defaultFreelook != held; + } + return held; + } + + static void onCameraAngles(CameraAngles event) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null || mc.isPaused()) { + return; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return; + } + float partialTicks = event.partialTick(); + boolean firstPerson = mc.options.getCameraType().isFirstPerson(); + + if (seat == 0 && vehicle.steersWithMouse() && !vehicle.isGunnerMode() && vehicle.getControllingPassenger() == player) { + steerWithMouse(vehicle, player, event, partialTicks, firstPerson); + } else { + tracking = false; + constrainView(vehicle, def, def.seats.get(seat), seat, player); + } + + TvMissileEntity missile = tvCamera(vehicle, seat); + if (!(firstPerson && missile != null)) { + // Every seat of every kind tilts with the hull, measured along the way the rider looks. + event.setRoll(AircraftRotation.cameraRoll(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), + vehicle.roll(partialTicks), event.yaw(), event.pitch())); + } + if (firstPerson && missile != null) { + placeCamera(event.camera(), missile.getPosition(partialTicks)); + } else if (firstPerson) { + Vector3 eye = eyePosition(vehicle, def, seat); + moveCamera(event.camera(), vehicle, def, eye != null ? eye : seatEye(vehicle, def, seat, player), seat, partialTicks); + } + } + + /** + * The normal eye, in hull space. Vanilla puts it straight above the rider in world space, + * which leaves the head outside the cockpit once the hull banks or pitches; measured along the + * hull's own up axis it stays where the rider model's head is drawn. + */ + private static Vector3 seatEye(AircraftEntity vehicle, AircraftDefinition def, int seat, LocalPlayer player) { + Vector3 pos = def.seats.get(seat).position(); + double up = player.getEyeHeight() - player.getVehicleAttachmentPoint(vehicle).y; + return new Vector3(pos.x(), pos.y() + up, pos.z()); + } + + /** The phase one pilot model: mouse movement becomes aircraft rotation, the view stays on the nose. */ + private static void steerWithMouse(AircraftEntity vehicle, LocalPlayer player, CameraAngles event, + float partialTicks, boolean firstPerson) { + boolean freeLook = KeyBindings.FREE_LOOK.isDown(); + boolean ownPitch = vehicle.isHovering(); + if (!freeLook) { + if (tracking) { + float dYaw = Mth.wrapDegrees(player.getYRot() - lastYaw); + float dPitch = player.getXRot() - lastPitch; + vehicle.applyPilotLook(dYaw, dPitch); + HudRenderer.onPilotLook(dYaw, dPitch); + } + lockView(player, vehicle.getYRot(), vehicle.yRotO, ownPitch ? null : vehicle.getXRot(), vehicle.xRotO); + lastYaw = player.getYRot(); + lastPitch = player.getXRot(); + tracking = true; + } else { + // Re-acquire the nose smoothly once free look is released. + tracking = false; + } + if (firstPerson && !freeLook) { + event.setYaw(vehicle.getYRot(partialTicks)); + if (!ownPitch) { + event.setPitch(vehicle.getXRot(partialTicks)); + } + } + } + + /** Seat limits for everyone who does not fly with the mouse. */ + private static void constrainView(AircraftEntity vehicle, AircraftDefinition def, SeatDefinition seat, int index, LocalPlayer player) { + boolean freeLookHeld = KeyBindings.FREE_LOOK.isDown(); + if (seat.fixedRotation() && !freeLookHeld) { + lockView(player, vehicle.getYRot() + seat.fixedYaw(), vehicle.yRotO + seat.fixedYaw(), + vehicle.getXRot() + seat.fixedPitch(), vehicle.xRotO + seat.fixedPitch()); + return; + } + boolean pilot = index == 0; + if (pilot && vehicle instanceof TankEntity && !isFreeLook(vehicle)) { + lockView(player, vehicle.getYRot(), vehicle.yRotO, vehicle.getXRot(), vehicle.xRotO); + return; + } + float min; + float max; + if (pilot && (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity)) { + min = def.minRotationPitch; + max = def.maxRotationPitch; + } else if (seat.gunner() || (pilot && vehicle.isGunnerMode())) { + min = seat.minPitch(); + max = seat.maxPitch(); + } else { + return; + } + // Limits are relative to the hull's tilt along the view direction. + double rel = Math.toRadians(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot())); + float tilt = (float) (vehicle.getXRot() * Math.cos(rel) + vehicle.roll() * Math.sin(rel)); + float pitch = Mth.clamp(player.getXRot(), tilt + min, tilt + max); + pitch = Mth.clamp(pitch, -90.0F, 90.0F); + if (pitch != player.getXRot()) { + player.setXRot(pitch); + player.xRotO = pitch; + } + } + + private static void lockView(LocalPlayer player, float yaw, float yawO, @Nullable Float pitch, float pitchO) { + player.setYRot(yaw); + player.yRotO = yawO; + player.setYHeadRot(yaw); + player.setYBodyRot(yaw); + if (pitch != null) { + player.setXRot(pitch); + player.xRotO = pitchO; + } + } + + /** + * Where the first person eye goes, in hull space, or {@code null} for the normal eye. Mirrors + * the original's choice between the pilot's camera positions and a gunner seat's sight. + */ + private static @Nullable Vector3 eyePosition(AircraftEntity vehicle, AircraftDefinition def, int seat) { + SeatDefinition s = def.seats.get(seat); + boolean sight = s.gunner() && (!s.switchGunner() || gunnerSight); + CameraPosition camera = null; + if (seat == 0) { + if (cameraIndex > 0 && cameraIndex < def.cameraPositions.size()) { + camera = def.cameraPositions.get(cameraIndex); + } else if (def.uav || def.alwaysCameraView || sight || vehicle.isGunnerMode() || vehicle instanceof GroundVehicleEntity) { + camera = def.cameraPositions.getFirst(); + } + } else if (sight) { + camera = s.camera() != null && s.camera() != CameraPosition.DEFAULT ? s.camera() : def.cameraPositions.getFirst(); + } + // A camera at the origin means the file never set one; the normal eye is better there. + if (camera == null || camera.position().equals(Vector3.ZERO)) { + return null; + } + return camera.position(); + } + + private static void moveCamera(Camera camera, AircraftEntity vehicle, AircraftDefinition def, Vector3 eye, int seat, float partialTicks) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + SeatDefinition s = def.seats.get(seat); + Vector3 local = s.rotatingSeat() ? vehicle.onTurret(def, eye, partialTicks) : eye; + Vector3f offset = AircraftRotation.toWorld(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), vehicle.roll(partialTicks), + local.x(), local.y(), local.z()); + placeCamera(camera, new Vec3(Mth.lerp(partialTicks, vehicle.xo, vehicle.getX()) + offset.x, + Mth.lerp(partialTicks, vehicle.yo, vehicle.getY()) + offset.y, + Mth.lerp(partialTicks, vehicle.zo, vehicle.getZ()) + offset.z)); + } + + /** + * The TV missile this seat is flying, whose nose camera replaces the eye. Like the original, + * only while the seat has a TV missile selected in its guided mode. + */ + static @Nullable TvMissileEntity tvCamera(AircraftEntity vehicle, int seat) { + TvMissileEntity missile = vehicle.tvMissile(); + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + if (missile == null || set == null || set.definition().type != WeaponType.TV_MISSILE || set.mode() != 0) { + return null; + } + return missile; + } + + static void placeCamera(Camera camera, Vec3 at) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + try { + setter.invoke(camera, at); + } catch (Throwable t) { + MCHeli.LOGGER.warn("Could not move the camera; the first person eye stays where vanilla puts it", t); + setCameraPosition = null; + cameraLookupFailed = true; + } + } + + private static Method accessible(Method method) { + method.setAccessible(true); + return method; + } + + /** {@code Camera.setPosition} is protected. Looked up once; on failure the feature turns itself off. */ + private static @Nullable MethodHandle cameraSetter() { + if (setCameraPosition == null && !cameraLookupFailed) { + try { + setCameraPosition = MethodHandles.lookup().unreflect( + accessible(Camera.class.getDeclaredMethod("setPosition", Vec3.class))); + } catch (Exception e) { + MCHeli.LOGGER.warn("Camera.setPosition is not accessible; the first person eye stays where vanilla puts it", e); + cameraLookupFailed = true; + } + } + return setCameraPosition; + } +} From cb0bcd01c6737d1884a6440686406f22a5a5c9e2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:37 +0800 Subject: [PATCH 0675/1461] Add client/GltdController.java to the 1.21.11 shared sources --- .../mcheli/client/GltdController.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/GltdController.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/GltdController.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/GltdController.java new file mode 100644 index 00000000..a8db0832 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/GltdController.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.network.EquipmentActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +/** + * The local player at a GLTD, {@code MCH_ClientGLTDTickHandler} and {@code MCH_GuiGLTD}: the + * view moves to the sight, the zoom and camera mode keys work as in a vehicle's sight, the use + * key calls the A-10 on the spot under the cross, and a small readout says when it can be called + * again. + */ +final class GltdController { + /** The original zoomed smoothly up to ten; the vehicle sights double each press. */ + private static final int MAX_ZOOM = 8; + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int CROSS = 0xC0FF4040; + + /** Edge of the use key, so holding it calls one strike rather than one per tick. */ + private static boolean useWasDown; + + private GltdController() { + } + + static @Nullable GltdEntity gltdOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof GltdEntity gltd ? gltd : null; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + GltdEntity gltd = gltdOf(player); + if (gltd == null) { + useWasDown = false; + return; + } + while (KeyBindings.FOLD.consumeClick()) { + VehicleCamera.zoomStep(player, MAX_ZOOM); + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + VehicleCamera.nextMode(player, on -> send(gltd, + on ? EquipmentActionPacket.Action.NIGHT_VISION_ON : EquipmentActionPacket.Action.NIGHT_VISION_OFF)); + } + boolean useDown = mc.screen == null && mc.options.keyUse.isDown(); + if (useDown && !useWasDown) { + send(gltd, EquipmentActionPacket.Action.CALL_STRIKE); + } + useWasDown = useDown; + } + + /** The use key belongs to the sight while sitting at a GLTD. */ + static boolean onUseItem() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + /** First person looks out of the sight rather than from behind the tripod. */ + static void onCameraAngles(CameraAngles angles) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd != null && mc.options.getCameraType().isFirstPerson()) { + CrewController.placeCamera(angles.camera(), gltd.sightPosition(angles.partialTick())); + } + } + + static boolean hidesHand() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + static void drawHud(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd == null || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int cx = g.guiWidth() / 2; + int cy = g.guiHeight() / 2; + g.fill(cx - 12, cy, cx - 3, cy + 1, CROSS); + g.fill(cx + 4, cy, cx + 13, cy + 1, CROSS); + g.fill(cx, cy - 12, cx + 1, cy - 3, CROSS); + g.fill(cx, cy + 4, cx + 1, cy + 13, CROSS); + + Font font = mc.font; + int x = cx + 20; + int y = cy + 20; + int wait = gltd.waitTicks(); + if (wait > 0) { + g.drawString(font, Component.translatable("hud.mcheli.gltd.wait", (wait + 19) / 20), x, y, AMBER); + } else { + g.drawString(font, Component.translatable("hud.mcheli.gltd.ready"), x, y, WHITE); + } + g.drawString(font, Component.translatable("hud.mcheli.zoom", Math.round(VehicleCamera.zoom())), x, y + 10, WHITE); + } + + private static void send(GltdEntity gltd, EquipmentActionPacket.Action action) { + ModNetwork.sendToServer(new EquipmentActionPacket(gltd.getId(), action)); + } +} From 5635ad1dff626885d40ab4b69e1fd23e4462ae58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:38 +0800 Subject: [PATCH 0676/1461] Add client/KeyBindings.java to the 1.21.11 shared sources --- .../mcheli/client/KeyBindings.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java new file mode 100644 index 00000000..111759ac --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.KeyMapping; +import net.minecraft.resources.Identifier; + +import java.util.List; + +/** + * Mod key bindings. Throttle, steering, hover, brake and eject reuse the vanilla movement and jump + * keys (W/S, A/D and Space by default), which matches the original defaults. Firing uses the + * vanilla use key and the pick block button switches weapons, the right and middle mouse buttons + * of the original. The rest keep the original's letters where vanilla leaves them free. + */ +public final class KeyBindings { + public static final KeyMapping.Category CATEGORY = KeyMapping.Category.register(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "main")); + + /** Hold to look around without steering. The original used Left Ctrl, which now clashes with sprint. */ + public static final KeyMapping FREE_LOOK = new KeyMapping("key.mcheli.free_look", InputConstants.KEY_LALT, CATEGORY); + /** Cycles the pilot's camera positions, or toggles the gunner sight in seats that allow it. */ + public static final KeyMapping SWITCH_CAMERA = new KeyMapping("key.mcheli.switch_camera", InputConstants.KEY_H, CATEGORY); + public static final KeyMapping LANDING_GEAR = new KeyMapping("key.mcheli.landing_gear", InputConstants.KEY_B, CATEGORY); + /** Zooms through a sight, as the original Z key did; otherwise folds hatches and wings, sweeps variable geometry wings. */ + public static final KeyMapping FOLD = new KeyMapping("key.mcheli.fold", InputConstants.KEY_Z, CATEGORY); + /** The original used F, which vanilla now uses to swap hands. */ + public static final KeyMapping VTOL = new KeyMapping("key.mcheli.vtol", InputConstants.KEY_V, CATEGORY); + public static final KeyMapping SWITCH_SEAT = new KeyMapping("key.mcheli.switch_seat", InputConstants.KEY_R, CATEGORY); + public static final KeyMapping RACK_LOAD = new KeyMapping("key.mcheli.rack_load", InputConstants.KEY_J, CATEGORY); + public static final KeyMapping RACK_UNLOAD = new KeyMapping("key.mcheli.rack_unload", InputConstants.KEY_U, CATEGORY); + /** The second weapon switch key of the original; the first is the vanilla pick block button. */ + public static final KeyMapping SWITCH_WEAPON = new KeyMapping("key.mcheli.switch_weapon", InputConstants.KEY_G, CATEGORY); + public static final KeyMapping WEAPON_MODE = new KeyMapping("key.mcheli.weapon_mode", InputConstants.KEY_X, CATEGORY); + /** The original used V, which VTOL took over. */ + public static final KeyMapping FLARE = new KeyMapping("key.mcheli.flare", InputConstants.KEY_C, CATEGORY); + /** Fuel and parachute slots, reloading and the cargo chest. The original used R, which switches seats here. */ + public static final KeyMapping VEHICLE_SCREEN = new KeyMapping("key.mcheli.vehicle_screen", InputConstants.KEY_I, CATEGORY); + /** Night vision, thermal view and back in vehicles with {@code EnableNightVision}. The original used C, now the flare key. */ + public static final KeyMapping CAMERA_MODE = new KeyMapping("key.mcheli.camera_mode", InputConstants.KEY_K, CATEGORY); + /** The pilot lets the troops down; anyone else aboard slides down a rope or jumps. The original used Y for the pilot's part. */ + public static final KeyMapping DROP_CREW = new KeyMapping("key.mcheli.drop_crew", InputConstants.KEY_Y, CATEGORY); + /** Operators open the team manager, as the original's multiplayer manager key M did. */ + public static final KeyMapping TEAM_MANAGER = new KeyMapping("key.mcheli.team_manager", InputConstants.KEY_M, CATEGORY); + + public static final List ALL = List.of(FREE_LOOK, SWITCH_CAMERA, LANDING_GEAR, FOLD, VTOL, SWITCH_SEAT, RACK_LOAD, RACK_UNLOAD, + SWITCH_WEAPON, WEAPON_MODE, FLARE, VEHICLE_SCREEN, CAMERA_MODE, DROP_CREW, TEAM_MANAGER); + + private KeyBindings() { + } +} From 15b17e3442b99046d70b4e5e2cd1b54b053d8567 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:38 +0800 Subject: [PATCH 0677/1461] Add client/LauncherController.java to the 1.21.11 shared sources --- .../mcheli/client/LauncherController.java | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java new file mode 100644 index 00000000..f502e689 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java @@ -0,0 +1,238 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.network.LauncherPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The Stinger and Javelin in the player's hands, {@code MCH_ClientLightWeaponTickHandler} and + * {@code MCH_GuiLightWeapon}. Raised, the launcher's sight covers the view and its guidance locks + * onto what the cross holds; the attack key fires once the lock is complete. Z steps the zoom, X + * the Javelin's direct and top attack modes. An empty launcher held down reloads itself from a + * missile in the inventory. + */ +final class LauncherController { + /** Ticks of raising before the sight and the lock start, as in the original. */ + private static final int RAISE_TICKS = 10; + private static final int RELOAD_TICKS = 40; + /** Top attack climbs first and needs this much room, measured flat. */ + private static final double TOP_ATTACK_MIN_DISTANCE = 40.0; + private static final int GREEN = 0xFF2AD848; + private static final int RED = 0xFFDF0808; + private static final int WHITE = 0xFFFFFFFF; + + private static @Nullable String weapon; + private static @Nullable Guidance guidance; + private static int zoomIndex; + private static int mode; + private static int reloadTicks; + private static int lockSoundTicks; + private static boolean attackWasDown; + + private LauncherController() { + } + + private static ItemStack launcher(@Nullable LocalPlayer player) { + if (player == null || !LauncherItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getItemInHand(InteractionHand.MAIN_HAND); + return stack.getItem() instanceof LauncherItem ? stack : ItemStack.EMPTY; + } + + /** Whether the player holds a launcher up to the eye. */ + static boolean isRaised() { + LocalPlayer player = Minecraft.getInstance().player; + ItemStack stack = launcher(player); + return !stack.isEmpty() && player.isUsingItem() && player.getUseItem() == stack && player.getTicksUsingItem() > RAISE_TICKS; + } + + private static @Nullable WeaponDefinition definition(ItemStack stack) { + return MCHeli.content().weapon(((LauncherItem) stack.getItem()).weapon()); + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = launcher(player); + WeaponDefinition def = stack.isEmpty() ? null : definition(stack); + if (def == null) { + weapon = null; + guidance = null; + reloadTicks = 0; + attackWasDown = false; + return; + } + String name = ((LauncherItem) stack.getItem()).weapon(); + if (!name.equals(weapon) || guidance == null) { + weapon = name; + boolean air = def.type != WeaponType.AT_MISSILE; + guidance = new Guidance(air, !air, def.ridableOnly, def.lockTime, e -> e != player && !e.isAlliedTo(player)); + zoomIndex = 0; + mode = 0; + } + boolean raised = isRaised(); + boolean attackDown = raised && mc.screen == null && mc.options.keyAttack.isDown(); + boolean attackPressed = attackDown && !attackWasDown; + attackWasDown = attackDown; + + if (LauncherItem.isLoaded(stack)) { + reloadTicks = 0; + if (raised) { + if (guidance.lock(player, player.getEyePosition())) { + lockSoundTicks = 0; + } + playLockSound(player); + } else { + guidance.clear(); + } + } else { + guidance.clear(); + reload(player, stack); + } + guidance.tick(); + + if (!raised) { + return; + } + while (KeyBindings.FOLD.consumeClick()) { + if (def.zoom.length > 1) { + zoomIndex = (zoomIndex + 1) % def.zoom.length; + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + if (def.modeNum > 1) { + mode = (mode + 1) % def.modeNum; + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 0.9F); + } + } + if (attackPressed) { + Entity target = guidance.lastLocked(); + if (LauncherItem.isLoaded(stack) && guidance.isLocked() && target != null && canFire(player, target)) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.FIRE, target.getId(), mode)); + guidance.clear(); + } else { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + } + + /** Top attack needs room to climb and dive. */ + private static boolean canFire(LocalPlayer player, @Nullable Entity target) { + if (mode == 0 || target == null) { + return true; + } + double dx = target.getX() - player.getX(); + double dz = target.getZ() - player.getZ(); + return Math.sqrt(dx * dx + dz * dz) >= TOP_ATTACK_MIN_DISTANCE; + } + + /** The tone quickens as the lock builds. */ + private static void playLockSound(LocalPlayer player) { + if (guidance == null || guidance.lockCount() <= 0) { + lockSoundTicks = 0; + return; + } + if (lockSoundTicks > 0) { + lockSoundTicks--; + return; + } + lockSoundTicks = Math.max(2, (int) (7 * (1.0 - (double) guidance.lockCount() / guidance.lockCountMax()))); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + + /** Held down and empty, with a missile to hand: two seconds to load it. */ + private static void reload(LocalPlayer player, ItemStack stack) { + LauncherItem item = (LauncherItem) stack.getItem(); + boolean hasMissile = player.getAbilities().instabuild || player.getInventory().findSlotMatchingItem(new ItemStack(item.missile())) >= 0; + if (!hasMissile || player.isUsingItem()) { + reloadTicks = 0; + return; + } + reloadTicks++; + if (reloadTicks == 10) { + player.level().playLocalSound(player, ModSounds.get("fim92_reload"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (reloadTicks == RELOAD_TICKS) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.RELOAD, 0, 0)); + } + } + + static float fov(float fov) { + if (!isRaised()) { + return fov; + } + WeaponDefinition def = definition(launcher(Minecraft.getInstance().player)); + if (def == null || def.zoom.length == 0) { + return fov; + } + float zoom = def.zoom[Math.min(zoomIndex, def.zoom.length - 1)]; + return zoom > 1.0F ? fov / zoom : fov; + } + + static void drawHud(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (!mc.options.getCameraType().isFirstPerson()) { + return; + } + ItemStack stack = launcher(player); + if (stack.isEmpty()) { + return; + } + Font font = mc.font; + if (!isRaised()) { + if (!LauncherItem.isLoaded(stack) && reloadTicks > 0) { + g.drawString(font, Component.translatable("hud.mcheli.launcher.reloading"), g.guiWidth() / 2 - 30, g.guiHeight() / 2 + 20, WHITE); + } + return; + } + WeaponDefinition def = definition(stack); + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + String texture = def != null && def.type == WeaponType.AT_MISSILE ? "javelin" : "stinger"; + g.blit(RenderPipelines.GUI_TEXTURED, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + texture + ".png"), + (w - size) / 2, (h - size) / 2, 0.0F, 0.0F, size, size, 256, 256, 256, 256); + + int x = w / 2 + 20; + int y = h / 2 + 20; + if (!LauncherItem.isLoaded(stack)) { + g.drawString(font, Component.translatable("hud.mcheli.launcher.empty"), x, y, RED); + } else if (guidance != null && guidance.isLocked()) { + Entity target = guidance.lastLocked(); + g.drawString(font, Component.translatable("hud.mcheli.locked"), x, y, canFire(player, target) ? GREEN : RED); + if (target != null) { + g.drawString(font, String.format("[%.0f m]", target.distanceTo(player)), x, y + 10, GREEN); + } + } else if (guidance != null && guidance.lockCount() > 0) { + g.drawString(font, Component.translatable("hud.mcheli.locking", guidance.lockCount() * 100 / Math.max(guidance.lockCountMax(), 1)), x, y, WHITE); + } + if (def != null && def.modeNum > 1) { + g.drawString(font, Component.translatable(mode == 0 ? "hud.mcheli.launcher.direct" : "hud.mcheli.launcher.top_attack"), x, y + 20, WHITE); + } + if (def != null && def.zoom.length > 0) { + g.drawString(font, Component.translatable("hud.mcheli.zoom", String.format("%.1f", def.zoom[Math.min(zoomIndex, def.zoom.length - 1)])), + x, y + 30, WHITE); + } + } +} From 9850bc6b3cf555b8968cf4991ebf02839748a548 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:39 +0800 Subject: [PATCH 0678/1461] Add client/RangefinderController.java to the 1.21.11 shared sources --- .../mcheli/client/RangefinderController.java | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java new file mode 100644 index 00000000..d1eb31d9 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java @@ -0,0 +1,155 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.SpotPacket; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.Locale; + +/** + * Looking through the rangefinder, {@code MCH_ClientToolTickHandler} and {@code MCH_GuiRangeFinder}. + * Once it has been held up for a moment the scope covers the view and reads the range; the zoom + * key and the weapon mode key zoom in and out while held, the flare key picks what to spot, and + * the attack key spots. + */ +final class RangefinderController { + private static final Identifier SCOPE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/rangefinder.png"); + /** Ticks of holding it up before the scope shows, as in the original. */ + private static final int RAISE_TICKS = 8; + private static final int COOLDOWN = 60; + private static final float MIN_ZOOM = 1.2F; + private static final float MAX_ZOOM = 10.0F; + private static final double RANGE = 300.0; + private static final int GREEN = 0xFF10FF20; + private static final int RED = 0xFFFF1010; + private static final int WHITE = 0xFFFFFFFF; + private static final int GREY = 0xFF404040; + + private static float zoom = 2.0F; + private static SpotPacket.Mode mode = SpotPacket.Mode.PLAYERS_AND_VEHICLES; + private static int cooldown; + + private RangefinderController() { + } + + /** The rangefinder the player is looking through, or empty. */ + private static ItemStack scope(LocalPlayer player) { + if (player == null || !player.isUsingItem() || player.getTicksUsingItem() <= RAISE_TICKS || !RangefinderItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getUseItem(); + return stack.getItem() instanceof RangefinderItem ? stack : ItemStack.EMPTY; + } + + static boolean isScoped() { + return !scope(Minecraft.getInstance().player).isEmpty(); + } + + static void onClientTick() { + if (cooldown > 0) { + cooldown--; + } + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty()) { + zoom = 2.0F; + return; + } + if (KeyBindings.FOLD.isDown() && zoom < MAX_ZOOM) { + zoom = Math.min(zoom + zoom / 10.0F, MAX_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 1.0F); + } + if (KeyBindings.WEAPON_MODE.isDown() && zoom > MIN_ZOOM) { + zoom = Math.max(zoom - zoom / 10.0F, MIN_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 0.9F); + } + while (KeyBindings.FLARE.consumeClick()) { + mode = SpotPacket.Mode.values()[(mode.ordinal() + 1) % SpotPacket.Mode.values().length]; + // Alone in a world there is no one else to spot, as the original reasoned. + boolean alone = mc.hasSingleplayerServer() && !mc.getSingleplayerServer().isPublished(); + if (alone && mode == SpotPacket.Mode.PLAYERS_AND_VEHICLES) { + mode = SpotPacket.Mode.MOBS; + } + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 0.9F); + } + if (mc.options.keyAttack.isDown() && cooldown == 0) { + if (mode != SpotPacket.Mode.MARK_POINT && RangefinderItem.charges(stack) <= 0) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 1.0F, 1.0F); + } else { + cooldown = COOLDOWN; + ModNetwork.sendToServer(new SpotPacket(mode)); + } + } + } + + static float fov(float fov) { + return isScoped() ? fov / zoom : fov; + } + + static void drawHud(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty() || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + g.blit(RenderPipelines.GUI_TEXTURED, SCOPE, (w - size) / 2, (h - size) / 2, 0.0F, 0.0F, size, size, 256, 256, 256, 256); + + // The readouts sit on the scope texture's own labels: lay them out in its 512 wide space, + // centred, as the original did, and scale that with the scope. + float k = size / 512.0F; + g.pose().pushMatrix(); + g.pose().translate(w / 2.0F, h / 2.0F); + g.pose().scale(k, k); + drawReadouts(g, mc.font, player, stack); + g.pose().popMatrix(); + } + + /** In scope space: the centre of the view is at 0, 0. */ + private static void drawReadouts(GuiGraphics g, Font font, LocalPlayer player, ItemStack stack) { + int charge = RangefinderItem.charges(stack) * 100 / RangefinderItem.CHARGES; + // Over BATT. + g.drawString(font, String.format("%3d%%", charge), -100, 55, charge > 0 ? GREEN : RED); + if (charge <= 0) { + g.drawString(font, Component.translatable("hud.mcheli.rangefinder.empty"), -40, 55, RED); + } else { + // Over RANGE METERS. + Vec3 eye = player.getEyePosition(); + HitResult hit = player.level().clip(new ClipContext(eye, eye.add(player.getLookAngle().scale(RANGE)), + ClipContext.Block.OUTLINE, ClipContext.Fluid.ANY, player)); + String range = hit.getType() == HitResult.Type.MISS ? "----" : String.format("%4d", Math.round(hit.getLocation().distanceTo(eye))); + g.drawString(font, range, 40, 55, hit.getType() == HitResult.Type.MISS ? RED : GREEN); + } + // The spot cooldown bar, and the zoom above it. + g.fill(-100, 51, -70, 53, GREEN); + g.fill(-100, 51, -100 + cooldown / 2, 53, RED); + g.drawString(font, String.format("x%.1f", zoom), -100, 31, WHITE); + int y = 21; + for (SpotPacket.Mode m : SpotPacket.Mode.values()) { + if (m == mode) { + g.drawString(font, ">", 30, y, WHITE); + } + g.drawString(font, Component.translatable("hud.mcheli.rangefinder." + m.name().toLowerCase(Locale.ROOT)), 40, y, m == mode ? WHITE : GREY); + y += 10; + } + } +} From 82ee888c5d3357e27478e914dda2ed07e4bf585b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:39 +0800 Subject: [PATCH 0679/1461] Add client/RuntimeAssets.java to the 1.21.11 shared sources --- .../mcheli/client/RuntimeAssets.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java new file mode 100644 index 00000000..7f7afd84 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java @@ -0,0 +1,165 @@ +package com.jnhprojects.mcheli.client; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Vehicle textures, models and sounds that can change while the game runs, without a resource + * reload (F3 + T), which would stall the game for every pack loaded and is not available on a + * server at all. + * + *

Lookups try the resource manager first, so resource packs keep the last word, then the + * runtime roots: folders laid out like {@code assets/mcheli} that content can add at any time. + * Content packs and, later, the workshop register their folders here. + *

    + *
  • Textures found only in a runtime root are uploaded as a {@link DynamicTexture} the first + * time a vehicle draws them. {@link #refreshTextures} uploads every vehicle texture again + * from disk, which is how a content reload shows edited textures.
  • + *
  • Models are read through {@link #open} by the mesh cache, which a reload clears.
  • + *
  • Sounds found only in a runtime root are streamed from the file + * ({@link #soundFile}); see {@code RuntimeSoundInstance}.
  • + *
+ */ +public final class RuntimeAssets { + private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeAssets.class); + + /** Searched in order, so the pack loaded last comes first. Replaced as a whole. */ + private static volatile List roots = List.of(); + /** The pack open in the workshop, searched first whether the game has loaded it or not. */ + private static volatile @Nullable Path workshopRoot; + /** Every vehicle texture asked for, and whether we uploaded it ourselves. */ + private static final Map TEXTURES = new ConcurrentHashMap<>(); + + private RuntimeAssets() { + } + + /** + * Sets the folders laid out like {@code assets/mcheli} that are searched after the resource + * packs, the first one first. Content packs set theirs whenever the packs in use change. + */ + public static void setRoots(List folders) { + roots = List.copyOf(folders); + } + + /** Sets the {@code assets/mcheli} folder of the pack open in the workshop, or {@code null} for none. */ + public static void setWorkshopRoot(@Nullable Path folder) { + workshopRoot = folder; + } + + /** Opens an {@code mcheli} asset from the resource packs, or else from a runtime root. */ + public static @Nullable InputStream open(Identifier id) throws IOException { + Optional resource = Minecraft.getInstance().getResourceManager().getResource(id); + if (resource.isPresent()) { + return resource.get().open(); + } + Path file = find(id.getPath()); + return file != null ? Files.newInputStream(file) : null; + } + + /** + * A texture ready to bind. The first request for a texture no resource pack has, but a runtime + * root does, uploads it. Cheap after the first call, so renderers can call it every frame. + */ + public static Identifier texture(Identifier id) { + TEXTURES.computeIfAbsent(id, key -> { + if (Minecraft.getInstance().getResourceManager().getResource(key).isPresent()) { + return false; + } + return upload(key); + }); + return id; + } + + /** + * Uploads every texture vehicles have drawn so far again, read fresh from disk. Resource pack + * textures are replaced too, until the next resource reload loads them the normal way. One that + * no longer exists anywhere, because its pack is gone, is released. + */ + public static void refreshTextures() { + for (Map.Entry entry : TEXTURES.entrySet()) { + boolean ours = upload(entry.getKey()); + if (!ours && entry.getValue()) { + Minecraft.getInstance().getTextureManager().release(entry.getKey()); + } + entry.setValue(ours); + } + } + + /** Uploads one texture again, read fresh from disk: the workshop just replaced its file. */ + public static void refreshTexture(Identifier id) { + TEXTURES.put(id, upload(id)); + } + + private static boolean upload(Identifier id) { + try (InputStream in = open(id)) { + if (in == null) { + return false; + } + NativeImage image = NativeImage.read(in); + Minecraft.getInstance().getTextureManager().register(id, new DynamicTexture(id::toString, image)); + return true; + } catch (IOException e) { + LOGGER.warn("Could not load texture {}", id, e); + return false; + } + } + + /** + * Drops the textures we uploaded, so the texture manager loads them again the normal way. + * Called on resource reload. + */ + public static void clearTextures() { + TEXTURES.forEach((id, ours) -> { + if (ours) { + Minecraft.getInstance().getTextureManager().release(id); + } + }); + TEXTURES.clear(); + } + + /** + * The OGG file for a sound event of ours that no resource pack defines, such as {@code heli} + * from {@code sounds/heli.ogg} in a runtime root. Numbered variants ({@code heli1.ogg}) count. + */ + public static @Nullable Path soundFile(String event) { + Path file = find("sounds/" + event + ".ogg"); + return file != null ? file : find("sounds/" + event + "1.ogg"); + } + + private static @Nullable Path find(String path) { + Path workshop = workshopRoot; + if (workshop != null) { + Path file = inRoot(workshop, path); + if (file != null) { + return file; + } + } + for (Path root : roots) { + Path file = inRoot(root, path); + if (file != null) { + return file; + } + } + return null; + } + + private static @Nullable Path inRoot(Path root, String path) { + Path file = root.resolve(path).normalize(); + return file.startsWith(root) && Files.isRegularFile(file) ? file : null; + } +} From 077c8de4b24d350c94b1c81d24619831eb4594bb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:40 +0800 Subject: [PATCH 0680/1461] Add client/TargetMarkers.java to the 1.21.11 shared sources --- .../mcheli/client/TargetMarkers.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java new file mode 100644 index 00000000..2578a666 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java @@ -0,0 +1,87 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3fc; +import org.jspecify.annotations.Nullable; + +import java.util.Map; + +/** + * {@code MCH_GuiTargetMarker}: the team's spotted targets and marked point on screen. A target + * gets a triangle over its head with its distance; one off screen gets a diamond at the edge on + * its side. The marked point gets a diamond with its distance and coordinates. + */ +final class TargetMarkers { + private static final int TARGET = 0xFFFF4040; + private static final int POINT = 0xFFFFD000; + private static final int EDGE = 12; + + private TargetMarkers() { + } + + static void tick() { + Markers.tick(); + } + + static void draw(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || mc.player == null) { + return; + } + float partialTicks = delta.getGameTimeDeltaPartialTick(false); + for (Map.Entry spotted : Markers.spotted().entrySet()) { + Entity entity = mc.level.getEntity(spotted.getKey()); + if (entity == null || entity == mc.player || !entity.isAlive()) { + continue; + } + Vec3 top = entity.getPosition(partialTicks).add(0.0, entity.getBbHeight() + 0.5, 0.0); + mark(g, mc, top, "â–¼", TARGET, "%.0fm".formatted(mc.player.distanceTo(entity)), null); + } + BlockPos point = Markers.point(); + if (point != null) { + Vec3 at = Vec3.atCenterOf(point).add(0.0, 1.0, 0.0); + String coordinates = "%d %d %d".formatted(point.getX(), point.getY(), point.getZ()); + mark(g, mc, at, "â—†", POINT, "%.0fm".formatted(Math.sqrt(mc.player.distanceToSqr(at))), coordinates); + } + } + + private static void mark(GuiGraphics g, Minecraft mc, Vec3 at, String symbol, int color, String distance, @Nullable String extra) { + Camera camera = mc.gameRenderer.getMainCamera(); + Vector3fc forward = camera.forwardVector(); + Vec3 offset = at.subtract(camera.position()); + boolean ahead = offset.x * forward.x() + offset.y * forward.y() + offset.z * forward.z() > 0.0; + Vec3 ndc = mc.gameRenderer.projectPointToScreen(at); + int w = g.guiWidth(); + int h = g.guiHeight(); + double x = (ndc.x + 1.0) / 2.0 * w; + double y = (1.0 - ndc.y) / 2.0 * h; + Font font = mc.font; + if (ahead && x >= 0 && x <= w && y >= 0 && y <= h) { + g.drawCenteredString(font, symbol, (int) x, (int) y - 4, color); + g.drawCenteredString(font, distance, (int) x, (int) y + 6, color); + if (extra != null) { + g.drawCenteredString(font, extra, (int) x, (int) y + 16, color); + } + return; + } + // Behind or off to the side: point the way along the screen edge. + double dx = ahead ? x - w / 2.0 : w / 2.0 - x; + double dy = ahead ? y - h / 2.0 : h / 2.0 - y; + double scale = Math.max(Math.abs(dx) / (w / 2.0 - EDGE), Math.abs(dy) / (h / 2.0 - EDGE)); + if (scale <= 0.0) { + return; + } + int ex = Mth.clamp((int) (w / 2.0 + dx / scale), EDGE, w - EDGE); + int ey = Mth.clamp((int) (h / 2.0 + dy / scale), EDGE, h - EDGE); + g.drawCenteredString(font, "â—†", ex, ey - 4, color); + } +} From 8a9d6dd82692fa6f6c7c6b47975f54d4c2c13351 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:40 +0800 Subject: [PATCH 0681/1461] Drive the thermal view through the single post effect slot of 1.21.11 --- .../mcheli/client/VehicleCamera.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java new file mode 100644 index 00000000..de7ef3c4 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java @@ -0,0 +1,127 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; + +import java.util.function.Consumer; + +/** + * The local crew member's sight, {@code MCH_Camera}: zoom for vehicles with a {@code CameraZoom} + * above one, and night vision and thermal view for those with {@code EnableNightVision}. Both + * reset whenever the player changes seat or leaves the vehicle. + */ +public final class VehicleCamera { + /** The original's camera modes; the HUD reads the ordinal as {@code cam_mode}. */ + public enum Mode { + NORMAL, + NIGHT_VISION, + /** Night vision seen through a white hot filter, the original's "pencil" shader mode. */ + THERMAL + } + + /** The post effect of the thermal view, {@code assets/mcheli/post_effect/thermal.json}. */ + private static final Identifier THERMAL_EFFECT = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "thermal"); + + private static float zoom = 1.0F; + private static Mode mode = Mode.NORMAL; + + private VehicleCamera() { + } + + public static float zoom() { + return zoom; + } + + public static Mode mode() { + return mode; + } + + /** Back to the plain view, on a seat change or after leaving the vehicle. */ + static void reset() { + zoom = 1.0F; + mode = Mode.NORMAL; + } + + /** + * Keeps the thermal filter in the player's post effects while the thermal view is on. The list + * is the one a server's post effect packet fills, so ours is put back if that replaces it. + */ + static void tick() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + return; + } + boolean active = THERMAL_EFFECT.equals(mc.gameRenderer.currentPostEffect()); + if (mode == Mode.THERMAL && !active) { + ClientHooks.postEffects().load(THERMAL_EFFECT); + } else if (mode != Mode.THERMAL && active) { + mc.gameRenderer.clearPostEffect(); + } + } + + /** + * {@code zoomCamera}: each press doubles the zoom up to the vehicle's {@code CameraZoom}, and + * the press after that goes back to one. The caller checks that the seat looks through a sight. + */ + static void onZoomKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + zoomStep(player, def != null && !vehicle.isDestroyed() ? def.cameraZoom : 1); + } + + /** One press of the zoom key on a sight that zooms up to {@code max}. */ + static void zoomStep(LocalPlayer player, int max) { + if (max <= 1) { + refuse(player); + return; + } + zoom = zoom >= max - 0.01F ? 1.0F : Math.min(zoom * 2.0F, max); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + + /** + * {@code switchCameraMode}: steps from the plain view to night vision, to thermal view and + * back. The server applies night vision as an effect in both of the last two; the thermal + * filter is drawn here. + */ + static void onCameraModeKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + if (def == null || !def.nightVision || vehicle.isDestroyed()) { + refuse(player); + return; + } + nextMode(player, on -> ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), + on ? CrewActionPacket.Action.NIGHT_VISION_ON : CrewActionPacket.Action.NIGHT_VISION_OFF))); + } + + /** + * Steps to the next camera mode on a sight that has night vision. {@code nightVision} asks + * the server to switch the effect on or off, only when that changes. + */ + static void nextMode(LocalPlayer player, Consumer nightVision) { + Mode next = Mode.values()[(mode.ordinal() + 1) % Mode.values().length]; + if (mode == Mode.NORMAL || next == Mode.NORMAL) { + nightVision.accept(next != Mode.NORMAL); + } + mode = next; + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + + /** The view's field of view, narrowed by the zoom. */ + public static float fov(float fov) { + return zoom > 1.0F ? fov / zoom : fov; + } + + /** {@code playSoundNG}: the key does nothing in this seat. */ + private static void refuse(LocalPlayer player) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } +} From b85dd506e4a52b1bdc3ecc79d55f71ae09f201b3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:41 +0800 Subject: [PATCH 0682/1461] Add client/VehiclePicker.java to the 1.21.11 shared sources --- .../mcheli/client/VehiclePicker.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java new file mode 100644 index 00000000..9507412f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.BoardVehiclePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Right-clicking anywhere on a vehicle's hull boards it. + * + *

Vanilla only picks an entity by its main hit box, which for most MCHeli vehicles is the + * default two by 0.7 block box at their feet; the hull's real shape lives in its extra + * {@code BoundingBox} lines. In the original those boxes were clickable too. Inflating the main box + * instead would change how the vehicle collides with terrain, so the use key looks for them here. + */ +final class VehiclePicker { + private VehiclePicker() { + } + + static boolean onUseItem(InteractionHand hand) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || hand != InteractionHand.MAIN_HAND || player.getVehicle() instanceof AircraftEntity) { + return false; + } + // Vanilla already handles a click on the main box. + if (mc.hitResult instanceof EntityHitResult hit && hit.getEntity() instanceof AircraftEntity) { + return false; + } + Hit hit = pick(player, mc.hitResult); + if (hit == null) { + return false; + } + ModNetwork.sendToServer(new BoardVehiclePacket(hit.vehicle.getId(), hit.location)); + return true; + } + + private record Hit(AircraftEntity vehicle, Vec3 location) { + } + + /** The nearest vehicle hull along the view, closer than the block or entity vanilla picked. */ + private static @Nullable Hit pick(Player player, @Nullable HitResult vanilla) { + double range = player.entityInteractionRange(); + Vec3 eye = player.getEyePosition(); + Vec3 to = eye.add(player.getViewVector(1.0F).scale(range)); + double limit = vanilla != null && vanilla.getType() != HitResult.Type.MISS ? eye.distanceToSqr(vanilla.getLocation()) : range * range; + Hit best = null; + for (AircraftEntity vehicle : player.level().getEntitiesOfClass(AircraftEntity.class, new AABB(eye, to).inflate(16.0))) { + Optional at = vehicle.clipHitBoxes(eye, to, 0.2); + vehicle.consumeHitBoxFactor(); + if (at.isPresent() && eye.distanceToSqr(at.get()) < limit) { + limit = eye.distanceToSqr(at.get()); + best = new Hit(vehicle, at.get()); + } + } + return best; + } +} From 4b09dac39fa8324fb40c0762c085579afc4d37c0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:41 +0800 Subject: [PATCH 0683/1461] Add client/WeaponController.java to the 1.21.11 shared sources --- .../mcheli/client/WeaponController.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java new file mode 100644 index 00000000..c0ab2443 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.FireWeaponPacket; +import com.jnhprojects.mcheli.network.LockWarningPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The crew member's trigger: fires, switches weapons and modes, drops flares and resupplies. + * + *

Like the original, the client fires first. Its copy of the weapon decides whether the shot + * can happen (ready, loaded, locked on, a target in sight) and works out the option values; only + * then is the server asked, which fires the real projectile. + */ +final class WeaponController { + private WeaponController() { + } + + private record Seat(AircraftEntity vehicle, VehicleWeapons weapons, int seat) { + } + + private static @Nullable Seat seat(@Nullable LocalPlayer player) { + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle) || vehicle.definition() == null) { + return null; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (weapons == null || seat < 0 || seat >= vehicle.definition().seatCount()) { + return null; + } + return new Seat(vehicle, weapons, seat); + } + + /** The use key is the trigger while the seat has a weapon, so it must not also use items. */ + static boolean onUseItem() { + Seat s = seat(Minecraft.getInstance().player); + return s != null && s.weapons.selected(s.seat) >= 0; + } + + static boolean onPickBlock() { + Seat s = seat(Minecraft.getInstance().player); + if (s == null || s.weapons.sets().isEmpty()) { + return false; + } + send(s.vehicle, Minecraft.getInstance().player != null && Minecraft.getInstance().player.isShiftKeyDown() + ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + return true; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + Seat s = seat(player); + if (s == null) { + return; + } + while (KeyBindings.SWITCH_WEAPON.consumeClick()) { + send(s.vehicle, player.isShiftKeyDown() ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + WeaponSet set = s.weapons.current(s.seat); + if (set != null && set.switchMode()) { + send(s.vehicle, CrewActionPacket.Action.WEAPON_MODE); + click(); + } + } + while (KeyBindings.FLARE.consumeClick()) { + if (s.seat <= 1 && s.weapons.flares.use()) { + send(s.vehicle, CrewActionPacket.Action.FLARE); + click(); + } + } + + if (mc.screen == null && mc.options.keyUse.isDown() && !s.vehicle.isDestroyed()) { + fire(s, player); + } + } + + private static void fire(Seat s, LocalPlayer player) { + int id = s.weapons.selected(s.seat); + if (id < 0 || s.weapons.operatorSeat(id) != s.seat) { + return; + } + Shot shot = new Shot(s.vehicle, player); + boolean fired = s.weapons.use(player, s.seat, shot, (set, mount, sh) -> clientShot(s, id, set, sh, player)); + if (fired) { + ModNetwork.sendToServer(new FireWeaponPacket(s.vehicle.getId(), id, shot.option1, shot.option2)); + } + } + + /** The client half of each weapon's {@code shot}: can it fire, and with which options. */ + private static boolean clientShot(Seat s, int id, WeaponSet set, Shot shot, LocalPlayer player) { + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_2, ROCKET, MARKER_ROCKET -> { + shot.option1 = set.mode(); + yield true; + } + case AA_MISSILE, AT_MISSILE -> { + Guidance g = s.weapons.guidance(id); + if (g == null) { + yield false; + } + boolean locked = g.lock(player, viewPoint(player)); + if (g.shouldNotifyTarget() && g.target() != null) { + ModNetwork.sendToServer(new LockWarningPacket(g.target().getId())); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (!locked || g.lastLocked() == null) { + yield false; + } + shot.option1 = g.lastLocked().getId(); + shot.option2 = set.mode(); + yield true; + } + case TV_MISSILE -> { + shot.option1 = set.mode(); + shot.option2 = 0; + yield true; + } + case AS_MISSILE -> landAhead(s.vehicle, player, 1.62, 200.0, false) != null; + case CAS -> landAhead(s.vehicle, player, 2.0, 80.0, false) != null; + case TORPEDO -> !w.guidedTorpedo || landAhead(player, player, 0.0, 100.0, true) != null; + case DUMMY, SMOKE -> false; + case null -> false; + default -> true; + }; + } + + /** + * Where the player sees from: in first person the camera, which CrewController moves to gunner + * sights and launcher cameras; otherwise the player's eyes. + */ + private static Vec3 viewPoint(LocalPlayer player) { + Minecraft mc = Minecraft.getInstance(); + return mc.options.getCameraType().isFirstPerson() ? mc.gameRenderer.getMainCamera().position() : player.getEyePosition(); + } + + /** A block along the player's view: dry ground for strike weapons, water for guided torpedoes. */ + private static @Nullable BlockHitResult landAhead(Entity origin, LocalPlayer player, double eyeHeight, double range, boolean water) { + Vec3 from = origin.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(Vec3.directionFromRotation(player.getXRot(), player.getYRot()).scale(range)); + BlockHitResult hit = player.level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, player)); + if (hit.getType() != HitResult.Type.BLOCK) { + return null; + } + boolean wet = false; + BlockPos pos = hit.getBlockPos(); + for (Direction d : Direction.values()) { + wet |= player.level().getFluidState(pos.relative(d)).is(FluidTags.WATER); + } + return wet == water ? hit : null; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + private static void click() { + LocalPlayer player = Minecraft.getInstance().player; + if (player != null) { + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + } +} From 8c6f0c9390c10676484d46d7452558fc2bf85427 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:42 +0800 Subject: [PATCH 0684/1461] Add client/WeaponHud.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/client/WeaponHud.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java new file mode 100644 index 00000000..347ac6f1 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java @@ -0,0 +1,123 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; + +/** + * The vehicle HUD layer. In first person it runs the seat's HUD script (the {@code HUD} line of + * the definition, or the kind's default), or {@code tv_missile} while flying a TV missile, as the + * original did; a seat whose HUD is {@code none} gets nothing. Where no script draws (third + * person, a destroyed vehicle, a script that does not exist) it shows a plain text readout of hit + * points, ammunition, reload, heat, lock and flares. + */ +final class WeaponHud { + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int RED = 0xFFFF5050; + private static final int GREEN = 0xFF60FF60; + private static final String NO_HUD = "none"; + + private WeaponHud() { + } + + /** A pilot in gunner mode gets the gunner seat's HUD, as in the original. */ + private static int hudSeat(AircraftEntity vehicle, int seat) { + return seat == 0 && vehicle.isGunnerMode() ? 1 : seat; + } + + static void draw(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + AircraftDefinition def = vehicle.definition(); + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (def == null || weapons == null || seat < 0 || seat >= def.seatCount()) { + return; + } + if (mc.options.getCameraType().isFirstPerson() && !vehicle.isDestroyed()) { + String hud = CrewController.tvCamera(vehicle, seat) != null ? "tv_missile" + : hudSeat(vehicle, seat) < def.huds.size() ? def.huds.get(hudSeat(vehicle, seat)) : null; + if (NO_HUD.equals(hud)) { + // The original's way of saying a seat has no HUD at all. + return; + } + if (hud != null && HudRenderer.draw(g, vehicle, def, player, seat, hud, delta.getGameTimeDeltaPartialTick(false))) { + return; + } + } + Font font = mc.font; + int x = g.guiWidth() - 160; + int y = g.guiHeight() - 92; + + float hp = (float) vehicle.hp() / def.maxHp; + int hpColor = vehicle.isDestroyed() ? RED : hp < 0.5F ? AMBER : GREEN; + g.drawString(font, Component.translatable("hud.mcheli.hp", vehicle.hp(), def.maxHp), x, y, hpColor); + y += 11; + + WeaponSet set = weapons.current(seat); + if (set != null) { + g.drawString(font, set.displayName(), x, y, WHITE); + y += 11; + Component ammo = vehicle.hasInfiniteAmmo(player) && set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo_infinite", set.ammo()) + : set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo", set.ammo(), set.restAmmo()) + : Component.translatable("hud.mcheli.ammo_none"); + g.drawString(font, ammo, x, y, set.ammo() == 0 && set.magazineSize() > 0 ? RED : WHITE); + y += 11; + Component status = null; + int color = AMBER; + if (set.isReloading()) { + status = Component.translatable("hud.mcheli.reloading"); + } else if (set.isOverheated()) { + status = Component.translatable("hud.mcheli.overheat"); + color = RED; + } else if (set.isInPreparation()) { + status = Component.translatable("hud.mcheli.preparing"); + } else if (set.magazineSize() > 0 && set.ammo() == 0 && set.restAmmo() == 0) { + status = Component.translatable("hud.mcheli.resupply", KeyBindings.VEHICLE_SCREEN.getTranslatedKeyMessage()); + color = RED; + } + Guidance lock = weapons.guidance(weapons.selected(seat)); + if (status == null && lock != null && lock.lockCount() > 0) { + status = lock.isLocked() + ? Component.translatable("hud.mcheli.locked") + : Component.translatable("hud.mcheli.locking", lock.lockCount() * 100 / Math.max(lock.lockCountMax(), 1)); + color = lock.isLocked() ? RED : AMBER; + } + if (status != null) { + g.drawString(font, status, x, y, color); + y += 11; + } + if (set.definition().maxHeatCount > 0) { + int width = 100; + int filled = Math.min(width, set.currentHeat * width / set.definition().maxHeatCount); + g.fill(x, y + 2, x + width, y + 5, 0x80000000); + g.fill(x, y + 2, x + filled, y + 5, set.isOverheated() ? RED : AMBER); + y += 8; + } + } else if (!weapons.sets().isEmpty()) { + g.drawString(font, Component.translatable("hud.mcheli.no_weapon"), x, y, WHITE); + y += 11; + } + if (seat <= 1 && weapons.flares.isFitted()) { + Component flare = vehicle.isFlareActive() ? Component.translatable("hud.mcheli.flare_active") + : weapons.flares.isPreparing() ? Component.translatable("hud.mcheli.flare_wait") + : Component.translatable("hud.mcheli.flare_ready", KeyBindings.FLARE.getTranslatedKeyMessage()); + g.drawString(font, flare, x, y, vehicle.isFlareActive() ? GREEN : WHITE); + } + } +} From 567c9dc41b3835219c1d5a28537141caa70aad1c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:42 +0800 Subject: [PATCH 0685/1461] Add client/hud/HudCanvas.java to the 1.21.11 shared sources --- .../mcheli/client/hud/HudCanvas.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java new file mode 100644 index 00000000..b16caadc --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java @@ -0,0 +1,165 @@ +package com.jnhprojects.mcheli.client.hud; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.Identifier; +import org.joml.Matrix3x2fStack; + +/** + * The drawing primitives of {@code MCH_HudItem}, on the 26.x GUI. + * + *

The original drew GL lines, points and quads at fractional GUI coordinates. The GUI now only + * takes whole numbers, but every element keeps a copy of the pose, so each primitive is a unit + * square stretched and turned into place. Lines are one GUI unit thick, which is what + * {@code glLineWidth(scaleFactor)} gave; blips are two units square. + */ +final class HudCanvas { + private static final float LINE_WIDTH = 1.0F; + private static final float POINT_SIZE = 2.0F; + + private final GuiGraphics g; + private final Font font; + + HudCanvas(GuiGraphics g, Font font) { + this.g = g; + this.font = font; + } + + Matrix3x2fStack pose() { + return g.pose(); + } + + /** Turns everything drawn until the matching {@code pop} by {@code degrees} about a point. */ + void pushRotation(double x, double y, float degrees) { + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) x, (float) y); + pose.rotate((float) Math.toRadians(degrees)); + pose.translate((float) -x, (float) -y); + } + + void pop() { + g.pose().popMatrix(); + } + + void line(double x0, double y0, double x1, double y1, int color) { + double dx = x1 - x0; + double dy = y1 - y0; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + return; + } + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) x0, (float) y0); + pose.rotate((float) Math.atan2(dy, dx)); + pose.translate(0.0F, -LINE_WIDTH / 2.0F); + pose.scale((float) length, LINE_WIDTH); + g.fill(0, 0, 1, 1, color); + pose.popMatrix(); + } + + /** {@code GL_LINES}: x, y pairs taken two points at a time. */ + void lines(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 4) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_STRIP}. */ + void strip(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 2) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_LOOP}. */ + void loop(double[] p, int color) { + strip(p, color); + if (p.length >= 4) { + line(p[p.length - 2], p[p.length - 1], p[0], p[1], color); + } + } + + /** + * {@code GL_LINES} with {@code glLineStipple(factor, pattern)}: each segment starts the 16 bit + * pattern afresh, lowest bit first, every bit covering {@code factor} GUI units. + */ + void stippled(double[] p, int color, int factor, int pattern) { + double step = Math.max(factor, 1); + for (int i = 0; i + 3 < p.length; i += 4) { + double dx = p[i + 2] - p[i]; + double dy = p[i + 3] - p[i + 1]; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + continue; + } + double ux = dx / length; + double uy = dy / length; + double runStart = -1.0; + int bit = 0; + for (double t = 0.0; t < length; t += step, bit = (bit + 1) & 15) { + boolean on = (pattern >> bit & 1) != 0; + if (on && runStart < 0.0) { + runStart = t; + } else if (!on && runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i] + ux * t, p[i + 1] + uy * t, color); + runStart = -1.0; + } + } + if (runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i + 2], p[i + 3], color); + } + } + } + + void rect(double x0, double y0, double x1, double y1, int color) { + double left = Math.min(x0, x1); + double top = Math.min(y0, y1); + double width = Math.abs(x1 - x0); + double height = Math.abs(y1 - y0); + if (width <= 0.0 || height <= 0.0) { + return; + } + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) left, (float) top); + pose.scale((float) width, (float) height); + g.fill(0, 0, 1, 1, color); + pose.popMatrix(); + } + + void point(double x, double y, int color) { + rect(x - POINT_SIZE / 2.0, y - POINT_SIZE / 2.0, x + POINT_SIZE / 2.0, y + POINT_SIZE / 2.0, color); + } + + /** + * {@code drawTexture}: texels {@code u, v, uWidth, vHeight} of a {@code textureWidth} by + * {@code textureHeight} image, drawn into a box and turned about its centre. Drawn untinted, + * as the original reset the colour to white first. + */ + void texture(Identifier texture, double left, double top, double width, double height, double u, double v, + double uWidth, double vHeight, float rotation, int textureWidth, int textureHeight) { + if (width == 0.0 || height == 0.0) { + return; + } + Matrix3x2fStack pose = g.pose(); + pose.pushMatrix(); + pose.translate((float) (left + width / 2.0), (float) (top + height / 2.0)); + pose.rotate((float) Math.toRadians(rotation)); + pose.translate((float) (-width / 2.0), (float) (-height / 2.0)); + pose.scale((float) width, (float) height); + g.blit(texture, 0, 0, 1, 1, + (float) (u / textureWidth), (float) ((u + uWidth) / textureWidth), + (float) (v / textureHeight), (float) ((v + vHeight) / textureHeight)); + pose.popMatrix(); + } + + void text(String text, int x, int y, int color, boolean centered) { + if (centered) { + g.drawCenteredString(font, text, x, y, color); + } else { + g.drawString(font, text, x, y, color); + } + } +} From 6c0e147b153c8ddd89ebf99b680d166398711ed7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:43 +0800 Subject: [PATCH 0686/1461] Add client/hud/HudRenderer.java to the 1.21.11 shared sources --- .../mcheli/client/hud/HudRenderer.java | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java new file mode 100644 index 00000000..4abb7368 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java @@ -0,0 +1,354 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.hud.Expression; +import com.jnhprojects.mcheli.hud.HudCommand; +import com.jnhprojects.mcheli.hud.HudScript; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.IllegalFormatException; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Runs HUD scripts, the drawing half of {@code MCH_Hud} and the {@code MCH_HudItem} subclasses. + * + *

An {@code If} hides every line up to its {@code EndIf}; {@code Exit} ends the current script + * only, so a {@code Call}er carries on; the colour carries into called scripts and over to the next + * frame. A script cannot call itself, directly or through another, the same guard the original had. + * A line that fails to format is skipped and reported once, where the original crashed. + */ +public final class HudRenderer { + private static final Logger LOGGER = LoggerFactory.getLogger(HudRenderer.class); + private static final int AZIMUTH_RED = 0xFFFF0000; + private static final String[] AZIMUTH_8 = {"S", "SW", "W", "NW", "N", "NE", "E", "SE"}; + /** {@code glLineStipple} pattern of the original's dashed lines. */ + private static final int DASHES = 0xCCCC; + + private static final HudState STATE = new HudState(); + private static final Set REPORTED = new HashSet<>(); + + private final HudCanvas canvas; + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final LocalPlayer player; + private final double centerX; + private final double centerY; + private final Set running = new HashSet<>(); + + private HudRenderer(HudCanvas canvas, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, double width, double height) { + this.canvas = canvas; + this.vehicle = vehicle; + this.def = def; + this.player = player; + this.centerX = width / 2.0; + this.centerY = height / 2.0; + } + + public static void tick() { + LocalPlayer player = Minecraft.getInstance().player; + STATE.tick(player != null && player.getVehicle() instanceof AircraftEntity vehicle ? vehicle : null); + } + + /** Fed by the pilot's mouse steering; moves the control stick indicator. */ + public static void onPilotLook(float dYaw, float dPitch) { + STATE.onPilotLook(dYaw, dPitch); + } + + /** + * Draws a HUD script for the local player's seat. + * + * @return false if no script by that name is loaded, so the caller can draw something else + */ + public static boolean draw(GuiGraphics g, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, + int seat, String hud, float partialTicks) { + HudScript script = HudScripts.INSTANCE.get(hud); + if (script == null) { + return false; + } + double width = g.guiWidth(); + double height = g.guiHeight(); + STATE.update(vehicle, def, player, seat, width, height, partialTicks); + HudRenderer renderer = new HudRenderer(new HudCanvas(g, Minecraft.getInstance().font), vehicle, def, player, width, height); + renderer.run(script); + return true; + } + + private double eval(Expression e) { + return e.eval(STATE.vars); + } + + private double x(Expression e) { + return centerX + eval(e); + } + + private double y(Expression e) { + return centerY + eval(e); + } + + private double[] points(List coordinates) { + double[] out = new double[coordinates.size()]; + for (int i = 0; i + 1 < out.length; i += 2) { + out[i] = x(coordinates.get(i)); + out[i + 1] = y(coordinates.get(i + 1)); + } + return out; + } + + private void run(HudScript script) { + if (!running.add(script.name())) { + return; + } + try { + boolean hidden = false; + for (HudCommand command : script.commands()) { + if (command instanceof HudCommand.If c) { + hidden = eval(c.condition()) == 0.0; + continue; + } + if (command instanceof HudCommand.EndIf) { + hidden = false; + continue; + } + if (hidden) { + continue; + } + if (command instanceof HudCommand.Exit) { + return; + } + execute(script, command); + } + } finally { + running.remove(script.name()); + } + } + + private void execute(HudScript script, HudCommand command) { + int color = STATE.color; + switch (command) { + case HudCommand.Color c -> { + STATE.color = (int) (long) eval(c.argb()); + STATE.vars.set(HudVariable.COLOR, STATE.color & 0xFFFFFFFFL); + } + case HudCommand.Call c -> { + HudScript called = HudScripts.INSTANCE.get(c.hud()); + if (called != null) { + run(called); + } + } + case HudCommand.Text c -> text(script, c, color); + case HudCommand.Texture c -> { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + c.name() + ".png"); + int[] size = HudScripts.INSTANCE.textureSize(Minecraft.getInstance().getResourceManager(), texture); + canvas.texture(texture, x(c.left()), y(c.top()), eval(c.width()), eval(c.height()), + eval(c.u()), eval(c.v()), eval(c.uWidth()), eval(c.vHeight()), (float) eval(c.rotation()), size[0], size[1]); + } + case HudCommand.Rect c -> { + // The original truncated the size, not the position. + double left = x(c.left()); + double top = y(c.top()); + canvas.rect(left, top, left + (int) eval(c.width()), top + (int) eval(c.height()), color); + } + case HudCommand.Line c -> canvas.strip(points(c.points()), color); + case HudCommand.LineStipple c -> canvas.stippled(points(c.points()), color, (int) eval(c.factor()), (int) eval(c.pattern())); + case HudCommand.Radar c -> radar(c, color); + case HudCommand.Graduation c -> graduation(c, color); + case HudCommand.CameraRot c -> cameraRot(x(c.x()), y(c.y()), color); + case HudCommand.If c -> { + } + case HudCommand.EndIf c -> { + } + case HudCommand.Exit c -> { + } + } + } + + /** {@code MCH_HudItemString}: skipped while a weapon value is shown and no weapon (or magazine) applies. */ + private void text(HudScript script, HudCommand.Text c, int color) { + Object[] args = new Object[c.args().size()]; + for (int i = 0; i < args.length; i++) { + TextArg arg = c.args().get(i); + if ((arg.needsWeapon() && !STATE.hasWeapon()) || (arg.needsMagazine() && !STATE.hasMagazine())) { + return; + } + args[i] = STATE.text(arg, vehicle, def, player); + } + String text; + try { + text = String.format(Locale.ROOT, c.format(), args); + } catch (IllegalFormatException e) { + String key = script.name() + ':' + c.line(); + if (REPORTED.add(key)) { + LOGGER.warn("HUD {}.txt line {}: cannot format \"{}\": {}", script.name(), c.line(), c.format(), e.getMessage()); + } + return; + } + canvas.text(text, (int) x(c.x()), (int) y(c.y()), color, c.centered()); + } + + /** {@code MCH_HudItemRadar}: blips at half scale for a 64 unit box, turned with the view. */ + private void radar(HudCommand.Radar c, int color) { + List blips = c.enemies() ? STATE.enemyBlips : STATE.entityBlips; + if (blips.isEmpty()) { + return; + } + double rotation = Math.toRadians(eval(c.rotation())); + double left = x(c.left()); + double top = y(c.top()); + double w = eval(c.width()); + double h = eval(c.height()); + double cos = Math.cos(rotation); + double sin = Math.sin(rotation); + for (double[] blip : blips) { + double px = blip[0] / 2.0 * w / 64.0; + double py = blip[1] / 2.0 * h / 64.0; + double rx = px * cos - py * sin; + double ry = px * sin + py * cos; + if (rx > -w / 2.0 && rx < w / 2.0 && ry > -h / 2.0 && ry < h / 2.0) { + canvas.point(rx + left + w / 2.0, ry + top + h / 2.0, color); + } + } + } + + private void graduation(HudCommand.Graduation c, int color) { + int x = (int) x(c.x()); + int y = (int) y(c.y()); + double angle = eval(c.angle()); + canvas.pushRotation(x, y, (float) eval(c.roll())); + switch (c.kind()) { + case YAW -> headingTape(angle, color, x, y); + case PITCH_1 -> pitchScale(angle, color, x, y); + case PITCH_2 -> pitchLadder(angle, color, x, y, true); + case PITCH_3 -> pitchLadder(angle, color, x, y, false); + } + canvas.pop(); + } + + /** {@code drawCommonGraduationYaw}: a tick every degree, labels every three, compass points every 45. */ + private void headingTape(double playerYaw, int color, int posX, int posY) { + double yaw = HudState.rotate360(playerYaw); + posX -= 90; + double[] line = new double[76]; + int offset = (int) (yaw * 10.0) % 10; + int degree = (int) yaw - 9; + for (int i = 0; i < line.length / 4; i++, degree++) { + int azimuthX = posX + i * 10 - offset; + line[i * 4] = azimuthX; + line[i * 4 + 1] = posY; + line[i * 4 + 2] = azimuthX; + line[i * 4 + 3] = posY + (degree % 45 == 0 ? 15 : degree % 3 == 0 ? 10 : 5); + if (degree % 45 == 0) { + canvas.text(AZIMUTH_8[Math.floorMod(degree, 360) / 45], azimuthX, posY - 10, AZIMUTH_RED, true); + } else if (degree % 3 == 0) { + int heading = degree + 180; + if (heading < 0) { + heading += 360; + } + if (heading > 360) { + heading -= 360; + } + canvas.text(Integer.toString(heading), azimuthX, posY - 10, color, true); + } + } + canvas.lines(line, color); + } + + /** {@code drawCommonGraduationPitch1}: a scale of ticks on both sides that slides with the pitch. */ + private void pitchScale(double playerPitch, int color, int posX, int posY) { + int pitch = (int) playerPitch % 360; + int y = (int) (playerPitch * 10.0 % 10.0); + if (y < 0) { + y += 10; + } + int left = posX - 100; + int right = posX + 100; + posY -= 80; + double[] line = new double[144]; + int p = playerPitch < 0.0 && y != 0 ? pitch - 9 : pitch - 8; + for (int i = 0; i < line.length / 8; i++, p++) { + int outer = p % 3 == 0 ? 15 : 5; + double rowY = posY + i * 10 - y; + line[i * 8] = left - outer; + line[i * 8 + 1] = rowY; + line[i * 8 + 2] = left; + line[i * 8 + 3] = rowY; + line[i * 8 + 4] = right + outer; + line[i * 8 + 5] = rowY; + line[i * 8 + 6] = right; + line[i * 8 + 7] = rowY; + } + canvas.lines(line, color); + canvas.strip(new double[]{left - 25, posY - 90, left, posY - 90, left, posY + 90, left - 25, posY + 90}, color); + canvas.strip(new double[]{right + 25, posY - 90, right, posY - 90, right, posY + 90, right + 25, posY + 90}, color); + } + + /** + * {@code drawCommonGraduationPitch2}: a ladder with a rung every five degrees, dashed below + * the horizon. {@code wide} is {@code Pitch2} with five rungs; {@code Pitch3} has three, twice as far apart. + */ + private void pitchLadder(double playerPitch, int color, int posX, int posY, boolean wide) { + playerPitch = -playerPitch; + int pitchStep = (int) playerPitch / 5 * 5; + int start = wide ? 0 : 1; + int end = wide ? 5 : 4; + int spacing = wide ? 1 : 2; + double rest = playerPitch % 5.0; + double[] line = new double[8]; + for (int i = start; i < end; i++) { + int pitch = -(-pitchStep - 10 + i * 5); + int halfWidth = pitch != 0 ? 50 : 100; + int y = posY + (int) (-60 * spacing + rest * 6.0 * spacing + i * 30 * spacing); + line[0] = posX - halfWidth; + line[1] = y + (pitch == 0 ? 0 : pitch > 0 ? 2 : -2); + line[2] = posX - 50; + line[3] = y; + line[4] = posX + halfWidth; + line[5] = line[1]; + line[6] = posX + 50; + line[7] = y; + canvas.lines(line, color); + line[0] = posX - 50; + line[1] = y; + line[2] = posX - 30; + line[3] = y; + line[4] = posX + 50; + line[5] = y; + line[6] = posX + 30; + line[7] = y; + if (pitch >= 0) { + canvas.lines(line, color); + } else { + canvas.stippled(line, color, 1, DASHES); + } + if (pitch != 0) { + canvas.text(Integer.toString(pitch), posX - 60, y - 4, color, true); + canvas.text(Integer.toString(pitch), posX + 60, y - 4, color, true); + } + } + } + + /** {@code MCH_HudItemCameraRot}: a frame with a box showing where the crew's view points against the hull. */ + private void cameraRot(double posX, double posY, int color) { + canvas.loop(new double[]{posX - 21, posY - 11, posX + 21, posY - 11, posX + 21, posY + 11, posX - 21, posY + 11}, color); + canvas.stippled(new double[]{posX - 21, posY, posX, posY, posX + 21, posY, posX, posY, + posX, posY - 11, posX, posY, posX, posY + 11, posX, posY}, color, 1, DASHES); + float pitch = Mth.clamp(player.getXRot(), -30.0F, 70.0F); + pitch = (float) ((pitch - 20.0F) * 0.16); + float yaw = Mth.clamp(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot()) * 2.0F, -50.0F, 50.0F); + yaw = (float) (yaw * 0.34); + canvas.loop(new double[]{posX + yaw - 3, posY + pitch - 2, posX + yaw + 3, posY + pitch - 2, + posX + yaw + 3, posY + pitch + 2, posX + yaw - 3, posY + pitch + 2}, color); + } +} From 696c1858282f8edd33f75ff88d6d9bb0d9426c07 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:43 +0800 Subject: [PATCH 0687/1461] Add client/hud/HudScripts.java to the 1.21.11 shared sources --- .../mcheli/client/hud/HudScripts.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java new file mode 100644 index 00000000..6e611a23 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java @@ -0,0 +1,86 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.hud.HudFormatException; +import com.jnhprojects.mcheli.hud.HudScript; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +/** + * The HUD scripts in {@code assets/mcheli/hud}, read through the resource manager so resource + * packs can replace or add them, and reloaded with F3 + T. A script that fails to parse is logged + * and left out; the vehicles using it fall back to the plain readout. + */ +public final class HudScripts implements ResourceManagerReloadListener { + public static final HudScripts INSTANCE = new HudScripts(); + private static final Logger LOGGER = LoggerFactory.getLogger(HudScripts.class); + private static final String FOLDER = "hud"; + private static final int DEFAULT_TEXTURE_SIZE = 256; + + private Map scripts = Map.of(); + private final Map textureSizes = new HashMap<>(); + + private HudScripts() { + } + + public @Nullable HudScript get(String name) { + return scripts.get(name.toLowerCase(Locale.ROOT)); + } + + @Override + public void onResourceManagerReload(ResourceManager resources) { + Map loaded = new HashMap<>(); + resources.listResources(FOLDER, id -> id.getNamespace().equals(MCHeli.MOD_ID) && id.getPath().endsWith(".txt")) + .forEach((id, resource) -> { + String path = id.getPath(); + String name = path.substring(FOLDER.length() + 1, path.length() - 4).toLowerCase(Locale.ROOT); + try (Reader in = resource.openAsReader()) { + loaded.put(name, HudScript.parse(name, in)); + } catch (IOException | HudFormatException e) { + LOGGER.error("Skipping HUD {}: {}", id, e.getMessage()); + } + }); + scripts = Map.copyOf(loaded); + textureSizes.clear(); + LOGGER.info("Loaded {} HUD scripts", scripts.size()); + } + + /** + * Width and height of a GUI texture in pixels, read from the PNG header once. The original + * fell back to 256 when it could not tell, and so does this. + */ + int[] textureSize(ResourceManager resources, Identifier texture) { + return textureSizes.computeIfAbsent(texture, id -> readSize(resources.getResource(id))); + } + + private static int[] readSize(Optional resource) { + if (resource.isPresent()) { + try (InputStream in = resource.get().open(); DataInputStream data = new DataInputStream(in)) { + // 8 byte signature, IHDR length and type, then width and height as big endian ints. + data.skipNBytes(16); + int width = data.readInt(); + int height = data.readInt(); + if (width > 0 && height > 0) { + return new int[]{width, height}; + } + } catch (IOException e) { + LOGGER.warn("Could not read the size of a HUD texture", e); + } + } + return new int[]{DEFAULT_TEXTURE_SIZE, DEFAULT_TEXTURE_SIZE}; + } +} From 98a22aef5a15f013dff9a2c05d20fe01045fa011 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:43 +0800 Subject: [PATCH 0688/1461] Report 1.21.11 to HUD scripts and read the day time the way it does --- .../mcheli/client/hud/HudState.java | 373 ++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java new file mode 100644 index 00000000..59371f55 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java @@ -0,0 +1,373 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.client.VehicleCamera; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * What the HUD scripts read, the static state of {@code MCH_HudItem}. Ticked with the client, so + * the radar sweep, altitude and stick behave as in the original; {@link #update} then fills in the + * per frame values before a script runs. + * + *

Systems that arrive later read as their idle state for now: the vehicle inventory is empty + * and the mortar distance is unknown. + */ +final class HudState { + private static final int RADAR_RANGE = 64; + private static final int ALTITUDE_INTERVAL = 10; + private static final double STICK_MAX = 40.0; + /** Roughly the mouse units of the original per degree of pilot look. */ + private static final double STICK_PER_DEGREE = 6.0; + private static final int STICK_SAMPLES = 4; + + final HudVariable.Values vars = new HudVariable.Values(); + final List entityBlips = new ArrayList<>(); + final List enemyBlips = new ArrayList<>(); + + private @Nullable AircraftEntity vehicle; + /** Where the selected mortar would land, or -1. */ + private float mortarDistance = -1.0F; + private int radarRotate; + private int radarRotateO; + private int altitude; + private int altitudeCountdown; + private double stickX; + private double stickY; + private final double[] stickXSamples = new double[STICK_SAMPLES]; + private final double[] stickYSamples = new double[STICK_SAMPLES]; + private int stickSample; + private int fuelWarnCount; + + /** Colour set by the last {@code Color} line. Like the original it carries over between frames. */ + int color = 0xFF000000; + + // Per frame, for the text arguments. + private @Nullable WeaponSet weapon; + private String weaponName = ""; + private String weaponAmmo = ""; + private String weaponRestAmmo = ""; + private float reloadPercent; + private float reloadSeconds; + + /** Mouse look of the pilot this frame, in degrees; drives the control stick indicator. */ + void onPilotLook(float dYaw, float dPitch) { + stickX += dYaw * STICK_PER_DEGREE; + stickY += dPitch * STICK_PER_DEGREE; + double length = Math.sqrt(stickX * stickX + stickY * stickY); + if (length > STICK_MAX) { + stickX *= STICK_MAX / length; + stickY *= STICK_MAX / length; + } + } + + void tick(@Nullable AircraftEntity riding) { + if (riding != vehicle) { + vehicle = riding; + radarRotate = 0; + radarRotateO = 0; + altitudeCountdown = 0; + entityBlips.clear(); + enemyBlips.clear(); + } + stickX *= 0.85; + stickY *= 0.85; + if (riding == null || riding.definition() == null) { + return; + } + if (riding.definition().entityRadar) { + radarRotateO = radarRotate; + radarRotate += radarSpeed(riding); + if (radarRotate >= 360) { + radarRotate = 0; + radarRotateO = 0; + sweepRadar(riding); + } + } + if (--altitudeCountdown <= 0) { + altitude = measureAltitude(riding); + altitudeCountdown = ALTITUDE_INTERVAL; + } + } + + /** Degrees per tick, from each kind's client tick handler. */ + private static int radarSpeed(AircraftEntity vehicle) { + return vehicle instanceof PlaneEntity || vehicle instanceof TankEntity ? 10 : 5; + } + + /** {@code MCH_Radar.updateXZ}: mobs within 64 blocks that are not buried under five or more blocks. */ + private void sweepRadar(AircraftEntity center) { + entityBlips.clear(); + enemyBlips.clear(); + Level level = center.level(); + for (Mob mob : level.getEntitiesOfClass(Mob.class, center.getBoundingBox().inflate(RADAR_RANGE))) { + double x = mob.getX() - center.getX(); + double z = mob.getZ() - center.getZ(); + if (x * x + z * z >= RADAR_RANGE * RADAR_RANGE || isBuried(level, mob)) { + continue; + } + (mob instanceof Enemy ? enemyBlips : entityBlips).add(new double[]{x, z}); + } + } + + private static boolean isBuried(Level level, Mob mob) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(mob.getBlockX(), Math.max(mob.getBlockY() + 1, level.getMinY()), mob.getBlockZ()); + int blocks = 0; + for (; pos.getY() < level.getMaxY(); pos.move(0, 1, 0)) { + if (!level.getBlockState(pos).isAir() && ++blocks >= 5) { + return true; + } + } + return false; + } + + /** {@code updateAltitude}: blocks of air below the vehicle, looking at most 256 down. */ + private static int measureAltitude(AircraftEntity vehicle) { + Level level = vehicle.level(); + BlockPos.MutableBlockPos pos = vehicle.blockPosition().mutable(); + int top = pos.getY(); + for (int i = 0; i < 256 && top - i > level.getMinY(); i++) { + pos.setY(top - i); + if (!level.getBlockState(pos).isAir()) { + return i; + } + } + return 0; + } + + float radarRotation(float partialTicks) { + float rot = radarRotate; + float prev = radarRotateO; + if (rot < prev) { + rot += 360.0F; + } + return prev + (rot - prev) * partialTicks; + } + + void update(AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, int seat, double width, double height, + float partialTicks) { + HudVariable.Values v = vars; + v.set(HudVariable.COLOR, color & 0xFFFFFFFFL); + v.set(HudVariable.CENTER_X, width / 2.0); + v.set(HudVariable.CENTER_Y, height / 2.0); + v.set(HudVariable.WIDTH, width); + v.set(HudVariable.HEIGHT, height); + v.set(HudVariable.TIME, Math.floorMod(vehicle.level().getDayTime(), 24000L)); + v.set(HudVariable.TEST_MODE, 0.0); + v.set(HudVariable.PLYR_YAW, Mth.wrapDegrees(player.getViewYRot(partialTicks))); + v.set(HudVariable.PLYR_PITCH, player.getViewXRot(partialTicks)); + v.set(HudVariable.YAW, Mth.wrapDegrees(vehicle.getYRot(partialTicks))); + v.set(HudVariable.PITCH, vehicle.getXRot(partialTicks)); + v.set(HudVariable.ROLL, Mth.wrapDegrees(vehicle.roll(partialTicks))); + v.set(HudVariable.ALTITUDE, altitude); + v.set(HudVariable.SEA_ALT, Math.max(vehicle.getY() - vehicle.level().getSeaLevel(), 0.0)); + v.set(HudVariable.HAVE_RADAR, def.entityRadar); + v.set(HudVariable.RADAR_ROT, radarRotation(partialTicks)); + v.set(HudVariable.HP, vehicle.hp()); + v.set(HudVariable.MAX_HP, def.maxHp); + v.set(HudVariable.HP_RTO, def.maxHp > 0 ? (double) vehicle.hp() / def.maxHp : 0.0); + v.set(HudVariable.THROTTLE, vehicle.visibleThrottle()); + v.set(HudVariable.POS_X, vehicle.getX()); + v.set(HudVariable.POS_Y, vehicle.getY()); + v.set(HudVariable.POS_Z, vehicle.getZ()); + // Only the pilot's client simulates the hull, so read motion from the position instead. + double mx = vehicle.getX() - vehicle.xo; + double my = vehicle.getY() - vehicle.yo; + double mz = vehicle.getZ() - vehicle.zo; + v.set(HudVariable.MOTION_X, mx); + v.set(HudVariable.MOTION_Y, my); + v.set(HudVariable.MOTION_Z, mz); + v.set(HudVariable.SPEED, Math.sqrt(mx * mx + my * my + mz * mz)); + v.set(HudVariable.FUEL, vehicle.fuelRatio()); + v.set(HudVariable.LOW_FUEL, lowFuel(vehicle)); + updateStick(seat == 0 && vehicle.steersWithMouse()); + updateWeapon(vehicle, seat); + v.set(HudVariable.VTOL_STAT, vehicle.vtolMode()); + v.set(HudVariable.FREE_LOOK, seat == 0 && vehicle.input().freeLook); + v.set(HudVariable.GUNNER_MODE, vehicle.isGunnerMode(player)); + v.set(HudVariable.CAM_MODE, VehicleCamera.mode().ordinal()); + v.set(HudVariable.CAM_ZOOM, VehicleCamera.zoom()); + v.set(HudVariable.AUTO_PILOT, vehicle instanceof PlaneEntity && vehicle.isGunnerMode(player)); + VehicleWeapons weapons = vehicle.weapons(); + boolean flares = weapons != null && weapons.flares.isFitted(); + v.set(HudVariable.HAVE_FLARE, flares); + v.set(HudVariable.CAN_FLARE, flares && !weapons.flares.isPreparing() && !weapons.flares.isActive()); + v.set(HudVariable.INVENTORY, 0.0); + v.set(HudVariable.HOVERING, vehicle instanceof HelicopterEntity && vehicle.isHoveringMode()); + v.set(HudVariable.IS_UAV, def.uav); + double uavDistance = vehicle.stationDistance(); + // {@code getUAV_Fs}: signal strength, full at the station and gone 120 blocks out. + v.set(HudVariable.UAV_FS, uavDistance < 0.0 ? 0.0 : 1.0 - Math.min(uavDistance, 120.0) / 120.0); + } + + /** + * {@code isLowFuel}: below 10% the warning blinks, on for eight seconds out of every fourteen. + * Ticks with each update, as the original counted per frame. + */ + private boolean lowFuel(AircraftEntity vehicle) { + if (fuelWarnCount <= 0) { + fuelWarnCount = 280; + } + fuelWarnCount--; + return fuelWarnCount < 160 && vehicle.maxFuel() > 0 && vehicle.fuelRatio() < 0.1F; + } + + /** The original averaged the last four stick positions, scaled to -1..1, with up positive. */ + private void updateStick(boolean pilot) { + stickXSamples[stickSample] = pilot ? stickX / STICK_MAX : 0.0; + stickYSamples[stickSample] = pilot ? -stickY / STICK_MAX : 0.0; + stickSample = (stickSample + 1) % STICK_SAMPLES; + double x = 0.0; + double y = 0.0; + for (int i = 0; i < STICK_SAMPLES; i++) { + x += stickXSamples[i]; + y += stickYSamples[i]; + } + vars.set(HudVariable.STICK_X, x / STICK_SAMPLES); + vars.set(HudVariable.STICK_Y, y / STICK_SAMPLES); + } + + /** {@code updateWeapon} and {@code updateVarMap_Weapon}. */ + private void updateWeapon(AircraftEntity vehicle, int seat) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + weapon = set; + double reloadTime = 0.0; + double heat = 0.0; + double lock = 0.0; + int sightType = 0; + if (set != null) { + WeaponDefinition def = set.definition(); + weaponName = set.displayName(); + weaponAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.ammo()) : ""; + weaponRestAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.restAmmo()) : ""; + float wait; + if (def.delay > def.reloadTime) { + reloadSeconds = Math.abs(set.countWait); + wait = Math.min(Math.abs((float) set.countWait / Math.max(def.delay, 1)), 1.0F); + } else { + reloadSeconds = set.countReloadWait; + wait = (float) set.countReloadWait / Math.max(def.reloadTime, 1); + } + reloadTime = wait; + reloadSeconds /= 20.0F; + reloadPercent = (1.0F - wait) * 100.0F; + if (def.maxHeatCount > 0) { + heat = Math.min((double) set.currentHeat / def.maxHeatCount, 1.0); + } + Guidance guidance = weapons.guidance(weapons.selected(seat)); + if (def.sight == WeaponDefinition.Sight.LOCK && guidance != null && guidance.lockCountMax() > 0) { + lock = (double) guidance.lockCount() / guidance.lockCountMax(); + sightType = 2; + } else if (def.sight == WeaponDefinition.Sight.ROCKET) { + sightType = 1; + } + vars.set(HudVariable.IS_HEAT_WPN, def.maxHeatCount > 0); + vars.set(HudVariable.RELOADING, set.isInPreparation()); + vars.set(HudVariable.DSP_MT_DIST, def.displayMortarDistance); + // The original worked the arc out every fifth tick; it is cheap enough to follow every update. + mortarDistance = def.displayMortarDistance ? (float) set.landingDistance(vehicle) : -1.0F; + } else { + vars.set(HudVariable.IS_HEAT_WPN, 0.0); + vars.set(HudVariable.RELOADING, 0.0); + vars.set(HudVariable.DSP_MT_DIST, 0.0); + mortarDistance = -1.0F; + } + vars.set(HudVariable.RELOAD_TIME, reloadTime); + vars.set(HudVariable.WPN_HEAT, heat); + vars.set(HudVariable.SIGHT_TYPE, sightType); + vars.set(HudVariable.LOCK, lock); + vars.set(HudVariable.MT_DIST, mortarDistance); + } + + boolean hasWeapon() { + return weapon != null; + } + + boolean hasMagazine() { + return weapon != null && weapon.magazineSize() > 0; + } + + /** A {@code DrawString} argument, typed the way the original passed it to {@code String.format}. */ + @Nullable Object text(TextArg arg, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player) { + int worldTime = (int) Math.floorMod(vehicle.level().getDayTime() + 6000L, 24000L); + return switch (arg) { + case NONE -> null; + // The vehicle inventory screen and its key come later in phase 4. + case KEY_GUI -> ""; + case NAME -> def.displayName; + case ALTITUDE -> altitude; + case DATE -> new Date(); + case MC_THOR -> worldTime / 1000; + case MC_TMIN -> worldTime % 1000 * 36 / 10 / 60; + case MC_TSEC -> worldTime % 1000 * 36 / 10 % 60; + case MAX_HP -> def.maxHp; + case HP -> vehicle.hp(); + case HP_PER -> def.maxHp > 0 ? 100.0 * vehicle.hp() / def.maxHp : 0.0; + case POS_X -> vehicle.getX(); + case POS_Y -> vehicle.getY(); + case POS_Z -> vehicle.getZ(); + case MOTION_X -> vars.get(HudVariable.MOTION_X); + case MOTION_Y -> vars.get(HudVariable.MOTION_Y); + case MOTION_Z -> vars.get(HudVariable.MOTION_Z); + case INVENTORY -> 0; + case WPN_NAME -> weaponName; + case WPN_AMMO -> weaponAmmo; + case WPN_RM_AMMO -> weaponRestAmmo; + case RELOAD_PER -> reloadPercent; + case RELOAD_SEC -> reloadSeconds; + case MORTAR_DIST -> mortarDistance; + case MC_VER -> "1.21.11"; + case MOD_VER -> Platform.get().modVersion(); + case MOD_NAME -> "MCHeli Rework"; + case YAW -> rotate360(vehicle.getYRot() + 180.0); + case PITCH -> -vehicle.getXRot(); + case ROLL -> Mth.wrapDegrees(vehicle.roll()); + case PLYR_YAW -> rotate360(player.getYRot() + 180.0); + case PLYR_PITCH -> -player.getXRot(); + case TVM_POS_X, TVM_POS_Y, TVM_POS_Z, TVM_DIFF -> tvMissile(arg, vehicle); + case CAM_ZOOM -> VehicleCamera.zoom(); + case UAV_DIST -> Math.max(vehicle.stationDistance(), 0.0); + case THROTTLE -> vehicle.visibleThrottle() * 100.0; + }; + } + + private static double tvMissile(TextArg arg, AircraftEntity vehicle) { + TvMissileEntity missile = vehicle.tvMissile(); + if (missile == null) { + return 0.0; + } + return switch (arg) { + case TVM_POS_X -> missile.getX(); + case TVM_POS_Y -> missile.getY(); + case TVM_POS_Z -> missile.getZ(); + default -> missile.position().distanceTo(vehicle.position()); + }; + } + + static double rotate360(double degrees) { + double r = degrees % 360.0; + return r >= 0.0 ? r : r + 360.0; + } +} From 7f6a0a8ca4fb79579041183e19f7e484e39918bf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:44 +0800 Subject: [PATCH 0689/1461] Keep glow particles full bright through getLightColor on 1.21.11 --- .../mcheli/client/particle/GlowParticle.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java new file mode 100644 index 00000000..6d741611 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java @@ -0,0 +1,103 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.renderer.state.QuadParticleRenderState; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; +import net.minecraft.util.ARGB; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import org.jspecify.annotations.Nullable; + +/** + * {@code mcheli:glow}: a ball of light added on top of the scene. A flash swells quickly and fades + * out; a fire holds its brightness, wavering, and only fades towards the end. How bright it gets + * depends on the light where it starts, so the same explosion is a flicker at noon and lights up + * the sky at night. + */ +public class GlowParticle extends SingleQuadParticle { + private final float size; + private final float peak; + private final boolean flicker; + private float waver = 1.0F; + + GlowParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, GlowParticleOptions options, + TextureAtlasSprite sprite, float strength) { + super(level, x, y, z, sprite); + setParticleSpeed(xd, yd, zd); + this.size = options.size(); + this.peak = strength; + this.flicker = options.flicker(); + this.quadSize = size; + this.lifetime = options.lifetime(); + this.friction = 0.9F; + this.gravity = 0.0F; + this.hasPhysics = false; + setColor(ARGB.redFloat(options.color()), ARGB.greenFloat(options.color()), ARGB.blueFloat(options.color())); + this.alpha = 0.0F; + } + + @Override + public void tick() { + super.tick(); + if (flicker) { + waver = 0.7F + random.nextFloat() * 0.3F; + } + } + + @Override + public void extract(QuadParticleRenderState state, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + if (flicker) { + // Burning: hold, then die down over the last third. + alpha = peak * waver * Math.min(1.0F, (1.0F - t) * 3.0F); + } else { + // A flash: brightest at once, gone quickly. + float left = 1.0F - t; + alpha = peak * left * left; + } + super.extract(state, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return flicker ? size * (0.85F + 0.15F * waver) : size * (0.6F + 0.4F * Math.min(1.0F, t * 4.0F)); + } + + @Override + protected int getLightColor(float partialTicks) { + return LightTexture.FULL_BRIGHT; + } + + @Override + protected Layer getLayer() { + return Glow.PARTICLE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public @Nullable Particle createParticle(GlowParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd, RandomSource random) { + float strength = Glow.strength(level, BlockPos.containing(x, y, z)); + if (strength <= 0.0F) { + return null; + } + return new GlowParticle(level, x, y, z, xd, yd, zd, options, sprites.first(), strength); + } + } +} From d292cb15d9815fb8067eb51e8c0252fc43c3efa0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:44 +0800 Subject: [PATCH 0690/1461] Add client/particle/SmokeParticle.java to the 1.21.11 shared sources --- .../mcheli/client/particle/SmokeParticle.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java new file mode 100644 index 00000000..2f8240c1 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java @@ -0,0 +1,83 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.renderer.state.QuadParticleRenderState; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; + +/** + * {@code mcheli:smoke}: a puff of coloured smoke, {@code MCH_EntityParticleSmoke}. It swells to its + * full size over the first part of its life, its colour drifts towards white, it rises slowly and + * fades out at the end. The sprites are vanilla's campfire smoke. + */ +public class SmokeParticle extends SingleQuadParticle { + private final float size; + + SmokeParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, SmokeParticleOptions options, + SpriteSet sprites, RandomSource random) { + super(level, x, y, z, sprites.get(random)); + setParticleSpeed(xd, yd, zd); + // The original drew a puff 0.1 x its size out from the middle, so size 10 is two blocks across. + this.size = options.size() * 0.1F * (0.8F + random.nextFloat() * 0.4F); + this.quadSize = size * 0.4F; + this.lifetime = options.lifetime() > 0 ? options.lifetime() : 60 + random.nextInt(40); + this.friction = 0.96F; + this.gravity = -0.002F; + this.hasPhysics = true; + setColor(ARGB.redFloat(options.color()), ARGB.greenFloat(options.color()), ARGB.blueFloat(options.color())); + this.alpha = 0.9F; + } + + @Override + public void tick() { + super.tick(); + // toWhite: pale towards the brightest channel while the colour is still strong. + float max = Math.max(rCol, Math.max(gCol, bCol)); + float min = Math.min(rCol, Math.min(gCol, bCol)); + if (max - min > 0.2F) { + rCol += (max - rCol) * 0.016F; + gCol += (max - gCol) * 0.016F; + bCol += (max - bCol) * 0.016F; + } + } + + @Override + public void extract(QuadParticleRenderState state, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + alpha = 0.9F * Math.min(1.0F, (1.0F - t) * 4.0F); + super.extract(state, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return size * (0.4F + 0.6F * Math.min(1.0F, t * 3.0F)); + } + + @Override + protected Layer getLayer() { + return Glow.SMOKE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public Particle createParticle(SmokeParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd, RandomSource random) { + return new SmokeParticle(level, x, y, z, xd, yd, zd, options, sprites, random); + } + } +} From 75e8c21eec5e5fa4ddd97f66c526b6c11524a0f0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:45 +0800 Subject: [PATCH 0691/1461] Add client/render/A10Renderer.java to the 1.21.11 shared sources --- .../mcheli/client/render/A10Renderer.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java new file mode 100644 index 00000000..fc8cd4c5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +/** The close air support A-10, {@code MCH_RenderA10}. */ +public class A10Renderer extends EntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/a10.png"); + + public A10Renderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 10.5F; + } + + public static class State extends EntityRenderState { + float yaw; + float pitch; + boolean visible; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(A10Entity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.visible = entity.isVisible(); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = state.visible ? MeshCache.INSTANCE.get("", "a-10") : null; + if (mesh != null) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(-state.pitch)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutout(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From 2ac13631378aeb75d4e29c8c27cb9ef0603e071e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:45 +0800 Subject: [PATCH 0692/1461] Add client/render/AircraftRenderState.java to the 1.21.11 shared sources --- .../client/render/AircraftRenderState.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java new file mode 100644 index 00000000..d2dd770d --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java @@ -0,0 +1,77 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import org.jspecify.annotations.Nullable; + +/** + * Snapshot of a vehicle for one frame, filled in by {@link AircraftRenderer#extractRenderState}. + * Every angle is already interpolated for the frame's partial tick. + */ +public class AircraftRenderState extends EntityRenderState { + /** Definitions are immutable after loading, so the snapshot can share them. */ + public @Nullable AircraftDefinition definition; + public String texture = ""; + public float yaw; + public float pitch; + public float roll; + /** Helicopter main rotor or plane propeller angle. */ + public float rotorAngle; + /** Each helicopter blade's own angle, per rotor, so folding blades can leave the rotor's spin. */ + public float[][] bladeAngles = new float[0][]; + public float throttle; + + // Part states, 0 to 90 degrees + public float canopy; + public float landingGear; + public float hatch; + /** Rope length below the hooks while troops go down, 0 with the ropes in. */ + public float ropeLength; + /** The beams of the search lights, or false with them off. */ + public boolean searchLight; + /** Where the crew aim a turning search light, in world degrees. */ + public float searchLightYaw; + public float searchLightPitch; + public float nozzle; + public float wing; + + public float wheelRoll; + public float steer; + public final float[] roller = new float[2]; + /** Track run per side from 0 to 1, in link spacings. */ + public final float[] track = new float[2]; + public float[] rotatingParts = new float[0]; + + /** Per seat: occupant view relative to the hull, and whether anyone sits there. */ + public float[] seatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public boolean[] seatOccupied = new boolean[0]; + /** Seat of the player at this client, or -1, and whether they look through their own eyes. */ + public int localSeat = -1; + public boolean localFirstPerson; + /** Shade for the unlit glass, from the light at the vehicle. */ + public int glassColor = 0xFFFFFFFF; + /** Drawn inside a screen, as the workshop does, rather than in the world. */ + public boolean inGui; + + /** Per {@code AddPartWeapon} line, the state of the weapon it belongs to; see {@link WeaponPartState}. */ + public WeaponPartState[] weaponParts = new WeaponPartState[0]; + /** Per weapon bay, 0 to 90 degrees. */ + public float[] bays = new float[0]; + + /** How one weapon part is drawn this frame. */ + public static final class WeaponPartState { + /** Whether the part's weapon exists; without one the part follows its operator's view. */ + public boolean hasWeapon; + /** Aim relative to the weapon's default direction. */ + public float yaw; + public float pitch; + public float defaultYaw; + /** How far a turret weapon lags behind the pilot's view. */ + public float turretLag; + public float recoil; + public float barrel; + /** A missile part whose round is on its way and not reloaded yet. */ + public boolean hidden; + } +} From a1169ff5216089542ad09057864b989dedf98f85 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:46 +0800 Subject: [PATCH 0693/1461] Port AircraftRenderer to 1.21.11, where entityCutout culls and entityCutoutNoCull does not --- .../client/render/AircraftRenderer.java | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java new file mode 100644 index 00000000..ec225672 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java @@ -0,0 +1,342 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.SearchLight; +import com.jnhprojects.mcheli.content.aircraft.RepellingHook; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.AnimationState; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PartState; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.debug.DebugScreenEntries; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.gizmos.GizmoStyle; +import net.minecraft.gizmos.Gizmos; +import net.minecraft.resources.Identifier; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * Draws any MCHeli vehicle from its MQO/OBJ model, following {@code MCH_RenderAircraft}: the hull, + * the kind's own moving parts (rotors, wings, gun mounts) and then the parts every kind can have. + */ +public class AircraftRenderer extends EntityRenderer { + private static final int HULL_COLOR = 0xFF00E0FF; + private static final int GENERATED_COLOR = 0xFFFFC000; + /** Ropes take the colour of brown wool. */ + private static final Identifier ROPE = Identifier.withDefaultNamespace("textures/block/brown_wool.png"); + private static final float ROPE_WIDTH = 0.03F; + + public AircraftRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 2.0F; + } + + /** + * With F3+B on, outlines the hull boxes next to vanilla's main box: what projectiles hit and + * what other vehicles collide with. The definition's own boxes are cyan, the ones generated + * from the model amber. + */ + private static void showHull(AircraftEntity entity, AircraftDefinition def, float partialTicks) { + List hull = entity.hullBoxes(); + if (hull.size() < 2) { + return; + } + Vec3 offset = entity.getPosition(partialTicks).subtract(entity.position()); + int listed = def.boundingBoxes.size(); + try (Gizmos.TemporaryCollection ignored = Minecraft.getInstance().levelRenderer.collectPerFrameGizmos()) { + // The first is the main box, which vanilla draws itself; then the listed boxes, then the generated ones. + for (int i = 1; i < hull.size(); i++) { + Gizmos.cuboid(hull.get(i).move(offset), GizmoStyle.stroke(i <= listed ? HULL_COLOR : GENERATED_COLOR)); + } + } + } + + @Override + public AircraftRenderState createRenderState() { + return new AircraftRenderState(); + } + + @Override + public void extractRenderState(T entity, AircraftRenderState state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + AircraftDefinition def = entity.definition(); + state.definition = def; + state.texture = entity.texture(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.roll = entity.roll(partialTicks); + if (def == null) { + return; + } + if (Minecraft.getInstance().debugEntries.isCurrentlyEnabled(DebugScreenEntries.ENTITY_HITBOXES)) { + showHull(entity, def, partialTicks); + } + + state.rotorAngle = switch (entity) { + case HelicopterEntity heli -> heli.rotorAngle(partialTicks); + case PlaneEntity plane -> plane.rotorAngle(partialTicks); + default -> 0.0F; + }; + state.canopy = angle(entity.canopy(), partialTicks); + state.landingGear = angle(entity.landingGear(), partialTicks); + state.hatch = angle(entity.hatch(), partialTicks); + if (entity instanceof HelicopterEntity heli && def instanceof HelicopterDefinition heliDef) { + bladeAngles(heli, heliDef, state, partialTicks); + } + state.ropeLength = entity.isRepelling() ? entity.ropeLength() : 0.0F; + state.searchLight = entity.isSearchLightOn(); + if (state.searchLight) { + Entity aimer = entity.searchLightAimer(); + state.searchLightYaw = aimer != null ? aimer.getYRot(partialTicks) : state.yaw; + state.searchLightPitch = aimer != null ? aimer.getXRot(partialTicks) : state.pitch; + } + if (entity instanceof PlaneEntity plane) { + state.nozzle = angle(plane.nozzle(), partialTicks); + state.wing = angle(plane.wing(), partialTicks); + } else { + state.nozzle = 0.0F; + state.wing = 0.0F; + } + + AnimationState anim = entity.animation; + state.throttle = Mth.lerp(partialTicks, anim.prevThrottle, anim.throttle); + state.wheelRoll = Mth.lerp(partialTicks, anim.prevWheelRoll, anim.wheelRoll); + state.steer = Mth.lerp(partialTicks, anim.prevSteer, anim.steer); + for (int i = 0; i < 2; i++) { + state.roller[i] = Mth.lerp(partialTicks, anim.prevRoller[i], anim.roller[i]); + state.track[i] = lerpWrapped(anim.prevTrack[i], anim.track[i], partialTicks); + } + if (state.rotatingParts.length != anim.rotatingParts.length) { + state.rotatingParts = new float[anim.rotatingParts.length]; + } + for (int i = 0; i < anim.rotatingParts.length; i++) { + state.rotatingParts[i] = Mth.lerp(partialTicks, anim.prevRotatingParts[i], anim.rotatingParts[i]); + } + + int seats = def.seatAndRackCount(); + if (state.seatYaw.length != seats) { + state.seatYaw = new float[seats]; + state.seatPitch = new float[seats]; + state.seatOccupied = new boolean[seats]; + } + for (int i = 0; i < seats; i++) { + state.seatYaw[i] = anim.seatYaw(i, partialTicks); + state.seatPitch[i] = anim.seatPitch(i, partialTicks); + state.seatOccupied[i] = entity.occupant(i) != null; + } + Minecraft mc = Minecraft.getInstance(); + state.localSeat = mc.player != null ? entity.seatOf(mc.player) : -1; + state.localFirstPerson = mc.options.getCameraType().isFirstPerson(); + state.glassColor = glassColor(entity, state.lightCoords); + extractWeapons(entity, def, state, partialTicks); + } + + /** + * Glass is drawn without the light map, so shade it by hand: as bright as the brighter of block + * light and the sky light left after night or rain darkens it. + */ + private static int glassColor(AircraftEntity entity, int lightCoords) { + int sky = Math.max(LightTexture.sky(lightCoords) - entity.level().getSkyDarken(), 0); + int light = Math.max(LightTexture.block(lightCoords), sky); + int shade = (int) ((0.3F + 0.7F * light / 15.0F) * 255.0F); + return 0xFF000000 | shade << 16 | shade << 8 | shade; + } + + /** {@code renderWeapon}'s inputs: each part's weapon aim, recoil, barrel spin and reload state. */ + private static void extractWeapons(AircraftEntity entity, AircraftDefinition def, AircraftRenderState state, float partialTicks) { + VehicleWeapons weapons = entity.weapons(); + int parts = def.weaponParts.size(); + if (state.weaponParts.length != parts) { + state.weaponParts = new AircraftRenderState.WeaponPartState[parts]; + for (int i = 0; i < parts; i++) { + state.weaponParts[i] = new AircraftRenderState.WeaponPartState(); + } + } + WeaponSet previous = null; + int mount = 0; + for (int i = 0; i < parts; i++) { + WeaponPart part = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState out = state.weaponParts[i]; + WeaponSet set = weapons != null && !part.weapons().isEmpty() ? weapons.byName(part.weapons().getFirst()) : null; + if (set != previous) { + mount = 0; + previous = set; + } + out.hasWeapon = set != null; + if (set == null) { + continue; + } + float yaw = set.rotationYaw; + float prevYaw = set.prevRotationYaw; + if (yaw - prevYaw > 180.0F) { + prevYaw += 360.0F; + } else if (yaw - prevYaw < -180.0F) { + prevYaw -= 360.0F; + } + out.yaw = Mth.lerp(partialTicks, prevYaw, yaw) - set.defaultRotationYaw; + out.pitch = Mth.lerp(partialTicks, set.prevRotationPitch, set.rotationPitch); + out.defaultYaw = set.defaultRotationYaw; + out.turretLag = set.rotationTurretYaw; + out.barrel = Mth.lerp(partialTicks, set.prevBarrel, set.barrel); + // A part shared by several weapons kicks back with whichever fired hardest. + float recoil = set.recoil(0, partialTicks); + for (String name : part.weapons()) { + WeaponSet other = weapons.byName(name); + if (other != null) { + recoil = Math.max(recoil, other.recoil(0, partialTicks)); + } + } + out.recoil = recoil; + out.hidden = part.missile() && weapons.isMountUsed(weapons.indexOf(set.name()), mount); + mount++; + } + if (state.bays.length != def.weaponBays.size()) { + state.bays = new float[def.weaponBays.size()]; + } + for (int i = 0; i < state.bays.length; i++) { + state.bays[i] = weapons != null ? weapons.bayAngle(i, partialTicks) : 0.0F; + } + } + + private static float angle(@Nullable PartState part, float partialTicks) { + return part != null ? part.angle(partialTicks) : 0.0F; + } + + /** Interpolates a value that wraps from 1 back to 0. */ + private static float lerpWrapped(float prev, float current, float partialTicks) { + float d = current - prev; + if (d > 0.5F) { + d -= 1.0F; + } else if (d < -0.5F) { + d += 1.0F; + } + float v = prev + d * partialTicks; + return v - Mth.floor(v); + } + + @Override + protected AABB getBoundingBoxForCulling(T entity) { + // The hit box is far smaller than most models, so cull on a generous box instead. + AircraftDefinition def = entity.definition(); + double reach = def != null ? Math.max(8.0, def.markerWidth * 4.0) : 8.0; + return entity.getBoundingBox().inflate(reach, reach / 2.0, reach); + } + + @Override + public void submit(AircraftRenderState state, PoseStack poseStack, SubmitNodeCollector collector, CameraRenderState camera) { + AircraftDefinition def = state.definition; + Mesh mesh = def != null ? MeshCache.INSTANCE.get(def.folder(), def.name()) : null; + if (mesh != null) { + // Canopies and windshields are semi-transparent texels; PartRenderer draws those + // triangles blended, as the original did, and everything else as cutout. + poseStack.pushPose(); + poseStack.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + poseStack.mulPose(Axis.XP.rotationDegrees(state.pitch)); + poseStack.mulPose(Axis.ZP.rotationDegrees(state.roll)); + new PartRenderer(def, mesh, state, poseStack, collector, texture(def.folder(), state.texture), state.lightCoords, + OverlayTexture.NO_OVERLAY, state.glassColor).submitAll(); + if (state.ropeLength < 0.0F) { + ropes(def, state, poseStack, collector); + } + if (state.searchLight) { + searchLights(def, state, poseStack, collector); + } + poseStack.popPose(); + } + super.submit(state, poseStack, collector, camera); + } + + /** + * {@code renderLight}: each lamp's cone. A turning light follows the crew's aim; a fixed one + * points along the vehicle, and a steering one also turns with the front wheels. + */ + private static void searchLights(AircraftDefinition def, AircraftRenderState state, PoseStack pose, SubmitNodeCollector collector) { + for (SearchLight light : def.searchLights) { + pose.pushPose(); + pose.translate(light.position().x(), light.position().y(), light.position().z()); + if (!light.fixed()) { + pose.mulPose(Axis.YN.rotationDegrees(state.searchLightYaw - state.yaw + light.yaw())); + pose.mulPose(Axis.XP.rotationDegrees(state.searchLightPitch + 90.0F - state.pitch + light.pitch())); + } else { + float steer = light.steering() ? -state.steer * light.steeringRotation() : 0.0F; + pose.mulPose(Axis.YN.rotationDegrees(light.yaw() + steer)); + pose.mulPose(Axis.XP.rotationDegrees(90.0F + light.pitch())); + } + Glow.cone(pose, collector, light.height(), light.width() / 2.0F, light.colorStart(), light.colorEnd()); + pose.popPose(); + } + } + + private static void bladeAngles(HelicopterEntity heli, HelicopterDefinition def, AircraftRenderState state, float partialTicks) { + if (state.bladeAngles.length != def.rotors.size()) { + state.bladeAngles = new float[def.rotors.size()][]; + } + for (int r = 0; r < def.rotors.size(); r++) { + RotorDefinition rotor = def.rotors.get(r); + int blades = Math.max(rotor.blades(), 1); + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + if (state.bladeAngles[r] == null || state.bladeAngles[r].length != blades) { + state.bladeAngles[r] = new float[blades]; + } + for (int b = 0; b < blades; b++) { + state.bladeAngles[r][b] = heli.bladeAngle(r, b, step, partialTicks); + } + } + } + + /** {@code renderRope}: a dark line straight down from each hook, in the vehicle's own frame. */ + private static void ropes(AircraftDefinition def, AircraftRenderState state, PoseStack pose, SubmitNodeCollector collector) { + int light = state.lightCoords; + float length = state.ropeLength; + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutNoCull(ROPE), (p, buffer) -> { + for (RepellingHook hook : def.repellingHooks) { + float x = (float) hook.position().x(); + float y = (float) hook.position().y(); + float z = (float) hook.position().z(); + // Two thin crossed strips, so the rope shows from every side. + ropeStrip(p, buffer, x - ROPE_WIDTH, z, x + ROPE_WIDTH, z, y, length, light); + ropeStrip(p, buffer, x, z - ROPE_WIDTH, x, z + ROPE_WIDTH, y, length, light); + } + }); + } + + private static void ropeStrip(PoseStack.Pose p, VertexConsumer buffer, float x0, float z0, float x1, float z1, float y, float length, int light) { + ropeVertex(p, buffer, x0, y, z0, 0.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y, z1, 1.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y + length, z1, 1.0F, 1.0F, light); + ropeVertex(p, buffer, x0, y + length, z0, 0.0F, 1.0F, light); + } + + private static void ropeVertex(PoseStack.Pose p, VertexConsumer buffer, float x, float y, float z, float u, float v, int light) { + buffer.addVertex(p, x, y, z).setColor(0xFFFFFFFF).setUv(u, v).setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(p, 0.0F, 1.0F, 0.0F); + } + + private static Identifier texture(String folder, String name) { + return RuntimeAssets.texture(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + folder + "/" + name + ".png")); + } +} From ff36dbdd5100b9260431fb79275c8b7023cf37f2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:46 +0800 Subject: [PATCH 0694/1461] Add client/render/ChainRenderer.java to the 1.21.11 shared sources --- .../mcheli/client/render/ChainRenderer.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java new file mode 100644 index 00000000..62c893c5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java @@ -0,0 +1,93 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +/** + * {@code MCH_RenderChain}: a line of links from the towed end to the end that pulls, one link + * every 0.95 blocks, each turned along the chain. + */ +public class ChainRenderer extends EntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/chain.png"); + private static final double LINK = 0.95; + private static final int GREY = 0xFF808080; + + public ChainRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + /** Towed end, relative to the chain entity. */ + Vec3 from = Vec3.ZERO; + /** From the towed end to the end that pulls. */ + Vec3 span = Vec3.ZERO; + } + + /** The chain hangs over the end that pulls but reaches to the other end; keep both in view. */ + @Override + protected AABB getBoundingBoxForCulling(ChainEntity chain) { + AABB box = super.getBoundingBoxForCulling(chain); + Entity towed = chain.towed(); + return towed != null ? box.minmax(towed.getBoundingBox()) : box; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ChainEntity chain, State state, float partialTicks) { + super.extractRenderState(chain, state, partialTicks); + Entity tow = chain.tow(); + Entity towed = chain.towed(); + if (tow == null || towed == null) { + state.span = Vec3.ZERO; + return; + } + Vec3 here = chain.getPosition(partialTicks); + Vec3 start = towed.getPosition(partialTicks); + state.from = start.subtract(here); + state.span = tow.getPosition(partialTicks).subtract(start); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("", "chain"); + double length = state.span.length(); + if (mesh != null && length > LINK) { + Vec3 step = state.span.scale(LINK / length); + float yaw = (float) Math.toDegrees(Math.atan2(step.z, step.x)); + float pitch = (float) Math.toDegrees(Math.atan2(step.y, Math.sqrt(step.x * step.x + step.z * step.z))); + int light = state.lightCoords; + pose.pushPose(); + pose.translate(state.from); + for (double left = length; left > LINK; left -= LINK) { + pose.translate(step); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-yaw)); + pose.mulPose(Axis.ZP.rotationDegrees(pitch)); + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutNoCull(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, GREY)); + pose.popPose(); + } + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From 761a7b51fd43e16b03fd18a14ed3ba22a27377a6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:47 +0800 Subject: [PATCH 0695/1461] Add client/render/DraftingTableRenderer.java to the 1.21.11 shared sources --- .../client/render/DraftingTableRenderer.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java new file mode 100644 index 00000000..edfd0742 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** {@code MCH_DraftingTableRenderer}: the original model, turned to the block's eighth of a circle. */ +public class DraftingTableRenderer implements BlockEntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/block/drafting_table.png"); + /** The original greyed the table a little. */ + private static final int TINT = 0xFFBFBFBF; + + public DraftingTableRenderer(BlockEntityRendererProvider.Context context) { + } + + public static class State extends BlockEntityRenderState { + int rotation; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(DraftingTableBlockEntity table, State state, float partialTicks, Vec3 cameraPosition, + ModelFeatureRenderer.@Nullable CrumblingOverlay breakProgress) { + BlockEntityRenderer.super.extractRenderState(table, state, partialTicks, cameraPosition, breakProgress); + state.rotation = table.getBlockState().getValue(DraftingTableBlock.ROTATION); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("blocks", "drafting_table"); + if (mesh == null) { + return; + } + pose.pushPose(); + pose.translate(0.5F, 0.0F, 0.5F); + pose.mulPose(Axis.YP.rotationDegrees(-state.rotation * 45.0F + 180.0F)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutNoCull(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, TINT)); + pose.popPose(); + } +} From 3097f5ff4ee1f82b3f9510695d56e07b3b558adf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:47 +0800 Subject: [PATCH 0696/1461] Add client/render/EquipmentRenderer.java to the 1.21.11 shared sources --- .../client/render/EquipmentRenderer.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java new file mode 100644 index 00000000..330a2800 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; + +/** + * Equipment drawn as one model from the models root with the texture of the same name, turned to + * the entity's heading: the container, {@code MCH_RenderContainer}. + */ +public class EquipmentRenderer extends EntityRenderer { + private final String model; + private final Identifier texture; + private final float yOffset; + private final int tint; + + /** + * @param model the file name under {@code models/}, without extension + * @param yOffset how far the model sits below the entity's position + * @param tint ARGB the model is multiplied by; the original greyed the container a little + */ + public EquipmentRenderer(EntityRendererProvider.Context context, String model, float yOffset, int tint, float shadow) { + super(context); + this.model = model; + this.texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + model + ".png"); + this.yOffset = yOffset; + this.tint = tint; + this.shadowRadius = shadow; + } + + public static class State extends EntityRenderState { + float yaw; + float pitch; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(T entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("", model); + if (mesh != null) { + pose.pushPose(); + pose.translate(0.0F, -yOffset, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutNoCull(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, tint)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From 3247624176aef026673a7f947ee47705ba7b272a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:48 +0800 Subject: [PATCH 0697/1461] Add client/render/FlareRenderer.java to the 1.21.11 shared sources --- .../mcheli/client/render/FlareRenderer.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java new file mode 100644 index 00000000..9f8f2f9d --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and the burning debris of a crash were only their particle trails. Not in the original: + * a burning flare now glows, which is what makes a flare salvo light up the night sky. Smoke + * grenades stay invisible. + */ +public class FlareRenderer extends EntityRenderer { + private static final float RADIUS = 1.4F; + + public FlareRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + float glow; + float flicker = 1.0F; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(FlareEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.glow = entity.isSmoke() || entity.isInWater() ? 0.0F : Glow.strength(entity.level(), entity.blockPosition()); + // A cheap waver keyed to the entity, so neighbouring flares do not pulse in step. + state.flicker = 0.8F + 0.2F * (float) Math.sin((entity.tickCount + partialTicks) * 1.7F + entity.getId()); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Glow.halo(pose, collector, camera, Vec3.ZERO, RADIUS * state.flicker, GlowParticleOptions.FIRE, state.glow); + Glow.halo(pose, collector, camera, Vec3.ZERO, RADIUS * 0.35F, GlowParticleOptions.FLASH, state.glow); + super.submit(state, pose, collector, camera); + } +} From 53623c4e05a9ae16bbb32ce6c6a3ae7352112dda Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:48 +0800 Subject: [PATCH 0698/1461] Add client/render/GlassSplit.java to the 1.21.11 shared sources --- .../mcheli/client/render/GlassSplit.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java new file mode 100644 index 00000000..d2a46857 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.resources.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Sorts a model's triangles into opaque ones and see-through glass, by the alpha of the texture + * under each triangle. + * + *

Drawing a whole vehicle translucent made its glass write depth before the game drew water, + * so water behind a canopy vanished. Opaque triangles now draw as cutout, like any entity, and + * only the glass draws translucent, through a type that leaves the depth buffer alone. + */ +final class GlassSplit { + static final GlassSplit INSTANCE = new GlassSplit(); + private static final Logger LOGGER = LoggerFactory.getLogger(GlassSplit.class); + /** Texels between these alphas are glass; fully clear ones are cut out, solid ones opaque. */ + private static final int MIN_GLASS_ALPHA = 10; + private static final int MAX_GLASS_ALPHA = 245; + + record Split(List opaque, List glass) { + static final Split EMPTY = new Split(List.of(), List.of()); + } + + private record Alpha(int width, int height, byte[] values) { + int at(float u, float v) { + int x = Math.floorMod((int) Math.floor(u * width), width); + int y = Math.floorMod((int) Math.floor(v * height), height); + return values[y * width + x] & 0xFF; + } + } + + private final Map> textures = new HashMap<>(); + private final Map, Map> splits = new IdentityHashMap<>(); + private final Map, Map, List>> buckets = new IdentityHashMap<>(); + + private GlassSplit() { + } + + /** Called from the render thread only. */ + Split split(List groups, Identifier texture) { + if (groups.isEmpty()) { + return Split.EMPTY; + } + return splits.computeIfAbsent(groups, g -> new HashMap<>()).computeIfAbsent(texture, t -> compute(groups, t)); + } + + /** + * A part's objects by the texture each draws with, for a vehicle with {@code PartTexture} lines. + * The lists stay the same objects from frame to frame, so {@link #split} can remember them too. + * Called from the render thread only. + */ + List buckets(List groups, Map overrides) { + return buckets.computeIfAbsent(groups, g -> new HashMap<>()) + .computeIfAbsent(Map.copyOf(overrides), o -> PartTextures.assign(groups, o)); + } + + void clear() { + textures.clear(); + splits.clear(); + buckets.clear(); + } + + private Split compute(List groups, Identifier texture) { + Alpha alpha = textures.computeIfAbsent(texture, GlassSplit::load).orElse(null); + if (alpha == null) { + return new Split(groups, List.of()); + } + List opaque = new ArrayList<>(); + List glass = new ArrayList<>(); + for (MeshGroup group : groups) { + split(group, alpha, opaque, glass); + } + return new Split(List.copyOf(opaque), List.copyOf(glass)); + } + + private static void split(MeshGroup group, Alpha alpha, List opaque, List glass) { + float[] uv = group.uvs(); + int triangles = group.vertexCount() / 3; + boolean[] isGlass = new boolean[triangles]; + int glassCount = 0; + for (int t = 0; t < triangles; t++) { + int i = t * 6; + float u = (uv[i] + uv[i + 2] + uv[i + 4]) / 3.0F; + float v = (uv[i + 1] + uv[i + 3] + uv[i + 5]) / 3.0F; + int a = alpha.at(u, v); + if (a > MIN_GLASS_ALPHA && a < MAX_GLASS_ALPHA) { + isGlass[t] = true; + glassCount++; + } + } + if (glassCount == 0) { + opaque.add(group); + } else if (glassCount == triangles) { + glass.add(group); + } else { + opaque.add(pick(group, isGlass, false, triangles - glassCount)); + glass.add(pick(group, isGlass, true, glassCount)); + } + } + + private static MeshGroup pick(MeshGroup group, boolean[] isGlass, boolean wanted, int count) { + float[] p = new float[count * 9]; + float[] n = new float[count * 9]; + float[] uv = new float[count * 6]; + int out = 0; + for (int t = 0; t < isGlass.length; t++) { + if (isGlass[t] != wanted) { + continue; + } + System.arraycopy(group.positions(), t * 9, p, out * 9, 9); + System.arraycopy(group.normals(), t * 9, n, out * 9, 9); + System.arraycopy(group.uvs(), t * 6, uv, out * 6, 6); + out++; + } + return new MeshGroup(group.name(), p, n, uv); + } + + private static Optional load(Identifier texture) { + try (InputStream in = RuntimeAssets.open(texture)) { + if (in == null) { + return Optional.empty(); + } + return Optional.of(read(in)); + } catch (IOException | RuntimeException e) { + LOGGER.warn("Could not read {} to find its glass; drawing it opaque", texture, e); + return Optional.empty(); + } + } + + private static Alpha read(InputStream in) throws IOException { + try (NativeImage image = NativeImage.read(in)) { + int w = image.getWidth(); + int h = image.getHeight(); + byte[] values = new byte[w * h]; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + values[y * w + x] = (byte) (image.getPixel(x, y) >>> 24); + } + } + return new Alpha(w, h, values); + } + } +} From ac9179616d0a1993f91afda0b369242a51187841 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:49 +0800 Subject: [PATCH 0699/1461] Build the glow pipelines with the blend and depth write calls of 1.21.11 --- .../mcheli/client/render/Glow.java | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java new file mode 100644 index 00000000..73947d7f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java @@ -0,0 +1,206 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.effect.NightGlow; +import com.mojang.blaze3d.pipeline.BlendFunction; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.client.particle.SingleQuadParticle; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.rendertype.RenderSetup; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.util.ARGB; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.phys.Vec3; + +/** + * Not in the original: light that adds to the scene instead of covering it, for tracers, rocket + * motors, flares and fireballs. Everything here blends additively and leaves the depth buffer + * alone, so overlapping glows brighten each other and never cut holes in the smoke behind them. + * With order independent transparency on, the vanilla translucent pipelines stand in. + */ +public final class Glow { + /** A white disc fading to nothing at the rim; also the {@code mcheli:glow} particle sprite. */ + public static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/particle/glow.png"); + + private static final RenderPipeline PARTICLE_PIPELINE = RenderPipeline.builder(RenderPipelines.PARTICLE_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/glow_particle")) + .withBlend(BlendFunction.LIGHTNING) + .withDepthWrite(false) + .build(); + + /** + * Vanilla's translucent particles with depth writes off. A puff's clear edge would otherwise + * hide the puffs drawn after it, and a smoke cloud is mostly overlapping puffs. + */ + private static final RenderPipeline SMOKE_PIPELINE = RenderPipeline.builder(RenderPipelines.PARTICLE_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/smoke_particle")) + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .build(); + + private static final RenderPipeline ENTITY_PIPELINE = RenderPipeline.builder(RenderPipelines.MATRICES_FOG_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/glow")) + .withVertexShader("core/entity") + .withFragmentShader("core/entity") + .withShaderDefine("EMISSIVE") + .withShaderDefine("NO_OVERLAY") + .withShaderDefine("NO_CARDINAL_LIGHTING") + .withSampler("Sampler0") + .withVertexFormat(DefaultVertexFormat.NEW_ENTITY, VertexFormat.Mode.QUADS) + .withBlend(BlendFunction.LIGHTNING) + .withDepthWrite(false) + .build(); + + /** The particle layer for {@code mcheli:glow}. */ + @SuppressWarnings("deprecation") // Vanilla's own particle layers still name the atlas this way. + public static final SingleQuadParticle.Layer PARTICLE_LAYER = new SingleQuadParticle.Layer( + true, TextureAtlas.LOCATION_PARTICLES, PARTICLE_PIPELINE); + + /** The particle layer for {@code mcheli:smoke}. */ + @SuppressWarnings("deprecation") + public static final SingleQuadParticle.Layer SMOKE_LAYER = new SingleQuadParticle.Layer( + true, TextureAtlas.LOCATION_PARTICLES, SMOKE_PIPELINE); + + private static final RenderType HALO = RenderType.create(MCHeli.MOD_ID + "_glow", RenderSetup.builder(ENTITY_PIPELINE) + .withTexture("Sampler0", TEXTURE) + .sortOnUpload() + .createRenderSetup()); + + private static final int FULL_BRIGHT = LightTexture.FULL_BRIGHT; + + private Glow() { + } + + /** + * Hands our pipelines to Iris, so a shader pack draws them with a matching program: the halos + * and tracers as emissive entities, the glow and smoke particles as translucent particles. + * Called once the mods are loaded; without Iris it does nothing. + */ + public static void assignShaderPrograms() { + IrisCompat.assign(ENTITY_PIPELINE, "EMISSIVE_ENTITIES"); + IrisCompat.assign(PARTICLE_PIPELINE, "PARTICLES_TRANSLUCENT"); + IrisCompat.assign(SMOKE_PIPELINE, "PARTICLES_TRANSLUCENT"); + } + + /** + * How strongly something at {@code pos} glows: faint by day, full at night, in caves and + * under water. 0 when the player turned the glow off. + */ + public static float strength(Level level, BlockPos pos) { + if (!ClientConfig.nightGlow()) { + return 0.0F; + } + return NightGlow.strength(level.getBrightness(LightLayer.BLOCK, pos), level.getBrightness(LightLayer.SKY, pos), level.getSkyDarken()); + } + + /** + * A disc of light facing the camera, centred {@code offset} away from the current pose. + * + * @param color RGB; {@code strength} becomes the alpha, which scales the light added + */ + public static void halo(PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera, Vec3 offset, float radius, int color, + float strength) { + if (strength <= 0.0F || radius <= 0.0F) { + return; + } + int argb = ARGB.color(alpha(strength), color); + pose.pushPose(); + pose.translate(offset); + pose.mulPose(camera.orientation); + collector.submitCustomGeometry(pose, HALO, (p, buffer) -> { + // Both windings, so culling never hides it whichever way the quad ends up facing. + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + }); + pose.popPose(); + } + + /** + * A tracer: a ribbon of light from the current pose back along {@code tail}, turned to face + * the camera, bright at the head and fading out towards the end. + * + * @param toCamera from the camera to the head, so the ribbon can be turned side on to the view + */ + public static void streak(PoseStack pose, SubmitNodeCollector collector, Vec3 toCamera, Vec3 tail, float width, int color, float strength) { + if (strength <= 0.0F || tail.lengthSqr() < 1.0E-4) { + return; + } + Vec3 side = tail.cross(toCamera); + if (side.lengthSqr() < 1.0E-8) { + // Seen exactly end on; the halo at the head covers this. + return; + } + side = side.normalize().scale(width); + int head = ARGB.color(alpha(strength), color); + int end = ARGB.color(0, color); + float sx = (float) side.x; + float sy = (float) side.y; + float sz = (float) side.z; + float tx = (float) tail.x; + float ty = (float) tail.y; + float tz = (float) tail.z; + collector.submitCustomGeometry(pose, HALO, (p, buffer) -> { + // The middle row of the disc texture gives the ribbon soft edges. + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + }); + } + + /** + * A search light's beam, {@code renderLight}: a cone {@code height} long along +Y to a disc + * {@code radius} across, fading from {@code start} at the lamp to {@code end} at the rim (ARGB). + */ + public static void cone(PoseStack pose, SubmitNodeCollector collector, float height, float radius, int start, int end) { + collector.submitCustomGeometry(pose, HALO, (p, buffer) -> { + int segments = 24; + for (int i = 0; i < segments; i++) { + float a0 = i * Mth.TWO_PI / segments; + float a1 = (i + 1) * Mth.TWO_PI / segments; + // A quad with the apex twice is the fan's triangle; the texture's bright middle colours it. + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, Mth.sin(a0) * radius, height, Mth.cos(a0) * radius, 0.5F, 0.5F, end); + vertex(buffer, p, Mth.sin(a1) * radius, height, Mth.cos(a1) * radius, 0.5F, 0.5F, end); + } + }); + } + + private static int alpha(float strength) { + return Math.clamp(Math.round(strength * 255.0F), 0, 255); + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float x, float y, float z, float u, float v, int argb) { + buffer.addVertex(pose, x, y, z) + .setColor(argb) + .setUv(u, v) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(FULL_BRIGHT) + .setNormal(pose, 0.0F, 1.0F, 0.0F); + } +} From 1f83b1581346abc343af9fb6a8509774d3098dd0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:49 +0800 Subject: [PATCH 0700/1461] Add client/render/GltdRenderer.java to the 1.21.11 shared sources --- .../mcheli/client/render/GltdRenderer.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java new file mode 100644 index 00000000..fed95cc9 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * {@code MCH_RenderGLTD}: the tripod, its head turned to where the viewer aims, and a faint red + * laser out of the head while someone uses it. The viewer's own head part is left out in first + * person, where it would fill the view. + */ +public class GltdRenderer extends EntityRenderer { + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gltd.png"); + private static final float HEAD_PIVOT = 0.45F; + private static final double LASER_LENGTH = 100.0; + private static final int LASER = 0xFF2020; + + public GltdRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + public static class State extends EntityRenderState { + float yaw; + float headYaw; + float headPitch; + boolean drawHead = true; + boolean laser; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(GltdEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + Entity viewer = entity.getFirstPassenger(); + Minecraft mc = Minecraft.getInstance(); + state.laser = viewer != null; + state.drawHead = viewer == null || viewer != mc.player || !mc.options.getCameraType().isFirstPerson(); + state.headYaw = viewer != null ? viewer.getYRot(partialTicks) : state.yaw; + state.headPitch = viewer != null ? viewer.getXRot(partialTicks) : 0.0F; + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = MeshCache.INSTANCE.get("", "gltd"); + if (mesh != null) { + int light = state.lightCoords; + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + draw(mesh.part("$body"), pose, collector, light); + pose.popPose(); + if (state.drawHead) { + pose.pushPose(); + pose.translate(0.0F, HEAD_PIVOT, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-state.headYaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.headPitch)); + pose.translate(0.0F, -HEAD_PIVOT, 0.0F); + draw(mesh.part("$head"), pose, collector, light); + pose.popPose(); + } + } + if (state.laser) { + float yaw = state.headYaw * Mth.DEG_TO_RAD; + float pitch = state.headPitch * Mth.DEG_TO_RAD; + Vec3 look = new Vec3(-Mth.sin(yaw) * Mth.cos(pitch), -Mth.sin(pitch), Mth.cos(yaw) * Mth.cos(pitch)); + Vec3 head = new Vec3(0.0, HEAD_PIVOT + 0.2, 0.0); + Vec3 toCamera = new Vec3(state.x, state.y, state.z).add(head).subtract(camera.pos); + pose.pushPose(); + pose.translate(head); + // Brightest at the tripod, fading out along the beam. + Glow.streak(pose, collector, toCamera, look.scale(LASER_LENGTH), 0.02F, LASER, 0.25F); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } + + private static void draw(List groups, PoseStack pose, SubmitNodeCollector collector, int light) { + if (!groups.isEmpty()) { + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutNoCull(TEXTURE), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY)); + } + } +} From fb01f3ae1c1f6fbcfd91a7ebf29bbd1d72a783c3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:50 +0800 Subject: [PATCH 0701/1461] Add client/render/IrisCompat.java to the 1.21.11 shared sources --- .../mcheli/client/render/IrisCompat.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java new file mode 100644 index 00000000..3803102e --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.pipeline.RenderPipeline; + +import java.lang.reflect.Method; + +/** + * Tells Iris which shader program draws our own render pipelines. Iris only knows the vanilla + * pipelines and draws any other with a program whose vertex layout does not match, which throws + * the vertices far off: the glows turn into long coloured lines running to the horizon. Iris is + * optional, so its API is looked up by reflection instead of compiled against. + */ +final class IrisCompat { + private static final String API = "net.irisshaders.iris.api.v0.IrisApi"; + private static final String PROGRAM = "net.irisshaders.iris.api.v0.IrisProgram"; + + private IrisCompat() { + } + + /** + * Assigns {@code pipeline} to the Iris program named {@code program}, such as + * {@code EMISSIVE_ENTITIES}. Does nothing without Iris. + * + * @return whether Iris took it + */ + static boolean assign(RenderPipeline pipeline, String program) { + Class api; + try { + api = Class.forName(API); + } catch (ClassNotFoundException absent) { + MCHeli.LOGGER.debug("Iris is not installed, {} keeps its own shader", pipeline.getLocation()); + return false; + } + try { + Class programs = Class.forName(PROGRAM); + Object instance = api.getMethod("getInstance").invoke(null); + Object target = programs.getField(program).get(null); + Method assign = api.getMethod("assignPipeline", RenderPipeline.class, programs); + assign.invoke(instance, pipeline, target); + MCHeli.LOGGER.debug("Iris draws {} with its {} program", pipeline.getLocation(), program); + return true; + } catch (ReflectiveOperationException | LinkageError e) { + // An Iris without this API: glows may look wrong under a shader pack, nothing worse. + MCHeli.LOGGER.warn("Could not hand {} to Iris, glows may look wrong with shaders: {}", pipeline.getLocation(), e.toString()); + return false; + } + } +} From 55e21aa215bd59374ceed89acf2487b0f0407b71 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:50 +0800 Subject: [PATCH 0702/1461] Add client/render/MeshCache.java to the 1.21.11 shared sources --- .../mcheli/client/render/MeshCache.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java new file mode 100644 index 00000000..57dad7e5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshLoader; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Lazily loads vehicle models, from the resource packs (so they can replace them) or else from the + * runtime roots of {@link RuntimeAssets}. Models are parsed the first time a vehicle of that kind + * is drawn, and dropped on resource reload and on content reload ({@link #clear}). + */ +public final class MeshCache implements ResourceManagerReloadListener { + public static final MeshCache INSTANCE = new MeshCache(); + private static final Logger LOGGER = LoggerFactory.getLogger(MeshCache.class); + private static final String[] EXTENSIONS = {".mqo", ".obj"}; + + private final Map> meshes = new ConcurrentHashMap<>(); + + private MeshCache() { + } + + /** + * @param folder content folder, for example {@code helicopters}, or empty for the models root + * @param name definition name + * @return the mesh, or {@code null} if no model exists or it failed to load + */ + public @Nullable Mesh get(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, true)).orElse(null); + } + + /** Like {@link #get}, for optional models such as separate part files; a missing one is not logged. */ + public @Nullable Mesh find(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, false)).orElse(null); + } + + private static Optional load(String folder, String name, boolean required) { + for (String extension : EXTENSIONS) { + Identifier id = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, + (folder.isEmpty() ? "models/" : "models/" + folder + "/") + name + extension); + try (InputStream in = RuntimeAssets.open(id)) { + if (in == null) { + continue; + } + Mesh mesh = MeshLoader.load(id.toString(), in); + LOGGER.debug("Loaded {}", mesh); + return Optional.of(mesh); + } catch (IOException | RuntimeException e) { + LOGGER.error("Failed to load model {}", id, e); + return Optional.empty(); + } + } + if (required) { + LOGGER.warn("No model found for {}/{}", folder, name); + } + return Optional.empty(); + } + + /** Forgets every model and glass split, so the next frame reads them again from disk. */ + public void clear() { + meshes.clear(); + GlassSplit.INSTANCE.clear(); + } + + @Override + public void onResourceManagerReload(ResourceManager resourceManager) { + clear(); + RuntimeAssets.clearTextures(); + } +} From 81f771f73722e705a5ae8fd192053a9807bc8294 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:51 +0800 Subject: [PATCH 0703/1461] Add client/render/MeshRenderer.java to the 1.21.11 shared sources --- .../mcheli/client/render/MeshRenderer.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java new file mode 100644 index 00000000..47cc66e3 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java @@ -0,0 +1,88 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.List; + +/** Streams {@link MeshGroup} triangles into an entity render type. */ +final class MeshRenderer { + private static final int WHITE = 0xFFFFFFFF; + + private MeshRenderer() { + } + + /** + * Entity render types draw quads, so each triangle is sent as a quad whose last corner repeats + * the third one. The GPU discards the degenerate half for free. + */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay) { + emit(groups, pose, buffer, light, overlay, WHITE); + } + + /** As above, tinted with an ARGB colour, for bullets drawn in their {@code BulletColor}. */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + /** + * As above, but only the triangles that face the camera. No vanilla type is both culled and + * free of depth writes, so glass is culled here. Back to back glass faces would otherwise sort + * in an arbitrary order and swap places as the view moves. + * + *

Entities are submitted in world-aligned space with the camera at the origin, so a front + * face (counter-clockwise, as in the original's GL setup) is one whose normal points back + * towards the origin. + */ + static void emitFrontFaces(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + Matrix4f m = pose.pose(); + Vector3f a = new Vector3f(); + Vector3f b = new Vector3f(); + Vector3f c = new Vector3f(); + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + m.transformPosition(p[v * 3], p[v * 3 + 1], p[v * 3 + 2], a); + m.transformPosition(p[v * 3 + 3], p[v * 3 + 4], p[v * 3 + 5], b); + m.transformPosition(p[v * 3 + 6], p[v * 3 + 7], p[v * 3 + 8], c); + b.sub(a); + c.sub(a); + if (b.cross(c).dot(a) >= 0.0F) { + continue; + } + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float[] p, float[] n, float[] uv, int i, int light, int overlay, int color) { + int a = i * 3; + int t = i * 2; + buffer.addVertex(pose, p[a], p[a + 1], p[a + 2]) + .setColor(color) + .setUv(uv[t], uv[t + 1]) + .setOverlay(overlay) + .setLight(light) + .setNormal(pose, n[a], n[a + 1], n[a + 2]); + } +} From 0cdf4c9f33a6bd5a24b58088308efc04c5b5a6bd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:51 +0800 Subject: [PATCH 0704/1461] Add client/render/ParachuteRenderer.java to the 1.21.11 shared sources --- .../client/render/ParachuteRenderer.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java new file mode 100644 index 00000000..061c6f0f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +import java.util.List; + +/** + * {@code MCH_RenderParachute}: the item's round canopy, or the ejection seat with its canopy, + * from the models the import brought in. + */ +public class ParachuteRenderer extends EntityRenderer { + private static final Identifier ROUND = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute1.png"); + private static final Identifier SEAT = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute2.png"); + + public ParachuteRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + public static class State extends EntityRenderState { + float yaw; + ParachuteEntity.Kind kind = ParachuteEntity.Kind.ITEM; + boolean open; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ParachuteEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.kind = entity.kind(); + state.open = entity.isOpen(); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + List groups = groups(state); + if (!groups.isEmpty()) { + Identifier texture = state.kind == ParachuteEntity.Kind.ITEM ? ROUND : SEAT; + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutNoCull(texture), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } + + private static List groups(State state) { + if (state.kind == ParachuteEntity.Kind.ITEM) { + Mesh mesh = MeshCache.INSTANCE.get("", "parachute1"); + return mesh != null ? mesh.groups() : List.of(); + } + Mesh mesh = MeshCache.INSTANCE.get("", "parachute2"); + boolean canopy = state.kind == ParachuteEntity.Kind.DROP || state.open; + return mesh != null ? mesh.part(canopy ? "$parachute" : "$seat") : List.of(); + } +} From 45673e6747843a4cb6c724af21f071d0e8c9bf73 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:52 +0800 Subject: [PATCH 0705/1461] Add client/render/PartRenderer.java to the 1.21.11 shared sources --- .../mcheli/client/render/PartRenderer.java | 554 ++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java new file mode 100644 index 00000000..6eb9b8bb --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java @@ -0,0 +1,554 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.CameraPart; +import com.jnhprojects.mcheli.content.aircraft.part.CrawlerTrack; +import com.jnhprojects.mcheli.content.aircraft.part.LandingGearPart; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.content.aircraft.part.PropellerPart; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import com.jnhprojects.mcheli.content.aircraft.part.ThrottlePart; +import com.jnhprojects.mcheli.content.aircraft.part.TrackRoller; +import com.jnhprojects.mcheli.content.aircraft.part.VehiclePart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.content.aircraft.part.WheelPart; +import com.jnhprojects.mcheli.content.aircraft.part.WingPart; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import org.joml.Vector3f; + +import java.util.List; + +/** + * Submits a vehicle's hull and moving parts for one frame. A port of the part routines in + * {@code MCH_RenderAircraft} and the per kind renderers; every transform is applied in hull space + * on top of the pose the caller already set up. + * + *

A part is looked up as {@code $name} inside the vehicle model, or as a separate + * {@code _} model file, the two places the original accepted. + */ +final class PartRenderer { + private static final Vector3 X_AXIS = new Vector3(1, 0, 0); + private static final Vector3 DOWN_AXIS = new Vector3(0, -1, 0); + + private final AircraftDefinition def; + private final Mesh mesh; + private final AircraftRenderState state; + private final PoseStack pose; + private final SubmitNodeCollector collector; + private final Identifier texture; + private final RenderType opaqueType; + private final RenderType glassType; + private final int light; + private final int overlay; + /** Glass draws unlit, so its colour carries the brightness of where the vehicle stands. */ + private final int glassColor; + + PartRenderer(AircraftDefinition def, Mesh mesh, AircraftRenderState state, PoseStack pose, SubmitNodeCollector collector, + Identifier texture, int light, int overlay, int glassColor) { + this.def = def; + this.mesh = mesh; + this.state = state; + this.pose = pose; + this.collector = collector; + this.texture = texture; + // Culled, as the original drew it. The models are full of back to back faces for thin + // panels, and drawing both sides of those makes them fight for the same depth. + this.opaqueType = RenderTypes.entityCutout(texture); + // The one entity translucent type that does not write depth, so water behind glass still + // draws. It does not cull, so MeshRenderer drops the back faces itself. + this.glassType = RenderTypes.entityTranslucentEmissive(texture); + this.light = light; + this.overlay = overlay; + this.glassColor = glassColor; + } + + void submitAll() { + switch (def) { + case PlaneDefinition plane -> { + nozzles(plane); + wings(plane); + propellers(plane); + hull(); + } + case HelicopterDefinition heli -> { + hull(); + rotors(heli); + } + case VehicleDefinition vehicle -> { + hull(); + for (VehiclePart part : vehicle.parts) { + vehiclePart(vehicle, part); + } + } + default -> hull(); + } + weapons(); + rotatingParts(); + movingParts(def.hatches, state.hatch); + trackRollers(); + crawlerTracks(); + steeringWheels(); + wheels(); + throttles(); + cameras(); + landingGear(); + weaponBays(); + movingParts(def.canopies, state.canopy); + } + + /** + * The body and, as a {@code $} object of its own so it can have its own {@code PartTexture}, + * the cockpit. A model without {@code $body} draws as one piece, cockpit included, and no original + * model has a {@code $cockpit}. + */ + private void hull() { + submit(mesh.body()); + if (mesh.contains("$body")) { + submit(mesh.part("$cockpit")); + } + } + + // Kind specific parts + + private void rotors(HelicopterDefinition heli) { + for (int r = 0; r < heli.rotors.size(); r++) { + RotorDefinition rotor = heli.rotors.get(r); + List blade = part(rotor.part()); + if (blade.isEmpty() || isZero(rotor.axis())) { + continue; + } + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + int blades = Math.max(rotor.blades(), 1); + for (int i = 0; i < blades; i++) { + pose.pushPose(); + Vector3 p = rotor.position(); + translate(p, 1.0F); + float[] angles = r < state.bladeAngles.length ? state.bladeAngles[r] : null; + rotate(angles != null && i < angles.length ? angles[i] : state.rotorAngle + i * step, rotor.axis()); + if (!rotor.legacyPivot()) { + translate(p, -1.0F); + } + submit(blade); + pose.popPose(); + } + } + } + + private void nozzles(PlaneDefinition plane) { + for (MovingPart nozzle : plane.nozzles) { + pose.pushPose(); + rotateAround(nozzle.position(), state.nozzle, nozzle.axis()); + submit(part(nozzle.part())); + pose.popPose(); + } + } + + private void wings(PlaneDefinition plane) { + for (WingPart wing : plane.wings) { + pose.pushPose(); + rotateAround(wing.position(), state.wing * wing.factor(), wing.axis()); + submit(part(wing.part())); + for (MovingPart pylon : wing.pylons()) { + pose.pushPose(); + rotateAround(pylon.position(), state.wing * pylon.factor(), pylon.axis()); + submit(part(pylon.part())); + pose.popPose(); + } + pose.popPose(); + } + } + + /** Tilt rotors follow the nozzle state, the blades spin with the engine. */ + private void propellers(PlaneDefinition plane) { + if (!plane.hasNozzleState()) { + return; + } + for (PropellerPart rotor : plane.propellers) { + pose.pushPose(); + rotateAround(rotor.position(), state.nozzle * rotor.factor(), rotor.axis()); + submit(part(rotor.part())); + for (PropellerPart.Blade blade : rotor.blades()) { + List groups = part(blade.part()); + pose.pushPose(); + rotateAround(blade.position(), state.rotorAngle, blade.axis()); + for (int i = 0; i < blade.count(); i++) { + rotateAround(blade.position(), blade.step(), blade.axis()); + submit(groups); + } + pose.popPose(); + } + pose.popPose(); + } + } + + /** {@code MCH_RenderVehicle.drawPart}: mount parts turn with the gunner's view. */ + private void vehiclePart(VehicleDefinition vehicle, VehiclePart part) { + pose.pushPose(); + if (part.yaw() || part.pitch() || part.type() == VehiclePart.TYPE_ROTATING_BARREL) { + translate(part.position(), 1.0F); + if (part.yaw()) { + rotate(seatYaw(0), DOWN_AXIS); + } + if (part.pitch()) { + rotate(Mth.clamp(seatPitch(0), vehicle.minRotationPitch, vehicle.maxRotationPitch), X_AXIS); + } + translate(part.position(), -1.0F); + } + for (VehiclePart child : part.children()) { + vehiclePart(vehicle, child); + } + boolean gunnerLooking = state.localSeat == 0 && state.localFirstPerson; + if (part.drawFirstPerson() || !gunnerLooking) { + submit(part(part.part())); + } + pose.popPose(); + } + + // Parts every kind can have + + /** + * Gun mounts, turrets and missiles, {@code renderWeapon}. Each follows its weapon's aim, kicks + * back as it fires, spins its barrels and, for missiles, disappears until reloaded. A part + * whose weapon is missing follows the view of the seat that would fire it. + */ + private void weapons() { + float turretYaw = seatYaw(0); + for (int i = 0; i < def.weaponParts.size(); i++) { + WeaponPart w = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState ws = i < state.weaponParts.length ? state.weaponParts[i] : null; + boolean live = ws != null && ws.hasWeapon; + WeaponMount mount = mount(w); + int seat = mount != null ? mount.seatIndex() : 0; + if (w.hideInFirstPerson() && state.localFirstPerson && state.localSeat == seat) { + continue; + } + float defaultYaw = live ? ws.defaultYaw : mount != null ? mount.defaultYaw() : 0.0F; + float yaw; + float pitch; + if (live) { + yaw = ws.yaw; + pitch = ws.pitch; + } else { + yaw = Mth.wrapDegrees(seatYaw(seat) - defaultYaw); + pitch = seatPitch(seat); + } + + pose.pushPose(); + if (w.turret()) { + rotateAround(def.turretPosition, turretYaw, DOWN_AXIS); + } + translate(w.position(), 1.0F); + if (w.yaw()) { + rotate(w.turret() && !live ? yaw - turretYaw : yaw, DOWN_AXIS); + } + if (w.turret() && live) { + rotate(-(turretYaw - ws.turretLag), DOWN_AXIS); + } + // Side facing guns pitch around their own axis, which the original did by turning + // into the gun's frame and flipping the sign for guns pointing left or right. + boolean sideways = false; + if ((int) defaultYaw != 0) { + float t = Mth.wrapDegrees(defaultYaw); + sideways = (t >= 45.0F && t <= 135.0F) || (t <= -45.0F && t >= -135.0F); + rotate(-defaultYaw, DOWN_AXIS); + } + if (w.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && w.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, w.recoil() * ws.recoil); + } + if (mount != null || live) { + rotate(defaultYaw, DOWN_AXIS); + } + if (live && w.rotatingBarrel()) { + rotate(ws.barrel, w.axis()); + } + translate(w.position(), -1.0F); + if (!w.missile() || !live || !ws.hidden) { + submit(part(w.part())); + for (WeaponPart.Child child : w.children()) { + pose.pushPose(); + translate(child.position(), 1.0F); + if (child.yaw()) { + rotate(yaw, DOWN_AXIS); + } + if (live && (int) defaultYaw != 0) { + rotate(-defaultYaw, DOWN_AXIS); + } + if (child.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && child.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, -child.recoil() * ws.recoil); + } + if (live && (int) defaultYaw != 0) { + rotate(defaultYaw, DOWN_AXIS); + } + translate(child.position(), -1.0F); + submit(part(child.part())); + pose.popPose(); + } + } + pose.popPose(); + } + } + + private WeaponMount mount(WeaponPart part) { + if (part.weapons().isEmpty()) { + return null; + } + int group = def.weaponGroup(part.weapons().getFirst()); + return group >= 0 ? def.weaponGroups.get(group).mounts().getFirst() : null; + } + + private void rotatingParts() { + for (int i = 0; i < def.rotatingParts.size() && i < state.rotatingParts.length; i++) { + RotatingPart part = def.rotatingParts.get(i); + pose.pushPose(); + rotateAround(part.position(), state.rotatingParts[i], part.axis()); + submit(part(part.part())); + pose.popPose(); + } + } + + /** Hatches and canopies: swing by {@code angle * factor}, or slide along their offset. */ + private void movingParts(List parts, float angle) { + for (MovingPart part : parts) { + pose.pushPose(); + moveOrSwing(part, angle); + submit(part(part.part())); + pose.popPose(); + } + } + + private void moveOrSwing(MovingPart part, float angle) { + if (part.slide()) { + translate(part.position(), angle / 90.0F); + } else { + rotateAround(part.position(), angle * part.factor(), part.axis()); + } + } + + private void trackRollers() { + for (TrackRoller roller : def.trackRollers) { + pose.pushPose(); + rotateAround(roller.position(), state.roller[roller.side()], X_AXIS); + submit(part(roller.part())); + pose.popPose(); + } + } + + /** + * Each link slides towards the next one by the track's run, so the loop appears to move. + * The link part is modelled at the origin and placed at each point of the outline. + */ + private void crawlerTracks() { + for (CrawlerTrack track : def.crawlerTracks) { + List link = part(track.part()); + List links = track.links(); + int count = links.size() - 1; + if (link.isEmpty() || count < 1) { + continue; + } + float run = state.track[track.side()]; + for (int i = 0; i < count; i++) { + CrawlerTrack.Link cur = links.get(i); + CrawlerTrack.Link next = links.get((i + 1) % count); + float angleNext = next.angle(); + if (angleNext - cur.angle() < -180.0F) { + angleNext += 360.0F; + } else if (angleNext - cur.angle() > 180.0F) { + angleNext -= 360.0F; + } + pose.pushPose(); + pose.translate(0.0F, cur.y() + (next.y() - cur.y()) * run, cur.z() + (next.z() - cur.z()) * run); + pose.mulPose(Axis.XN.rotationDegrees(cur.angle() + (angleNext - cur.angle()) * run)); + submit(link); + pose.popPose(); + } + } + } + + private void steeringWheels() { + for (WheelPart wheel : def.steeringWheels) { + pose.pushPose(); + rotateAround(wheel.position(), state.steer * wheel.steerAngle(), wheel.axis()); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void wheels() { + for (WheelPart wheel : def.wheels) { + pose.pushPose(); + rotateAround(wheel.pivot(), state.steer * wheel.steerAngle(), wheel.axis()); + rotateAround(wheel.position(), state.wheelRoll, X_AXIS); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void throttles() { + for (ThrottlePart lever : def.throttleParts) { + pose.pushPose(); + rotateAround(lever.position(), state.throttle * lever.angle(), lever.axis()); + translate(lever.slide(), state.throttle); + submit(part(lever.part())); + pose.popPose(); + } + } + + /** Sensor turrets look where the gunner looks, or the pilot when the gunner seat is empty. */ + private void cameras() { + int seat = state.seatOccupied.length > 1 && state.seatOccupied[1] ? 1 : 0; + for (CameraPart camera : def.cameraParts) { + pose.pushPose(); + translate(camera.position(), 1.0F); + if (camera.yawSync()) { + rotate(seatYaw(seat), DOWN_AXIS); + } + if (camera.pitchSync()) { + rotate(seatPitch(seat), X_AXIS); + } + translate(camera.position(), -1.0F); + submit(part(camera.part())); + pose.popPose(); + } + } + + /** + * Gear legs swing up with the gear state; reversed parts close as the gear rises, and gear bay + * doors open fully halfway through and shut again. + */ + private void landingGear() { + float angle = state.landingGear; + float reverse = 90.0F - angle; + float door = Math.min(90.0F, 90.0F * Mth.sin(angle * 2.0F * Mth.DEG_TO_RAD) * 3.0F); + for (LandingGearPart gear : def.landingGear) { + pose.pushPose(); + translate(gear.position(), 1.0F); + if (gear.reverse()) { + rotate(reverse * gear.factor(), gear.axis()); + } else { + rotate((gear.hatch() ? door : angle) * gear.factor(), gear.axis()); + } + if (gear.secondAxis() != null) { + rotate((gear.reverse() ? reverse : angle) * gear.secondFactor(), gear.secondAxis()); + } + translate(gear.position(), -1.0F); + if (gear.slide() != null) { + float f = angle / 90.0F; + translate(gear.slide(), gear.reverse() ? 1.0F - f : f); + } + submit(part(gear.part())); + pose.popPose(); + } + } + + /** Bays swing open while a crew member has one of their weapons selected. */ + private void weaponBays() { + for (int i = 0; i < def.weaponBays.size(); i++) { + WeaponBayPart bay = def.weaponBays.get(i); + pose.pushPose(); + moveOrSwing(bay.door(), i < state.bays.length ? state.bays[i] : 0.0F); + submit(part(bay.door().part())); + pose.popPose(); + } + } + + // Helpers + + private float seatYaw(int seat) { + return seat >= 0 && seat < state.seatYaw.length ? state.seatYaw[seat] : 0.0F; + } + + private float seatPitch(int seat) { + return seat >= 0 && seat < state.seatPitch.length ? state.seatPitch[seat] : 0.0F; + } + + private List part(String name) { + List groups = mesh.part("$" + name); + if (!groups.isEmpty()) { + return groups; + } + Mesh separate = MeshCache.INSTANCE.find(def.folder(), def.name() + "_" + name); + return separate != null ? separate.groups() : List.of(); + } + + private void submit(List groups) { + if (groups.isEmpty()) { + return; + } + if (def.partTextures.isEmpty()) { + submit(groups, texture, opaqueType, glassType); + return; + } + for (PartTextures.Bucket bucket : GlassSplit.INSTANCE.buckets(groups, def.partTextures)) { + if (bucket.texture() == null) { + submit(bucket.groups(), texture, opaqueType, glassType); + } else { + Identifier own = RuntimeAssets.texture(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, + "textures/" + def.folder() + "/" + bucket.texture() + ".png")); + submit(bucket.groups(), own, RenderTypes.entityCutout(own), RenderTypes.entityTranslucentEmissive(own)); + } + } + } + + private void submit(List groups, Identifier texture, RenderType opaqueType, RenderType glassType) { + GlassSplit.Split split = GlassSplit.INSTANCE.split(groups, texture); + if (!split.opaque().isEmpty()) { + collector.submitCustomGeometry(pose, opaqueType, (p, buffer) -> MeshRenderer.emit(split.opaque(), p, buffer, light, overlay)); + } + if (!split.glass().isEmpty()) { + // Culling glass relies on the camera sitting at the origin, which is not so in a GUI. + collector.submitCustomGeometry(pose, glassType, state.inGui + ? (p, buffer) -> MeshRenderer.emit(split.glass(), p, buffer, light, overlay, glassColor) + : (p, buffer) -> MeshRenderer.emitFrontFaces(split.glass(), p, buffer, light, overlay, glassColor)); + } + } + + private void translate(Vector3 v, float scale) { + pose.translate((float) v.x() * scale, (float) v.y() * scale, (float) v.z() * scale); + } + + private void rotateAround(Vector3 pivot, float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + translate(pivot, 1.0F); + rotate(degrees, axis); + translate(pivot, -1.0F); + } + + /** Like {@code glRotatef}: any axis length, a zero axis does nothing. */ + private void rotate(float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + Vector3f a = new Vector3f((float) axis.x(), (float) axis.y(), (float) axis.z()).normalize(); + pose.mulPose(Axis.of(a).rotationDegrees(degrees)); + } + + private static boolean isZero(Vector3 v) { + return v.x() * v.x() + v.y() * v.y() + v.z() * v.z() < 1.0E-12; + } +} From c74329fd018b0144ebd051a47bc764d3616f7fae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:52 +0800 Subject: [PATCH 0706/1461] Make a translucent culled render type for the main target on 1.21.11 --- .../client/render/ProjectileRenderer.java | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java new file mode 100644 index 00000000..38c5df0e --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java @@ -0,0 +1,181 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.weapon.WeaponColor; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderSetup; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Util; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +import java.util.function.Function; + +/** + * Draws rounds, rockets, missiles and bombs from {@code models/bullets}, tinted with the weapon's + * {@code BulletColor} (its in-water colour below the surface) and always fully lit, as the + * original's {@code MCH_RenderBulletBase} did. + * + *

Not in the original: rounds trail a tracer and rocket motors burn with a glow, faint by day + * and bright at night, so a firefight in the dark can be followed by eye. Nothing glows under + * water. + */ +public class ProjectileRenderer extends EntityRenderer { + /** Vanilla's own draws into the item target; these rounds belong in the main one. */ + private static final Function TRANSLUCENT_CULL = Util.memoize(texture -> RenderType.create( + MCHeli.MOD_ID + "_translucent_cull", RenderSetup.builder(RenderPipelines.ITEM_ENTITY_TRANSLUCENT_CULL) + .withTexture("Sampler0", texture) + .useLightmap() + .useOverlay() + .affectsCrumbling() + .sortOnUpload() + .setOutline(RenderSetup.OutlineProperty.AFFECTS_OUTLINE) + .createRenderSetup())); + private static final int FULL_BRIGHT = 0xF000F0; + /** Warm tracer colour for rounds whose weapon keeps the default white tint. */ + private static final int TRACER = 0xFFC46A; + /** The longest tracer ribbon, in blocks; fast rounds cover far more than this per tick. */ + private static final double MAX_TRACER = 6.0; + /** How far behind the projectile's centre the motor glow sits, in blocks. */ + private static final double MOTOR_OFFSET = 0.6; + + public ProjectileRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + enum GlowKind { + NONE, + TRACER, + MOTOR + } + + public static class State extends EntityRenderState { + String model = ""; + float yaw; + float pitch; + int color = 0xFFFFFFFF; + boolean visible = true; + GlowKind glowKind = GlowKind.NONE; + float glow; + int glowColor = TRACER; + /** Distance covered over the last tick, which points the tracer and the motor glow. */ + Vec3 travel = Vec3.ZERO; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ProjectileEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.model = entity.model(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.visible = !entity.isSpent() && (!(entity instanceof BombEntity bomb) || bomb.isVisible()); + WeaponDefinition w = entity.weapon(); + boolean inWater = inWater(entity.level(), entity); + WeaponColor c = w != null ? (inWater ? w.colorInWater : w.color) : WeaponColor.WHITE; + state.color = c.argb(); + + state.glowKind = inWater || entity.isSpent() ? GlowKind.NONE : glowKind(entity); + if (state.glowKind != GlowKind.NONE) { + state.glow = Glow.strength(entity.level(), entity.blockPosition()); + state.travel = new Vec3(entity.getX() - entity.xo, entity.getY() - entity.yo, entity.getZ() - entity.zo); + if (state.glowKind == GlowKind.MOTOR) { + state.glowColor = GlowParticleOptions.FIRE; + } else { + // A weapon with its own tint keeps it for the tracer; the default white burns warm. + int tint = state.color & 0xFFFFFF; + state.glowColor = tint == 0xFFFFFF ? TRACER : tint; + } + } + } + + private static GlowKind glowKind(ProjectileEntity entity) { + if (entity instanceof BulletEntity) { + return GlowKind.TRACER; + } + if (entity instanceof RocketEntity || entity instanceof MarkerRocketEntity || entity instanceof AAMissileEntity + || entity instanceof ASMissileEntity || entity instanceof ATMissileEntity || entity instanceof TvMissileEntity) { + return GlowKind.MOTOR; + } + return GlowKind.NONE; + } + + /** The original looked for water at the round and up to two blocks above it. */ + private static boolean inWater(Level level, ProjectileEntity e) { + for (int y = 0; y < 3; y++) { + if (level.getFluidState(BlockPos.containing(e.getX() + 0.5, e.getY() + 1.5 - y, e.getZ() + 0.5)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = state.visible ? MeshCache.INSTANCE.find("bullets", state.model) : null; + if (mesh != null) { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/" + state.model + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + int color = state.color; + collector.submitCustomGeometry(pose, TRANSLUCENT_CULL.apply(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, FULL_BRIGHT, OverlayTexture.NO_OVERLAY, color)); + pose.popPose(); + } + if (state.visible) { + submitGlow(state, pose, collector, camera); + } + super.submit(state, pose, collector, camera); + } + + private static void submitGlow(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + if (state.glowKind == GlowKind.NONE || state.glow <= 0.0F) { + return; + } + double speed = state.travel.length(); + Vec3 back = speed > 1.0E-3 ? state.travel.scale(-1.0 / speed) : Vec3.ZERO; + switch (state.glowKind) { + case TRACER -> { + Vec3 toCamera = new Vec3(state.x - camera.pos.x, state.y - camera.pos.y, state.z - camera.pos.z); + Glow.streak(pose, collector, toCamera, back.scale(Math.min(speed, MAX_TRACER)), 0.08F, state.glowColor, state.glow); + Glow.halo(pose, collector, camera, Vec3.ZERO, 0.35F, state.glowColor, state.glow); + } + case MOTOR -> { + Vec3 nozzle = back.scale(MOTOR_OFFSET); + Glow.halo(pose, collector, camera, nozzle, 1.1F, state.glowColor, state.glow); + Glow.halo(pose, collector, camera, nozzle, 0.4F, GlowParticleOptions.FLASH, state.glow); + } + default -> { + } + } + } +} From d98eeae865a85f32943a5e32110006a340305ae1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:53 +0800 Subject: [PATCH 0707/1461] Add client/render/RiderPose.java to the 1.21.11 shared sources --- .../mcheli/client/render/RiderPose.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java new file mode 100644 index 00000000..9ddb1690 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Tilts players riding a vehicle with its hull, as the original did from its render hook. + * + *

Vanilla keeps riders upright, so a banked plane left the pilot standing straight up through + * the canopy. The tilt is the hull's pitch and roll expressed in the world frame, applied about + * the rider's vehicle attachment point (the hip), which is the point that sits on the seat. The + * rider's own body yaw is applied by the renderer afterwards, so turning in the seat still works. + * + *

The entity render dispatcher pushes a pose around every entity, so nothing needs popping here. + */ +public final class RiderPose { + private RiderPose() { + } + + /** Called before a living entity is drawn, with the pose the renderer is about to use. */ + public static void beforeRender(LivingEntityRenderState renderState, PoseStack pose, float partialTicks) { + // Render states carry no entity reference; players are the ones that keep their id. + if (!(renderState instanceof AvatarRenderState state)) { + return; + } + ClientLevel level = Minecraft.getInstance().level; + Entity rider = level != null ? level.getEntity(state.id) : null; + if (rider == null || !(rider.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + float yaw = vehicle.getYRot(partialTicks); + float pitch = vehicle.getXRot(partialTicks); + float roll = vehicle.roll(partialTicks); + if (pitch == 0.0F && roll == 0.0F) { + return; + } + Quaternionf tilt = AircraftRotation.orientation(yaw, pitch, roll).rotateY((float) Math.toRadians(yaw)); + Vec3 hip = rider.getVehicleAttachmentPoint(vehicle); + pose.rotateAround(tilt, (float) hip.x, (float) hip.y, (float) hip.z); + } +} From b624a3bfa8de85457b7f1a5e5b2beb7050a4f104 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:53 +0800 Subject: [PATCH 0708/1461] Add client/render/ThrowableRenderer.java to the 1.21.11 shared sources --- .../client/render/ThrowableRenderer.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java new file mode 100644 index 00000000..fe472a4b --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +/** {@code MCH_RenderThrowable}: the grenade's own model from {@code models/throwable}. */ +public class ThrowableRenderer extends EntityRenderer { + public ThrowableRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + String name = ""; + float yaw; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ThrowableEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.name = entity.definitionName(); + state.yaw = entity.getYRot(partialTicks); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + Mesh mesh = state.name.isEmpty() ? null : MeshCache.INSTANCE.get("throwable", state.name); + if (mesh != null) { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/throwable/" + state.name + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = state.lightCoords; + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutNoCull(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } +} From 1bcac0911c6254422f2cf9cb9b9dd51d420d3652 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:54 +0800 Subject: [PATCH 0709/1461] Add client/render/UavStationRenderer.java to the 1.21.11 shared sources --- .../client/render/UavStationRenderer.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java new file mode 100644 index 00000000..41fe7a21 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +import java.util.List; + +/** + * The UAV station and the portable controller, {@code MCH_RenderUavStation}. The station's screen + * lights up while a drone is flown from it; the controller's lid, keyboard half and display fold + * open as it is opened, and light up once fully open. + */ +public class UavStationRenderer extends EntityRenderer { + private static final String[] MODEL = {"uav_station", "uav_portable_controller"}; + /** The original greyed both a little. */ + private static final int TINT = 0xFFBFBFBF; + + public UavStationRenderer(EntityRendererProvider.Context context) { + super(context); + shadowRadius = 1.0F; + } + + public static class State extends EntityRenderState { + float yaw; + int kind; + boolean lit; + float cover; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(UavStationEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.kind = entity.kind(); + state.cover = entity.cover(partialTicks); + state.lit = state.kind == UavStationEntity.PORTABLE ? state.cover > 0.95F : entity.isLinked(); + } + + @Override + public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) { + int index = state.kind == UavStationEntity.PORTABLE ? 1 : 0; + Mesh mesh = MeshCache.INSTANCE.get("", MODEL[index]); + if (mesh != null) { + Identifier texture = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + MODEL[index] + (state.lit ? "_on" : "") + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = state.lightCoords; + if (index == 0) { + draw(pose, collector, texture, mesh.groups(), light); + } else { + draw(pose, collector, texture, mesh.part("$body"), light); + folding(pose, collector, texture, mesh.part("$cover"), state.cover * 60.0F, -0.1812F, -0.3186F, light); + folding(pose, collector, texture, mesh.part("$laptop_cover"), state.cover * 95.0F, -0.1808F, -0.0422F, light); + folding(pose, collector, texture, mesh.part("$display"), state.cover * -85.0F, -0.1807F, 0.2294F, light); + } + pose.popPose(); + } + super.submit(state, pose, collector, camera); + } + + /** {@code renderRotPart}: turns a part about the x axis through its hinge. */ + private static void folding(PoseStack pose, SubmitNodeCollector collector, Identifier texture, List part, + float angle, float y, float z, int light) { + pose.pushPose(); + pose.translate(0.0F, y, z); + pose.mulPose(Axis.XN.rotationDegrees(angle)); + pose.translate(0.0F, -y, -z); + draw(pose, collector, texture, part, light); + pose.popPose(); + } + + private static void draw(PoseStack pose, SubmitNodeCollector collector, Identifier texture, List groups, int light) { + if (!groups.isEmpty()) { + collector.submitCustomGeometry(pose, RenderTypes.entityCutoutNoCull(texture), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY, TINT)); + } + } +} From a1daac72dce96588fd6e414b11bf87419a155b55 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:54 +0800 Subject: [PATCH 0710/1461] Port DraftingTableScreen to the render methods of 1.21.11 --- .../client/screen/DraftingTableScreen.java | 336 ++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java new file mode 100644 index 00000000..1966cc4a --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java @@ -0,0 +1,336 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.workshop.WorkshopHomeScreen; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.network.DraftingTablePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeCollection; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; +import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * The drafting table's recipe browser, {@code MCH_DraftingTableGui}. Pick a list (helicopters, + * planes, tanks, ground vehicles or equipment), step through its recipes, and Create makes the one + * shown into the output slot. The right page shows the original's illustrated description for + * equipment and a vehicle's numbers for vehicles; the original turned a 3D model there instead. + * + *

The recipes come from the player's recipe book, which holds every MCHeli recipe from login. + */ +public class DraftingTableScreen extends AbstractContainerScreen { + private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/drafting_table.png"); + private static final int TEXTURE_SIZE = 512; + /** The description pages take this part of their 512 square texture. */ + private static final int PAGE_WIDTH = 340; + private static final int PAGE_HEIGHT = 380; + private static final int PAGE_X = 210; + private static final int PAGE_Y = 20; + private static final int GRID_X = 121; + private static final int GRID_Y = 34; + private static final int TEXT = 0xFF6A3A00; + private static final int WHITE = 0xFFFFFFFF; + private static final String[] LISTS = {"helicopter", "plane", "vehicle", "tank", "item"}; + + private final List> lists = new ArrayList<>(); + private int list; + private int index; + private int page; + private int pageCount; + private @Nullable Button create; + private @Nullable Button previous; + private @Nullable Button next; + private @Nullable Button previousPage; + private @Nullable Button nextPage; + + public DraftingTableScreen(DraftingTableMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 400; + imageHeight = 240; + inventoryLabelX = 30; + inventoryLabelY = 130; + } + + @Override + protected void init() { + super.init(); + collectRecipes(); + for (int i = 0; i < LISTS.length; i++) { + int which = i; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.list." + LISTS[i]), b -> selectList(which)) + .bounds(leftPos + 20, topPos + 20 + i * 20, 90, 20).build()); + } + create = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.create"), b -> create()) + .bounds(leftPos + 120, topPos + 89, 50, 20).build()); + previous = addRenderableWidget(Button.builder(Component.literal("<<"), b -> step(-1)).bounds(leftPos + 120, topPos + 111, 36, 20).build()); + next = addRenderableWidget(Button.builder(Component.literal(">>"), b -> step(1)).bounds(leftPos + 155, topPos + 111, 35, 20).build()); + previousPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.previous_page"), b -> turnPage(-1)) + .bounds(leftPos + 210, topPos + 210, 60, 20).build()); + nextPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.next_page"), b -> turnPage(1)) + .bounds(leftPos + 270, topPos + 210, 60, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.workshop"), b -> openWorkshop()) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.drafting_table.workshop_hint"))) + .bounds(leftPos + 334, topPos + 210, 56, 20).build()); + selectList(list); + } + + /** The workshop is a screen of its own and needs no block, so the table's menu closes first. */ + private void openWorkshop() { + minecraft.player.closeContainer(); + minecraft.setScreen(new WorkshopHomeScreen()); + } + + /** Every MCHeli recipe the recipe book holds, sorted into the five lists by what it makes. */ + private void collectRecipes() { + lists.clear(); + for (int i = 0; i < LISTS.length; i++) { + lists.add(new ArrayList<>()); + } + ContextMap context = SlotDisplayContext.fromLevel(minecraft.level); + for (RecipeCollection collection : minecraft.player.getRecipeBook().getCollections()) { + for (RecipeDisplayEntry entry : collection.getRecipes()) { + ItemStack result = entry.display().result().resolveForFirstStack(context); + if (!result.isEmpty() && BuiltInRegistries.ITEM.getKey(result.getItem()).getNamespace().equals(MCHeli.MOD_ID)) { + lists.get(listOf(result.getItem())).add(entry); + } + } + } + for (List entries : lists) { + entries.sort(Comparator.comparing(e -> e.display().result().resolveForFirstStack(context).getHoverName().getString())); + } + } + + private static int listOf(Item item) { + if (item == ModItems.HELICOPTER.get()) { + return 0; + } + if (item == ModItems.PLANE.get()) { + return 1; + } + if (item == ModItems.VEHICLE.get()) { + return 2; + } + if (item == ModItems.TANK.get()) { + return 3; + } + return 4; + } + + private void selectList(int which) { + list = which; + index = 0; + showRecipe(); + } + + private void step(int direction) { + int size = lists.get(list).size(); + if (size > 0) { + index = Math.floorMod(index + direction, size); + showRecipe(); + } + } + + private void turnPage(int direction) { + page = Math.clamp(page + direction, 0, Math.max(pageCount - 1, 0)); + updateButtons(); + } + + private void showRecipe() { + page = 0; + ItemStack result = result(); + pageCount = descriptionPages(result); + updateButtons(); + } + + private @Nullable RecipeDisplayEntry current() { + List entries = lists.get(list); + return entries.isEmpty() ? null : entries.get(Math.min(index, entries.size() - 1)); + } + + private ItemStack result() { + RecipeDisplayEntry entry = current(); + return entry != null ? entry.display().result().resolveForFirstStack(SlotDisplayContext.fromLevel(minecraft.level)) : ItemStack.EMPTY; + } + + /** The original's description pages for equipment: {@code drafting_table_desc/_.png}. */ + private int descriptionPages(ItemStack result) { + if (result.isEmpty() || AircraftItem.vehicleOf(result) != null) { + return 0; + } + String name = descriptionName(result); + int pages = 0; + while (pages < 10 && minecraft.getResourceManager().getResource(pageTexture(name, pages)).isPresent()) { + pages++; + } + return pages; + } + + private static String descriptionName(ItemStack result) { + if (result.getItem() == ModItems.THROWABLE.get()) { + String throwable = result.get(ModDataComponents.THROWABLE.get()); + return throwable != null ? throwable : "throwable"; + } + return BuiltInRegistries.ITEM.getKey(result.getItem()).getPath(); + } + + private static Identifier pageTexture(String name, int page) { + return Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/drafting_table_desc/" + name + "_" + page + ".png"); + } + + private boolean canCreate() { + RecipeDisplayEntry entry = current(); + if (entry == null || menu.getSlot(menu.slots.size() - 1).hasItem() && !minecraft.player.getAbilities().instabuild) { + return false; + } + if (minecraft.player.getAbilities().instabuild) { + return true; + } + StackedItemContents contents = new StackedItemContents(); + minecraft.player.getInventory().fillStackedContents(contents); + return entry.canCraft(contents); + } + + private void create() { + RecipeDisplayEntry entry = current(); + if (entry != null) { + ModNetwork.sendToServer(new DraftingTablePacket(entry.id())); + } + } + + private void updateButtons() { + if (create == null || previous == null || next == null || previousPage == null || nextPage == null) { + return; + } + boolean any = current() != null; + create.active = any && canCreate(); + previous.active = lists.get(list).size() > 1; + next.active = lists.get(list).size() > 1; + previousPage.visible = pageCount > 1; + nextPage.visible = pageCount > 1; + previousPage.active = page > 0; + nextPage.active = page < pageCount - 1; + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, TEXTURE_SIZE, TEXTURE_SIZE); + if (pageCount > 0) { + String name = descriptionName(result()); + graphics.blit(RenderPipelines.GUI_TEXTURED, pageTexture(name, page), leftPos + PAGE_X, topPos + PAGE_Y, 0.0F, 0.0F, + PAGE_WIDTH / 2, PAGE_HEIGHT / 2, PAGE_WIDTH, PAGE_HEIGHT, TEXTURE_SIZE, TEXTURE_SIZE); + } + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, TEXT, false); + RecipeDisplayEntry entry = current(); + if (entry == null) { + graphics.drawString(font, Component.translatable("gui.mcheli.drafting_table.empty"), 120, 20, TEXT, false); + return; + } + ItemStack result = result(); + graphics.drawString(font, font.plainSubstrByWidth(result.getHoverName().getString(), 85), 120, 20, WHITE, true); + graphics.drawString(font, (index + 1) + " / " + lists.get(list).size(), 120, 134, TEXT, false); + ingredients(graphics, entry.display(), mouseX, mouseY); + graphics.renderItem(result, 178, 52); + if (pageCount == 0) { + details(graphics, result); + } + } + + /** {@code drawItemRecipe}: the 3x3 grid, each slot cycling through what it accepts once a second. */ + private void ingredients(GuiGraphics graphics, RecipeDisplay display, int mouseX, int mouseY) { + ContextMap context = SlotDisplayContext.fromLevel(minecraft.level); + List slots; + int width; + switch (display) { + case ShapedCraftingRecipeDisplay shaped -> { + slots = shaped.ingredients(); + width = shaped.width(); + } + case ShapelessCraftingRecipeDisplay shapeless -> { + slots = shapeless.ingredients(); + width = 3; + } + default -> { + return; + } + } + int tick = (int) (minecraft.level.getGameTime() / 20); + for (int i = 0; i < slots.size() && i < 9; i++) { + List options = slots.get(i).resolveForStacks(context); + if (options.isEmpty()) { + continue; + } + ItemStack stack = options.get(tick % options.size()); + int x = GRID_X + (i % width) * 18; + int y = GRID_Y + (i / width) * 18; + graphics.renderItem(stack, x, y); + if (mouseX - leftPos >= x && mouseX - leftPos < x + 16 && mouseY - topPos >= y && mouseY - topPos < y + 16) { + graphics.setTooltipForNextFrame(font, stack, mouseX - leftPos, mouseY - topPos); + } + } + } + + /** The vehicle's numbers, where the original turned its model. */ + private void details(GuiGraphics graphics, ItemStack result) { + String name = AircraftItem.vehicleOf(result); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + return; + } + List lines = new ArrayList<>(); + lines.add(Component.translatable("gui.mcheli.drafting_table.hp", def.maxHp)); + lines.add(Component.translatable("gui.mcheli.drafting_table.speed", "%.2f".formatted(def.speed))); + lines.add(Component.translatable("gui.mcheli.drafting_table.seats", def.seatCount())); + lines.add(Component.translatable("gui.mcheli.drafting_table.fuel", def.maxFuel)); + lines.add(Component.translatable("gui.mcheli.drafting_table.weapons")); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + String weaponName = weapon != null && weapon.displayName != null ? weapon.displayName : group.weaponType(); + lines.add(Component.literal(" " + weaponName + (group.mounts().size() > 1 ? " x" + group.mounts().size() : ""))); + } + int y = PAGE_Y + 20; + for (Component line : lines) { + if (y > PAGE_Y + 180) { + break; + } + graphics.drawString(font, font.split(line, 170).getFirst(), PAGE_X, y, TEXT, false); + y += 10; + } + } +} From 3389f47189d855b5e73c0bb65db7fe0e47cb25d8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:54 +0800 Subject: [PATCH 0711/1461] Pick team colours from ChatFormatting on 1.21.11, which has no TeamColor --- .../client/screen/TeamManagerScreen.java | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java new file mode 100644 index 00000000..3ec2606f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java @@ -0,0 +1,136 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.TeamPacket; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.scores.PlayerTeam; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * The team manager, {@code MCH_GuiScoreboard_Main} and {@code MCH_GuiScoreboard_CreateTeam}, for + * operators setting up a match: switch PvP, shuffle everyone into the teams, make a team, send + * everyone to their spawn point and clear away every vehicle. The teams and their players are + * listed on the right. + */ +public class TeamManagerScreen extends Screen { + private static final int WIDTH = 150; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + + private @Nullable EditBox name; + private int color = TeamManager.COLORS.indexOf(ChatFormatting.RED); + private boolean friendlyFire; + private @Nullable Button colorButton; + private @Nullable Button friendlyFireButton; + private @Nullable Button createButton; + + public TeamManagerScreen() { + super(Component.translatable("gui.mcheli.team.title")); + } + + @Override + protected void init() { + int left = width / 2 - WIDTH - 10; + int y = 40; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.pvp"), b -> send(TeamPacket.Action.PVP)) + .bounds(left, y, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.shuffle"), b -> send(TeamPacket.Action.SHUFFLE)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.shuffle_hint"))) + .bounds(left, y + 22, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.jump_spawn"), b -> send(TeamPacket.Action.JUMP_TO_SPAWN)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.jump_spawn_hint"))) + .bounds(left, y + 44, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.destroy_all"), b -> send(TeamPacket.Action.DESTROY_ALL)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.destroy_all_hint"))) + .bounds(left, y + 66, WIDTH, 20).build()); + + int create = y + 104; + name = addRenderableWidget(new EditBox(font, left, create, WIDTH, 20, Component.translatable("gui.mcheli.team.name"))); + name.setMaxLength(16); + name.setHint(Component.translatable("gui.mcheli.team.name")); + name.setResponder(text -> updateButtons()); + colorButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + color = (color + 1) % TeamManager.COLORS.size(); + updateButtons(); + }).bounds(left, create + 22, WIDTH, 20).build()); + friendlyFireButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + friendlyFire = !friendlyFire; + updateButtons(); + }).bounds(left, create + 44, WIDTH, 20).build()); + createButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.create"), b -> { + ModNetwork.sendToServer(new TeamPacket(TeamPacket.Action.CREATE_TEAM, name.getValue(), color, friendlyFire)); + name.setValue(""); + }).bounds(left, create + 66, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 - 50, height - 28, 100, 20).build()); + updateButtons(); + } + + private void send(TeamPacket.Action action) { + ModNetwork.sendToServer(new TeamPacket(action)); + } + + private void updateButtons() { + if (colorButton == null || friendlyFireButton == null || createButton == null || name == null) { + return; + } + ChatFormatting teamColor = TeamManager.COLORS.get(color); + colorButton.setMessage(Component.translatable("gui.mcheli.team.color", + Component.literal(teamColor.getName()).withStyle(teamColor))); + friendlyFireButton.setMessage(Component.translatable(friendlyFire ? "gui.mcheli.team.friendly_fire_on" : "gui.mcheli.team.friendly_fire_off")); + String value = name.getValue().trim(); + createButton.active = !value.isEmpty() && !value.contains(" "); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 15, TEXT); + int left = width / 2 - WIDTH - 10; + graphics.drawString(font, Component.translatable("gui.mcheli.team.new_team"), left, 132, DIM); + teams(graphics, width / 2 + 10, 40); + } + + /** The scoreboard's teams and who is in them, as this client sees the scoreboard. */ + private void teams(GuiGraphics graphics, int x, int y) { + if (minecraft.level == null) { + return; + } + List teams = new ArrayList<>(minecraft.level.getScoreboard().getPlayerTeams()); + if (teams.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.team.none"), x, y, DIM); + return; + } + int bottom = height - 40; + for (PlayerTeam team : teams) { + if (y > bottom) { + break; + } + graphics.drawString(font, team.getFormattedDisplayName(), x, y, TEXT); + y += 11; + for (String player : team.getPlayers()) { + if (y > bottom) { + break; + } + graphics.drawString(font, " " + player, x, y, DIM); + y += 10; + } + y += 4; + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From f9509f4cad68239a88ab163b2b40c4b8b7966034 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:55 +0800 Subject: [PATCH 0712/1461] Port UavStationScreen to the render methods of 1.21.11 --- .../client/screen/UavStationScreen.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java new file mode 100644 index 00000000..232e98e8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_GuiUavStation}: the slot for the drone, the launch offset + * per axis with its buttons, and Continue to take the last drone flown from here again. + */ +public class UavStationScreen extends AbstractContainerScreen { + private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/uav_station.png"); + private static final String[] AXES = {"X", "Y", "Z"}; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF0000; + + private @Nullable Button continueButton; + + public UavStationScreen(UavStationMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = 166; + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + int x = width / 2 - 5; + int y = height / 2 - 76; + for (int axis = 0; axis < 3; axis++) { + for (int step = 0; step < UavStationMenu.STEPS.length; step++) { + int s = UavStationMenu.STEPS[step]; + int id = axis * 4 + step; + addRenderableWidget(Button.builder(Component.literal((s > 0 ? "+" : "") + s), b -> press(id)) + .bounds(x + step * 20, y + axis * 22, 20, 20).build()); + } + } + continueButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.uav_station.continue"), b -> press(UavStationMenu.BUTTON_CONTINUE)) + .bounds(x - 77, y + 44, 50, 20).build()); + continueButton.active = menu.canContinue(); + } + + @Override + protected void containerTick() { + super.containerTick(); + if (continueButton != null) { + continueButton.active = menu.canContinue(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, 256, 256); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + UavStationEntity station = menu.station(); + ItemStack stack = menu.getSlot(0).getItem(); + AircraftDefinition def = stack.isEmpty() ? null : UavStationEntity.definitionOf(stack); + Component label; + int color = WHITE; + if (!stack.isEmpty() && (def == null || !def.uav)) { + label = Component.translatable("gui.mcheli.uav_station.not_uav"); + color = RED; + } else if (station != null && station.kind() == UavStationEntity.PORTABLE && def != null && !def.smallUav) { + label = Component.translatable("gui.mcheli.uav_station.small_only"); + color = RED; + } else { + label = title; + } + graphics.drawString(font, label, 8, 6, color, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, WHITE, false); + for (int axis = 0; axis < 3; axis++) { + graphics.drawString(font, String.format("%s.%+d", AXES[axis], menu.offset(axis)), 58, 15 + axis * 22, WHITE, false); + } + } +} From 5171c65f5425412e4a06c2d745d1ec8a839bde2f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:55 +0800 Subject: [PATCH 0713/1461] Port VehicleScreen to the render methods of 1.21.11 --- .../mcheli/client/screen/VehicleScreen.java | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java new file mode 100644 index 00000000..601b99d1 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java @@ -0,0 +1,181 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGui}: fuel and parachute slots on the left, the weapon + * panel on the right with its reload cost and button, and a button into the cargo chest for + * vehicles that have one. The panel is drawn from the chest texture so it looks like vanilla. + */ +public class VehicleScreen extends AbstractContainerScreen { + private static final Identifier CHEST = Identifier.withDefaultNamespace("textures/gui/container/generic_54.png"); + private static final Identifier SLOT = Identifier.withDefaultNamespace("container/slot"); + /** Height of the part above the player inventory. */ + private static final int TOP = 88; + private static final int PANEL_X = 72; + private static final int PANEL_WIDTH = 96; + private static final int LABEL = 0xFF404040; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF5555; + private static final int GREEN = 0xFF55FF55; + + private int weapon; + private boolean cargoButton; + private @Nullable Button reload; + private @Nullable Button previous; + private @Nullable Button next; + + public VehicleScreen(VehicleMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = TOP + 96; + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + previous = addRenderableWidget(Button.builder(Component.literal("<"), b -> step(-1)) + .bounds(leftPos + PANEL_X, topPos + 68, 16, 16).build()); + reload = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.reload"), b -> press(weapon)) + .bounds(leftPos + PANEL_X + 18, topPos + 68, PANEL_WIDTH - 36, 16).build()); + next = addRenderableWidget(Button.builder(Component.literal(">"), b -> step(1)) + .bounds(leftPos + PANEL_X + PANEL_WIDTH - 16, topPos + 68, 16, 16).build()); + AircraftEntity vehicle = menu.vehicle(); + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + cargoButton = def != null && def.cargoRows() > 0; + if (cargoButton) { + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.cargo"), b -> press(VehicleMenu.BUTTON_CARGO)) + .bounds(leftPos + imageWidth - 52, topPos + 3, 46, 13).build()); + } + updateButtons(); + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + private int weaponCount() { + AircraftEntity vehicle = menu.vehicle(); + VehicleWeapons weapons = vehicle != null ? vehicle.weapons() : null; + return weapons != null ? Math.min(weapons.sets().size(), VehicleMenu.MAX_WEAPONS) : 0; + } + + private void step(int direction) { + int count = weaponCount(); + if (count > 0) { + weapon = Math.floorMod(weapon + direction, count); + updateButtons(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + private void updateButtons() { + if (reload == null || previous == null || next == null) { + return; + } + int count = weaponCount(); + reload.visible = count > 0; + previous.visible = count > 1; + next.visible = count > 1; + reload.active = menu.canReload(weapon); + AircraftEntity vehicle = menu.vehicle(); + boolean grounded = vehicle != null && vehicle.canSupply(); + reload.setTooltip(grounded ? null : Tooltip.create(Component.translatable("gui.mcheli.vehicle.reload_hint"))); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + int x = leftPos; + int y = topPos; + // The chest's title bar, its plain middle stretched down to the inventory, then the inventory. + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y, 0.0F, 0.0F, imageWidth, 17, 256, 256); + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y + 17, 0.0F, 12.0F, imageWidth, TOP - 17, imageWidth, 1, 256, 256); + graphics.blit(RenderPipelines.GUI_TEXTURED, CHEST, x, y + TOP, 0.0F, 126.0F, imageWidth, 96, 256, 256); + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SLOT, x + VehicleMenu.FUEL_X + i * 18 - 1, y + VehicleMenu.FUEL_Y - 1, 18, 18); + } + for (int i = 0; i < menu.parachuteSlots(); i++) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SLOT, x + VehicleMenu.PARACHUTE_X + i * 18 - 1, y + VehicleMenu.PARACHUTE_Y - 1, 18, 18); + } + graphics.fill(x + PANEL_X - 2, y + 17, x + PANEL_X + PANEL_WIDTH + 2, y + 66, 0xFF373737); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + AircraftEntity vehicle = menu.vehicle(); + // Long names only give way to the Cargo button, where there is one. + int titleWidth = cargoButton ? imageWidth - 64 : imageWidth - 16; + graphics.drawString(font, font.plainSubstrByWidth(title.getString(), titleWidth), titleLabelX, titleLabelY, LABEL, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, LABEL, false); + if (vehicle == null) { + return; + } + if (vehicle.maxFuel() > 0) { + // A percentage and a gauge: the exact units would run into the weapon panel. + int percent = Math.round(vehicle.fuelRatio() * 100.0F); + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.fuel", percent), VehicleMenu.FUEL_X, VehicleMenu.FUEL_Y - 11, LABEL, false); + int gaugeY = VehicleMenu.FUEL_Y + 18; + int width = VehicleInventory.FUEL_SLOTS * 18 - 2; + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width, gaugeY + 3, 0xFF373737); + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width * percent / 100, gaugeY + 3, percent <= 10 ? RED : GREEN); + } + if (menu.parachuteSlots() > 0) { + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.parachute"), VehicleMenu.PARACHUTE_X, VehicleMenu.PARACHUTE_Y - 11, LABEL, false); + } + weaponPanel(graphics, vehicle); + } + + /** Name, rounds carried against the maximum, and what one reload costs. */ + private void weaponPanel(GuiGraphics graphics, AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null && weaponCount() > 0 ? weapons.set(Math.min(weapon, weaponCount() - 1)) : null; + if (set == null) { + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.no_weapons"), PANEL_X, 20, WHITE, false); + return; + } + graphics.drawString(font, font.plainSubstrByWidth(set.displayName(), PANEL_WIDTH), PANEL_X, 20, WHITE, false); + int rounds = menu.rounds(weapon); + int max = set.maxAmmo(); + int color = rounds == 0 && max > 0 ? RED : rounds >= max ? GREEN : WHITE; + graphics.drawString(font, rounds + " / " + max, PANEL_X, 31, color, false); + int itemX = PANEL_X; + for (RoundItem cost : set.definition().roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null) { + continue; + } + graphics.renderItem(new ItemStack(item), itemX, 42); + graphics.drawString(font, String.valueOf(cost.count()), itemX + 17, 47, WHITE, false); + itemX += 32; + if (itemX > PANEL_X + PANEL_WIDTH - 16) { + break; + } + } + } +} From 939f5c6c5fa26b0a885f4ed9098c0e484765fb4d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:56 +0800 Subject: [PATCH 0714/1461] Add client/sound/EngineSound.java to the 1.21.11 shared sources --- .../mcheli/client/sound/EngineSound.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java new file mode 100644 index 00000000..b32040f5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** + * A looping sound of one vehicle: its engine, or the rotor loop some vehicles have alongside it. + * Ported from {@code MCH_SoundUpdater}. + * + *

Volume and pitch follow the vehicle's throttle every tick, the pitch along the definition's + * curve if it has one and otherwise the way the kind of vehicle always has. For listeners outside + * the vehicle the sound is placed between them and the vehicle, so that the 16 block range of a + * normal sound stretches to the definition's {@code SoundRange}, and the pitch bends slightly as the + * vehicle approaches or leaves, which is what the original did. + */ +final class EngineSound extends RuntimeSoundInstance { + private static final float MAX_DOPPLER = 0.2F; + + private final AircraftEntity vehicle; + private final EngineSoundSource source; + private final boolean rotor; + private float doppler; + private double lastDistance = -1.0; + + EngineSound(AircraftEntity vehicle, EngineSoundSource source, String sound, boolean rotor) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.vehicle = vehicle; + this.source = source; + this.rotor = rotor; + this.looping = true; + this.delay = 0; + update(); + } + + @Override + public void tick() { + if (vehicle.isRemoved() || vehicle.definition() == null) { + stop(); + return; + } + update(); + if (volume <= 0.0F) { + stop(); + } + } + + private void update() { + AircraftDefinition def = vehicle.definition(); + LocalPlayer player = Minecraft.getInstance().player; + if (def == null || player == null) { + volume = 0.0F; + return; + } + volume = (rotor ? def.rotorSoundVolume : def.soundVolume) * source.engineVolume(); + PitchCurve curve = rotor ? def.rotorSoundPitchCurve : def.soundPitchCurve; + float pitch = def.soundPitch * (curve != null ? curve.at(vehicle.visibleThrottle()) : source.enginePitch()); + boolean aboard = player.getRootVehicle() == vehicle.getRootVehicle(); + if (aboard) { + x = vehicle.getX(); + y = vehicle.getY(); + z = vehicle.getZ(); + } else { + double dx = vehicle.getX() - player.getX(); + double dz = vehicle.getZ() - player.getZ(); + double distance = Math.sqrt(dx * dx + dz * dz); + if (lastDistance >= 0.0) { + double closing = lastDistance - distance; + if (Math.abs(closing) > 0.3) { + doppler = Math.clamp(doppler + (float) (closing / 40.0), -MAX_DOPPLER, MAX_DOPPLER); + } + } + doppler *= 0.9F; + pitch += doppler; + lastDistance = distance; + + double scale = def.soundRange / 16.0; + x = player.getX() + dx / scale; + y = player.getY() + (vehicle.getY() - player.getY()) / scale; + z = player.getZ() + dz / scale; + } + this.pitch = Math.max(pitch, 0.0F); + } + + @Override + public boolean canStartSilent() { + return true; + } +} From c098934b6c68d40dc9f86d1c21be96dcc6fd9b1f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:56 +0800 Subject: [PATCH 0715/1461] Add client/sound/EngineSoundPlayer.java to the 1.21.11 shared sources --- .../client/sound/EngineSoundPlayer.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java new file mode 100644 index 00000000..62e52284 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.Entity; + +import java.util.HashMap; +import java.util.Map; + +/** + * Starts engine and rotor loops for vehicles near the player and restarts them after they fall + * silent. The sounds themselves track their vehicle until it stops or disappears. + */ +public final class EngineSoundPlayer { + /** Ticks to wait before restarting a loop, as the original did, so a stuttering engine does not spam. */ + private static final int RESTART_DELAY = 20; + + /** By {@link #key}, since a vehicle can have two loops. */ + private static final Map PLAYING = new HashMap<>(); + private static final Map COOLDOWN = new HashMap<>(); + + private EngineSoundPlayer() { + } + + public static void tick() { + Minecraft mc = Minecraft.getInstance(); + ClientLevel level = mc.level; + LocalPlayer player = mc.player; + if (level == null || player == null || mc.isPaused()) { + if (level == null) { + PLAYING.clear(); + COOLDOWN.clear(); + } + return; + } + PLAYING.values().removeIf(EngineSound::isStopped); + COOLDOWN.replaceAll((id, ticks) -> ticks - 1); + COOLDOWN.values().removeIf(ticks -> ticks <= 0); + + for (Entity entity : level.entitiesForRendering()) { + if (!(entity instanceof AircraftEntity vehicle) || !(entity instanceof EngineSoundSource source)) { + continue; + } + AircraftDefinition def = vehicle.definition(); + if (def == null) { + continue; + } + double range = def.soundRange * 1.2; + if (vehicle.distanceToSqr(player) > range * range) { + continue; + } + start(mc, vehicle, source, def.soundOrDefault(), def.soundVolume, false); + start(mc, vehicle, source, def.rotorSound, def.rotorSoundVolume, true); + } + } + + private static void start(Minecraft mc, AircraftEntity vehicle, EngineSoundSource source, String sound, float volume, boolean rotor) { + long key = key(vehicle, rotor); + if (sound.isEmpty() || PLAYING.containsKey(key) || COOLDOWN.containsKey(key) || volume * source.engineVolume() <= 0.01F) { + return; + } + EngineSound loop = new EngineSound(vehicle, source, sound, rotor); + mc.getSoundManager().play(loop); + PLAYING.put(key, loop); + COOLDOWN.put(key, RESTART_DELAY); + } + + private static long key(AircraftEntity vehicle, boolean rotor) { + return (long) vehicle.getId() << 1 | (rotor ? 1L : 0L); + } +} From 8c4790b5a0541067fcbbdaeb360148ab58c87cc5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:57 +0800 Subject: [PATCH 0716/1461] Add client/sound/OneShotSound.java to the 1.21.11 shared sources --- .../mcheli/client/sound/OneShotSound.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java new file mode 100644 index 00000000..31825a4f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java @@ -0,0 +1,22 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** A sound from a content pack played once at a point, such as a gun firing. */ +public final class OneShotSound extends RuntimeSoundInstance { + public OneShotSound(String sound, double x, double y, double z, float volume, float pitch) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.x = x; + this.y = y; + this.z = z; + this.volume = volume; + this.pitch = pitch; + } + + @Override + public void tick() { + // It stays where it was started and ends when the file does. + } +} From 96f7273e0e3314a007e616843dbe0e48a0d17ea0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:57 +0800 Subject: [PATCH 0717/1461] Override resolve and use ConstantFloat.of on 1.21.11 --- .../client/sound/RuntimeSoundInstance.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java new file mode 100644 index 00000000..b3327f16 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -0,0 +1,79 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.JOrbisAudioStream; +import net.minecraft.client.sounds.LoopingAudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.client.sounds.WeighedSoundEvents; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.util.Util; +import net.minecraft.util.valueproviders.ConstantFloat; +import org.jspecify.annotations.Nullable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * A sound that plays through the sound manager when a resource pack defines its event, and + * otherwise streams the OGG file a runtime root holds for it ({@link RuntimeAssets#soundFile}). + * Content added while the game runs can bring sounds this way without touching + * {@code sounds.json} or reloading resources. + */ +public abstract class RuntimeSoundInstance extends AbstractTickableSoundInstance { + private @Nullable Path file; + + protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSource random) { + super(event, source, random); + } + + @Override + public @Nullable WeighedSoundEvents resolve(SoundManager manager) { + WeighedSoundEvents events = super.resolve(manager); + if (events != null) { + return events; + } + file = RuntimeAssets.soundFile(identifier.getPath()); + if (file == null) { + return null; + } + // Streamed, so the file is read as it plays and never cached as a buffer. + sound = new Sound(identifier, ConstantFloat.of(1.0F), ConstantFloat.of(1.0F), 1, Sound.Type.FILE, true, false, 16); + return new WeighedSoundEvents(identifier, null); + } + + /** + * Forge and NeoForge call this through their sound instance extension, which it overrides + * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + */ + public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { + return openStream(buffers, sound.getPath(), looping); + } + + /** Streams the file found on disk, or the resource {@code path} when there is none. */ + public CompletableFuture openStream(SoundBufferLibrary buffers, Identifier path, boolean looping) { + Path source = file; + if (source == null) { + return buffers.getStream(path, looping); + } + return CompletableFuture.supplyAsync(() -> { + try { + InputStream in = new BufferedInputStream(Files.newInputStream(source)); + return looping ? new LoopingAudioStream(JOrbisAudioStream::new, in) : new JOrbisAudioStream(in); + } catch (IOException e) { + throw new CompletionException(e); + } + }, Util.nonCriticalIoPool()); + } +} From c140104e4c859ec52e35a456958ab2936c889cdc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:58 +0800 Subject: [PATCH 0718/1461] Add client/workshop/WorkshopClient.java to the 1.21.11 shared sources --- .../client/workshop/WorkshopClient.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java new file mode 100644 index 00000000..26401727 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSubmitPacket; +import com.jnhprojects.mcheli.network.TestFlightPacket; +import net.minecraft.client.Minecraft; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; + +/** + * What the workshop does beyond its screens: takes the player to a test flight and back, and sends + * a pack to the server. The screen stays alive during a flight, so the undo history and the view are + * as they were when the pilot gets out. + */ +public final class WorkshopClient { + /** If no vehicle has come after this long, the server turned the flight down and said why. */ + private static final int SPAWN_TIMEOUT_TICKS = 100; + /** About 1.2 MB a second, so a download-sized pack does not crowd out the game's own packets. */ + private static final int SUBMIT_CHUNKS_PER_TICK = 2; + + private static @Nullable WorkshopScreen waiting; + private static boolean airborne; + private static int ticks; + private static byte @Nullable [] submitting; + private static int submitted; + + private WorkshopClient() { + } + + static void startTestFlight(WorkshopScreen screen, String vehicle) { + waiting = screen; + airborne = false; + ticks = 0; + Minecraft.getInstance().setScreen(null); + ModNetwork.sendToServer(new TestFlightPacket(vehicle)); + } + + /** Starts sending a pack's zip to the server, a few chunks a tick. */ + static void submit(byte[] zip) { + submitting = zip; + submitted = 0; + } + + /** How far along the zip being sent is, from 0 to 100, or -1 when none is. */ + static int submitProgress() { + byte[] zip = submitting; + return zip != null ? (int) (submitted * 100L / zip.length) : -1; + } + + public static void tick() { + tickSubmit(); + WorkshopScreen screen = waiting; + Minecraft mc = Minecraft.getInstance(); + if (screen == null || mc.player == null) { + return; + } + boolean flying = mc.player.getVehicle() instanceof AircraftEntity; + airborne |= flying; + ticks++; + if (airborne ? !flying : ticks > SPAWN_TIMEOUT_TICKS) { + waiting = null; + // Not over another screen the player has opened in flight, such as the pause menu. + if (mc.screen == null) { + mc.setScreen(screen); + } + } + } + + private static void tickSubmit() { + byte[] zip = submitting; + if (zip == null) { + return; + } + for (int i = 0; i < SUBMIT_CHUNKS_PER_TICK && submitted < zip.length; i++) { + int end = Math.min(submitted + PackSubmitPacket.CHUNK_SIZE, zip.length); + ModNetwork.sendToServer(new PackSubmitPacket(submitted, zip.length, Arrays.copyOfRange(zip, submitted, end))); + submitted = end; + } + if (submitted >= zip.length) { + submitting = null; + } + } + + public static void loggedOut() { + submitting = null; + WorkshopScreen screen = waiting; + waiting = null; + if (screen != null) { + screen.releaseAssets(); + } + } +} From 9a6085d78b9347545a0acbaf39aa766a8bba2a37 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:58 +0800 Subject: [PATCH 0719/1461] Add client/workshop/WorkshopHomeScreen.java to the 1.21.11 shared sources --- .../client/workshop/WorkshopHomeScreen.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java new file mode 100644 index 00000000..2ad552c6 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java @@ -0,0 +1,201 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.workshop.ModelImport; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopKind; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Where the workshop starts: the vehicles in the content packs on the left, ready to open, and on + * the right a form to start a new one from an OBJ file, which can be dropped on the window. + */ +public class WorkshopHomeScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int FORM_WIDTH = 190; + private static final int MAX_LIST_WIDTH = 300; + private static final int LINE_HEIGHT = 10; + + private final List entries; + private int scroll; + private WorkshopKind kind = WorkshopKind.HELICOPTER; + private String vehicleText = ""; + private String packText = ""; + private String modelText = ""; + private @Nullable Component error; + private @Nullable EditBox vehicleBox; + private @Nullable EditBox packBox; + private @Nullable EditBox modelBox; + + public WorkshopHomeScreen() { + super(Component.translatable("gui.mcheli.workshop.title")); + entries = WorkshopProject.list(MCHeli.packs().folder()); + } + + @Override + protected void init() { + listRows(); + form(); + } + + private int listWidth() { + return Math.clamp(width - FORM_WIDTH - 30, 100, MAX_LIST_WIDTH); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + private void listRows() { + int rows = visibleRows(); + for (int i = scroll; i < entries.size() && i < scroll + rows; i++) { + WorkshopProject.Entry entry = entries.get(i); + String suffix = entry.zipped() ? " (zip)" : ""; + addRenderableWidget(Button.builder(Component.literal(entry.pack() + " / " + entry.vehicle() + suffix), b -> open(entry)) + .bounds(10, TOP + (i - scroll) * ROW, listWidth(), 20).build()); + } + } + + private void form() { + int x = width - FORM_WIDTH - 10; + int y = TOP; + vehicleBox = box(x, y + 10, Component.translatable("gui.mcheli.workshop.new.vehicle"), vehicleText, 64); + vehicleBox.setResponder(text -> { + // The pack follows the vehicle's name until the player gives it its own. + if (packBox != null && packBox.getValue().equals(vehicleText)) { + packBox.setValue(text); + } + vehicleText = text; + }); + packBox = box(x, y + 44, Component.translatable("gui.mcheli.workshop.new.pack"), packText, 64); + packBox.setResponder(text -> packText = text); + addRenderableWidget(Button.builder(kindName(), b -> { + kind = WorkshopKind.values()[(kind.ordinal() + 1) % WorkshopKind.values().length]; + b.setMessage(kindName()); + }).bounds(x, y + 68, FORM_WIDTH, 20).build()); + modelBox = box(x, y + 102, Component.translatable("gui.mcheli.workshop.new.model"), modelText, 1024); + modelBox.setResponder(text -> modelText = text); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.new.create"), b -> create()) + .bounds(x, y + 126, FORM_WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.home.packs"), b -> minecraft.setScreen(new WorkshopPacksScreen())) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private EditBox box(int x, int y, Component name, String value, int maxLength) { + EditBox box = new EditBox(font, x, y, FORM_WIDTH, 18, name); + box.setMaxLength(maxLength); + box.setValue(value); + return addRenderableWidget(box); + } + + private Component kindName() { + return Component.translatable("gui.mcheli.workshop.new.kind", Component.translatable("gui.mcheli.workshop.kind." + kind.id())); + } + + private void open(WorkshopProject.Entry entry) { + try { + minecraft.setScreen(new WorkshopScreen(WorkshopProject.open(MCHeli.packs().folder(), entry))); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + private void create() { + String vehicle = vehicleText.trim(); + AircraftDefinition existing = MCHeli.content().any(vehicle); + if (existing != null && existing.pack().equals(ContentSource.ORIGINAL)) { + // The import's models come from a resource pack, which would win over this vehicle's. + error = Component.translatable("gui.mcheli.workshop.new.name_taken", vehicle); + return; + } + try { + Path obj = Path.of(modelText.trim().replace("\"", "")); + String author = minecraft.player != null ? minecraft.player.getGameProfile().name() : ""; + WorkshopProject project = WorkshopProject.create(MCHeli.packs().folder(), packText.trim(), kind, vehicle, author, + obj, ModelImport.findTexture(obj)); + minecraft.setScreen(new WorkshopScreen(project)); + } catch (InvalidPathException e) { + error = Component.translatable("gui.mcheli.workshop.new.bad_path", modelText); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + /** A file dropped on the window fills in the form, its name standing in for the vehicle's. */ + @Override + public void onFilesDrop(List files) { + if (files.isEmpty() || modelBox == null || vehicleBox == null) { + return; + } + Path file = files.getFirst(); + modelBox.setValue(file.toString()); + if (vehicleText.isEmpty()) { + String name = file.getFileName().toString(); + int dot = name.lastIndexOf('.'); + vehicleBox.setValue((dot > 0 ? name.substring(0, dot) : name).toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "-")); + } + error = null; + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int max = Math.max(entries.size() - visibleRows(), 0); + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, max); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 10, TEXT); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.open"), 10, TOP - 12, DIM); + if (entries.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.none"), 10, TOP, DIM); + } + int x = width - FORM_WIDTH - 10; + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.new"), x, TOP - 12, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.vehicle"), x, TOP, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.pack"), x, TOP + 34, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.model"), x, TOP + 92, DIM); + int y = TOP + 150; + for (var line : font.split(Component.translatable("gui.mcheli.workshop.home.drop"), FORM_WIDTH)) { + graphics.drawString(font, line, x, y, DIM); + y += LINE_HEIGHT; + } + if (error != null) { + y += 2; + for (var line : font.split(error, FORM_WIDTH)) { + graphics.drawString(font, line, x, y, ERROR); + y += LINE_HEIGHT; + } + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From fcf3c1654c48bfee7e6181f0cb6fef856be53222 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:59 +0800 Subject: [PATCH 0720/1461] Add client/workshop/WorkshopPacksScreen.java to the 1.21.11 shared sources --- .../client/workshop/WorkshopPacksScreen.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java new file mode 100644 index 00000000..c83d6d17 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java @@ -0,0 +1,218 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.ClientPacks; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.workshop.PackCatalog; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Util; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * The package browser: the packs in {@code mcheli/packs} and those the server in use sent, who made + * them and what is in them. A zip someone gave the player can be dropped on it to install. + * + *

This only ever reads the packs and adds to the folder. Removing one is left to the file manager + * the Open folder button opens, since a pack can be the only copy of someone's work. + */ +public class WorkshopPacksScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int GOOD = 0xFF80E080; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int LIST_WIDTH = 170; + private static final int LINE_HEIGHT = 10; + private static final int VEHICLES_SHOWN = 8; + + /** A pack in the list, and where it came from. */ + private record Row(PackCatalog.Info info, boolean fromServer) { + } + + private final List rows = new ArrayList<>(); + private final List problems = new ArrayList<>(); + private int scroll; + private int selected; + private @Nullable Component message; + private int messageColor = DIM; + + public WorkshopPacksScreen() { + super(Component.translatable("gui.mcheli.workshop.packs.title")); + read(); + } + + private void read() { + rows.clear(); + problems.clear(); + for (ContentPack pack : ClientPacks.INSTANCE.serverPacks()) { + try { + rows.add(new Row(PackCatalog.info(pack), true)); + } catch (IOException e) { + problems.add(new PackProblem(pack.id(), "could not be read: " + e)); + } + } + PackCatalog.Listing own = PackCatalog.read(MCHeli.packs().folder()); + for (PackCatalog.Info info : own.packs()) { + // A pack the player has that the server also uses is listed once, as the server's. + if (rows.stream().noneMatch(row -> row.info().hash().equals(info.hash()))) { + rows.add(new Row(info, false)); + } + } + problems.addAll(own.problems()); + selected = Math.clamp(selected, 0, Math.max(rows.size() - 1, 0)); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + @Override + protected void init() { + scroll = Math.clamp(scroll, 0, Math.max(rows.size() - visibleRows(), 0)); + for (int i = scroll; i < rows.size() && i < scroll + visibleRows(); i++) { + int index = i; + Button row = addRenderableWidget(Button.builder(Component.literal(rows.get(i).info().manifest().name()), b -> { + selected = index; + rebuildWidgets(); + }).bounds(10, TOP + (i - scroll) * ROW, LIST_WIDTH, 20).build()); + row.active = i != selected; + } + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.packs.open_folder"), b -> openFolder()) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private void openFolder() { + Path folder = MCHeli.packs().folder(); + try { + Files.createDirectories(folder); + Util.getPlatform().openPath(folder); + } catch (IOException e) { + say(Component.literal(e.toString()), ERROR); + } + } + + private void say(Component text, int color) { + message = text; + messageColor = color; + } + + @Override + public void onFilesDrop(List files) { + for (Path file : files) { + if (!file.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) { + say(Component.translatable("gui.mcheli.workshop.packs.not_zip", file.getFileName().toString()), ERROR); + continue; + } + try { + PackManifest installed = PackCatalog.install(file, MCHeli.packs().folder()); + say(Component.translatable("gui.mcheli.workshop.packs.installed", installed.name()), GOOD); + read(); + rebuildWidgets(); + } catch (WorkshopException e) { + say(Component.literal(e.getMessage()), ERROR); + } + } + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, Math.max(rows.size() - visibleRows(), 0)); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void onClose() { + minecraft.setScreen(new WorkshopHomeScreen()); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 10, TEXT); + if (rows.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.packs.none"), 10, TOP, DIM); + } + int x = 10 + LIST_WIDTH + 10; + int w = width - x - 10; + int y = TOP; + if (selected < rows.size()) { + y = details(graphics, rows.get(selected), x, y, w) + LINE_HEIGHT; + } + for (PackProblem problem : problems) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.problem", problem.pack(), problem.message()), x, y, w, ERROR); + } + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.drop"), x, y + 4, w, DIM); + if (message != null) { + wrapped(graphics, message, x, y + 4, w, messageColor); + } + } + + private int details(GuiGraphics graphics, Row row, int x, int y, int w) { + PackCatalog.Info info = row.info(); + PackManifest m = info.manifest(); + y = wrapped(graphics, Component.literal(m.name() + " " + m.version()), x, y, w, TEXT); + if (!m.author().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.author", m.author()), x, y, w, TEXT); + } + if (!m.description().isEmpty()) { + y = wrapped(graphics, Component.literal(m.description()), x, y + 2, w, DIM); + } + y += 4; + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.id", m.id()), x, y, w, DIM); + // A small pack is still something, and 0.0 MB reads as an empty one. + String megabytes = "%.1f".formatted(Math.max(info.size() / 1048576.0, 0.1)); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.size", megabytes, ContentPack.shortHash(info.hash())), x, y, w, DIM); + Component source = row.fromServer() ? Component.translatable("gui.mcheli.workshop.packs.from_server") + : Component.literal(info.source().getFileName().toString() + (info.zipped() ? "" : "/")); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.source", source), x, y, w, DIM); + y += 4; + if (!info.vehicles().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.vehicles", info.vehicles().size(), shortList(info.vehicles())), x, y, w, TEXT); + } + if (!info.weapons().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.weapons", info.weapons().size(), shortList(info.weapons())), x, y, w, TEXT); + } + return y; + } + + private static String shortList(List names) { + if (names.size() <= VEHICLES_SHOWN) { + return String.join(", ", names); + } + return String.join(", ", names.subList(0, VEHICLES_SHOWN)) + ", ..."; + } + + private int wrapped(GuiGraphics graphics, Component text, int x, int y, int w, int color) { + for (FormattedCharSequence line : font.split(text, w)) { + graphics.drawString(font, line, x, y, color); + y += LINE_HEIGHT; + } + return y; + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From f7671d4917cd0089550e871209b87b3a85918519 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:29:59 +0800 Subject: [PATCH 0721/1461] Port WorkshopScreen to the render methods of 1.21.11 --- .../client/workshop/WorkshopScreen.java | 1646 +++++++++++++++++ 1 file changed, 1646 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java new file mode 100644 index 00000000..19435963 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java @@ -0,0 +1,1646 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.render.AircraftRenderState; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.workshop.AssetImport; +import com.jnhprojects.mcheli.workshop.EditHistory; +import com.jnhprojects.mcheli.workshop.PartField; +import com.jnhprojects.mcheli.workshop.PartType; +import com.jnhprojects.mcheli.workshop.TuningKey; +import com.jnhprojects.mcheli.workshop.WorkshopCamera; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopPart; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.FormattedCharSequence; +import org.joml.Matrix3x2fStack; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.TreeSet; +import java.util.function.Consumer; + +/** + * The workshop's editor. The vehicle turns in the middle, drawn by the game's own vehicle renderer + * through a GUI entity, so what shows is what will fly. The parts list is on the left, the values + * of what is selected on the right, and gizmos for every part lie over the model. + * + *

Gizmos are plain GUI drawing at the points {@link WorkshopCamera#project} gives. They always + * show, even when the part is inside the hull, which is where most seats and pivots are. + * + *

Left drag on empty space turns the view, right drag moves it, the wheel zooms. Dragging a + * handle moves the part in the plane of the screen and an arrow along its axis, snapping to a + * sixteenth of a block unless Shift is held. + */ +public class WorkshopScreen extends Screen { + private static final int LEFT = 112; + private static final int RIGHT = 144; + private static final int TOP = 26; + private static final int BOTTOM = 26; + private static final int LIST_ROW = 16; + private static final int FIELD_ROW = 20; + private static final int LABEL_ROW = 10; + + private static final int PANEL = 0xFF181818; + private static final int VIEW = 0xFF2A2F36; + private static final int GRID = 0xFF3A414A; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int WARNING = 0xFFFFC040; + private static final int INVALID = 0xFFFF6060; + private static final int X_COLOR = 0xFFFF5050; + private static final int Y_COLOR = 0xFF60E060; + private static final int Z_COLOR = 0xFF5090FF; + private static final int SELECTED = 0xFFFFFFFF; + private static final int HIT_BOX = 0xFF00E0FF; + private static final int AXIS = 0xFFFF60FF; + + private static final int HANDLE_RADIUS = 3; + private static final double PICK_DISTANCE = 6.0; + private static final double ARROW_LENGTH = 34.0; + private static final double SNAP = 16.0; + private static final int GRID_HALF_SIZE = 16; + private static final int GRID_STEP = 2; + private static final double ZOOM_STEP = 1.15; + + private static final int PREVIEW_SIZE = 64; + private static final int PLOT_HEIGHT = 56; + private static final float PITCH_SNAP = 20.0F; + private static final String DEFAULT_CURVE = "0:0.7, 0.25:0.85, 0.5:1, 0.75:1.15, 1:1.3"; + + private enum Tab { + PARTS, PAINT, SOUND, TUNING, PACK + } + + private enum Drag { + NONE, ORBIT, PAN, PLANE, AXIS_X, AXIS_Y, AXIS_Z, CURVE + } + + private record Label(Component text, int x, int y, int color) { + } + + private record WrappedLine(FormattedCharSequence text, int x, int y) { + } + + private record MergeKey(int line, String field, int component) { + } + + private final WorkshopProject project; + private final WorkshopCamera camera = new WorkshopCamera(); + private final EditHistory> history = new EditHistory<>(); + private final List

Movement is simulated wherever {@link #isLocalInstanceAuthoritative()} is true. With a player + * at the controls that is the pilot's client, exactly like vanilla boats, and the server checks + * the result through the regular vehicle move packet. Without a pilot the server simulates the + * vehicle on its own. Roll, throttle and key state are not part of the vanilla packet, so the + * pilot sends them separately and the server republishes them through synched data. + * + *

Seats are explicit. Vanilla only knows an ordered passenger list, so the seat each passenger + * occupies is kept in a synched seat map of entity ids. Seat zero is the pilot and decides who + * controls the vehicle; crew seats follow, then cargo racks. + */ +public abstract class AircraftEntity extends VehicleEntity { + private static final EntityDataAccessor DATA_TYPE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TEXTURE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_HP = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_THROTTLE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_REVERSE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_ROLL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_HOVERING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_GUNNER_MODE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_INPUT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_PARTS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SEATS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_SELECTION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_USE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TV_MISSILE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_FLARE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SINKING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_FUEL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + /** The UAV station a drone is flown from, by entity id, or 0. */ + private static final EntityDataAccessor DATA_STATION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_REPELLING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SEARCH_LIGHT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + + /** Marks a player flying a drone from a station, so a stale hide can be undone at login. */ + public static final String REMOTE_PILOT_TAG = "mcheli_remote_pilot"; + + // Bits of DATA_PARTS, the same assignment the original used for its part status word. + protected static final int PART_CANOPY = 0; + protected static final int PART_NOZZLE = 1; + protected static final int PART_LANDING_GEAR = 2; + protected static final int PART_WING = 3; + protected static final int PART_HATCH = 4; + + private static final int EMPTY = -1; + /** How far above its hit box the original drew a vehicle's model; see {@link #waterDepth}. */ + private static final float LEGACY_MODEL_LIFT = 0.35F; + /** How fast a cooked-off boat goes under, in blocks per tick (about 0.4 blocks a second). */ + private static final double SINK_SPEED = 0.02; + /** The bow-up pitch and the list a sinking boat settles into, in degrees. */ + private static final float SINK_PITCH = 14.0F; + private static final float SINK_LIST = 12.0F; + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + protected final ControlInput input = new ControlInput(); + public final AnimationState animation = new AnimationState(); + + /** Throttle in [0, 1] as seen by the simulating side. Other sides read {@link #DATA_THROTTLE}. */ + protected double throttle; + /** Reverse throttle in [0, 0.6] for vehicles with {@code EnableBack}. */ + protected float reverse; + protected float roll; + public float rollO; + protected int modeSwitchCooldown; + private int ticksAlive; + + private @Nullable AircraftDefinition cachedDefinition; + private String cachedName = ""; + private int cachedGeneration; + + protected @Nullable PartState canopy; + protected @Nullable PartState landingGear; + protected @Nullable PartState hatch; + private final List parts = new ArrayList<>(); + + private int[] seatIds = new int[0]; + private int pendingSeat = EMPTY; + private final Map savedSeats = new HashMap<>(); + private final Map lastSeats = new HashMap<>(); + /** The station a drone is flown from, and where its pilot goes back to. Server only. */ + private @Nullable UUID station; + private @Nullable Vec3 stationReturn; + /** Remote pilots leaving this tick, by entity id, with where they come back down. */ + private final Map remoteReturns = new HashMap<>(); + /** Where a workshop test flight started, or {@code null} for an ordinary vehicle. Server only. */ + private @Nullable Vec3 testFlightStart; + private boolean testFlightOver; + /** How far the ropes hang below their hooks, as a negative number; both sides let them out. */ + private float ropeLength; + private int repellingTicks; + private int lastRope; + /** Passengers are leaving one by one under parachutes. Server only. */ + private boolean droppingTroops; + + private @Nullable VehicleWeapons weapons; + private int @Nullable [] savedAmmo; + private @Nullable TvMissileEntity tvMissile; + + // Damage model + /** Damage factor of the extra hit box the last projectile ray struck, {@code lastBBDamageFactor}. */ + private float hitBoxFactor = 1.0F; + private int timeSinceHit; + /** The fuel load from which an aircraft counts as heavy: the H8K flying boat and up. */ + private static final int HEAVY_FUEL = 3000; + /** How long a wreck stays, burning, before it disappears: 30 seconds. */ + private static final int WRECK_TICKS = 600; + /** The wreck fire dies down over the last five seconds. */ + private static final int WRECK_FADE_TICKS = 100; + /** Ticks until a destroyed wreck disappears; above zero means destroyed. Server only. */ + private int despawnCount; + /** Ticks the client has seen this vehicle as a wreck, for the fire to die down on time. */ + private int wreckTicks; + /** Ticks since this aircraft was shot down, for the crash spin. Counted on both sides. */ + private int spinTicks; + private boolean exploded; + /** Blasts still to come while a wrecked boat cooks off, and ticks until the next one. */ + private int cookOffBlasts; + private int cookOffDelay; + private @Nullable Entity cookOffCause; + private int alertCount; + + /** Fuel burnt but not yet taken from the tank, which only holds whole units. Server only. */ + private double fuelBurnt; + /** Ticks a fuel truck nearby keeps this vehicle from burning fuel. Server only. */ + private int fuelSuppliedCount; + /** One bit per seat whose occupant has night vision on. Server only, not saved. */ + private int nightVisionSeats; + /** Fuel and parachute slots and the cargo chest. Only the server's copy holds anything. */ + private final VehicleInventory inventory = new VehicleInventory(this); + + // Crashes, measured from position so they work whichever side simulates the vehicle. + /** Blocks per tick into terrain, sideways, that count as a crash: about 25 m/s. */ + private static final double TERRAIN_CRASH_SPEED = 1.25; + /** + * How far a hull reaches past its main box, at most, when looking for neighbours: other + * vehicles are found by their main box, and the longest hulls stretch about this far from it. + */ + private static final double HULL_REACH = 24.0; + /** Closing speed between two vehicles that counts as a crash: about 10 m/s. */ + private static final double VEHICLE_CRASH_SPEED = 0.5; + /** Highest point since a ground vehicle left the ground, or NaN while it is on it. */ + private double airborneFromY = Double.NaN; + private @Nullable Vec3 lastTickPos; + private Vec3 lastVelocity = Vec3.ZERO; + private int collisionCooldown; + + protected AircraftEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_TYPE, ""); + builder.define(DATA_TEXTURE, ""); + builder.define(DATA_HP, 0); + builder.define(DATA_THROTTLE, 0.0F); + builder.define(DATA_REVERSE, 0.0F); + builder.define(DATA_ROLL, 0.0F); + builder.define(DATA_HOVERING, false); + builder.define(DATA_GUNNER_MODE, false); + builder.define(DATA_INPUT, (byte) 0); + builder.define(DATA_PARTS, 0); + builder.define(DATA_SEATS, ""); + builder.define(DATA_WEAPON_SELECTION, ""); + builder.define(DATA_WEAPON_USE, 0); + builder.define(DATA_TV_MISSILE, 0); + builder.define(DATA_FLARE, false); + builder.define(DATA_SINKING, false); + builder.define(DATA_FUEL, 0); + builder.define(DATA_STATION, 0); + builder.define(DATA_REPELLING, false); + builder.define(DATA_SEARCH_LIGHT, false); + } + + // Definition + + /** + * Looks the definition up by name. Returns {@code null} for vehicles whose content was removed. + * After a content reload the next call picks up the new definition object. + */ + public @Nullable AircraftDefinition definition() { + String name = typeName(); + int generation = MCHeli.content().generation(); + if (!name.equals(cachedName) || generation != cachedGeneration) { + boolean reloaded = name.equals(cachedName); + cachedName = name; + cachedGeneration = generation; + AircraftDefinition previous = cachedDefinition; + cachedDefinition = name.isEmpty() ? null : lookup(name); + if (cachedDefinition != null && cachedDefinition != previous) { + if (reloaded && weapons != null) { + // The same vehicle with new numbers: keep the rounds it carries. + savedAmmo = weapons.saveAmmo(); + } + onDefinitionChanged(cachedDefinition); + } + } + return cachedDefinition; + } + + /** Finds a definition of this entity's kind. */ + protected abstract @Nullable AircraftDefinition lookup(String name); + + /** Builds the per vehicle state that depends on the definition: seats, parts, animation arrays. */ + protected void onDefinitionChanged(AircraftDefinition def) { + if (seatIds.length != def.seatAndRackCount()) { + int[] resized = new int[def.seatAndRackCount()]; + Arrays.fill(resized, EMPTY); + System.arraycopy(seatIds, 0, resized, 0, Math.min(seatIds.length, resized.length)); + seatIds = resized; + } + parts.clear(); + canopy = def.canopies.isEmpty() ? null : addPart(PART_CANOPY, 3.5F, PartState.Sounds.CANOPY); + landingGear = def.landingGear.isEmpty() ? null : addPart(PART_LANDING_GEAR, 2.5F, PartState.Sounds.LANDING_GEAR); + hatch = def.hatches.isEmpty() ? null : addPart(PART_HATCH, 1.5F, PartState.Sounds.HATCH); + animation.resize(def); + weapons = new VehicleWeapons(this, def); + if (savedAmmo != null) { + weapons.loadAmmo(savedAmmo); + savedAmmo = null; + } + } + + protected PartState addPart(int bit, float speed, PartState.Sounds sounds) { + PartState state = new PartState(this, bit, speed, sounds); + // A vehicle that comes into view with a part already switched should not animate it. + state.force(state.status()); + parts.add(state); + return state; + } + + /** Sets which vehicle this is. Call once, right after construction, before adding to the level. */ + public void setType(AircraftDefinition definition) { + entityData.set(DATA_TYPE, definition.name()); + entityData.set(DATA_TEXTURE, definition.textures().getFirst()); + entityData.set(DATA_HP, definition.maxHp); + entityData.set(DATA_FUEL, definition.maxFuel); + refreshDimensions(); + definition(); + } + + public String typeName() { + return entityData.get(DATA_TYPE); + } + + public String texture() { + String texture = entityData.get(DATA_TEXTURE); + return texture.isEmpty() ? typeName() : texture; + } + + public int hp() { + return entityData.get(DATA_HP); + } + + /** + * {@code repair}: adds hit points up to the maximum. A wreck stays a wreck. Server only. + * + * @return whether anything was repaired + */ + public boolean repair(int amount) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || hp() >= def.maxHp) { + return false; + } + entityData.set(DATA_HP, Math.min(hp() + amount, def.maxHp)); + return true; + } + + /** + * {@code switchNextTextureName}: the next paint scheme the definition lists. Server only. + * + * @return false when the vehicle has only one + */ + public boolean nextTexture() { + AircraftDefinition def = definition(); + if (def == null || def.textures().size() < 2) { + return false; + } + List textures = def.textures(); + int next = (textures.indexOf(texture()) + 1) % textures.size(); + entityData.set(DATA_TEXTURE, textures.get(next)); + return true; + } + + // Fuel + + public int fuel() { + return entityData.get(DATA_FUEL); + } + + public int maxFuel() { + AircraftDefinition def = definition(); + return def != null ? def.maxFuel : 0; + } + + /** {@code getFuelP}: how full the tank is, 0 to 1. Vehicles without a tank read as full. */ + public float fuelRatio() { + int max = maxFuel(); + return max <= 0 ? 1.0F : (float) fuel() / max; + } + + /** {@code canUseFuel}: a vehicle without a tank never runs dry. */ + public boolean hasFuel() { + return maxFuel() <= 0 || fuel() > 1; + } + + /** Sets the tank outright; a drone leaves its station with 5%. Server only. */ + public void setFuelLevel(int fuel) { + entityData.set(DATA_FUEL, Math.clamp(fuel, 0, Math.max(maxFuel(), 0))); + } + + /** + * Pours up to {@code amount} into the tank. Server only. + * + * @return how much went in + */ + public int refuel(int amount) { + int added = Math.clamp(maxFuel() - fuel(), 0, Math.max(amount, 0)); + if (added > 0) { + entityData.set(DATA_FUEL, fuel() + added); + } + return added; + } + + // Night vision + + /** A crew member switched night vision on or off. Server only. */ + public void setNightVision(Player player, boolean on) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (seat < 0 || seat >= 31) { + return; + } + if (on && def != null && def.nightVision && !isDestroyed()) { + nightVisionSeats |= 1 << seat; + } else if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(player); + } + } + + /** Keeps night vision on for everyone in the crew who switched it on, see {@link NightVision}. */ + private void tickNightVision() { + if (nightVisionSeats == 0) { + return; + } + if (isDestroyed()) { + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null) { + NightVision.end(crew); + } + } + nightVisionSeats = 0; + return; + } + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (hasNightVision(seat) && crew != null) { + NightVision.topUp(crew); + } + } + } + + /** Only the first 31 seats can switch it on; racks and the like come after them anyway. */ + private boolean hasNightVision(int seat) { + return seat >= 0 && seat < 31 && (nightVisionSeats & 1 << seat) != 0; + } + + /** + * {@code updateFuel}: once a second the engine burns its {@code FuelConsumption}, scaled by the + * throttle and topping out at 70% throttle. Nothing burns next to a fuel truck or with the + * server's infinite fuel on. + */ + private void burnFuel(AircraftDefinition def) { + if (def.maxFuel <= 0 || isDestroyed()) { + return; + } + if (fuelSuppliedCount > 0) { + fuelSuppliedCount--; + return; + } + if (ticksAlive % 20 != 0 || fuel() <= 1 || throttle <= 0.0 || ServerConfig.infiniteFuel()) { + return; + } + fuelBurnt += Math.min(throttle * 1.4, 1.0) * def.fuelConsumption; + if (fuelBurnt > 1.0) { + int whole = (int) fuelBurnt; + fuelBurnt -= whole; + entityData.set(DATA_FUEL, Math.max(fuel() - whole, 0)); + } + } + + /** + * {@code supplyFuel}: a fuel truck ({@code FuelSupplyRange}) tops up every vehicle within range, + * 30 units every half second. While it is on the ground it only reaches vehicles that are on + * the ground too. + */ + private void supplyFuel(AircraftDefinition def) { + float range = def.fuelSupplyRange; + if (range <= 0.0F || ticksAlive % 10 != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + if (other == this) { + continue; + } + if (!onGround() || other.onGround()) { + other.refuel(30); + } + other.fuelSuppliedCount = 40; + } + } + + // Inventory and supply + + public VehicleInventory inventory() { + return inventory; + } + + /** + * {@code canSupply}: fuel cans and resupply only work while the vehicle stands on something, + * within three blocks; a vehicle that cannot float must not be in the water either. + */ + public boolean canSupply() { + AircraftDefinition def = definition(); + return def != null && hasBlockBelow(1, 3, false) && (def.floats || !isInWater()); + } + + /** {@code openGui}: shows a crew member the vehicle screen. Server only. */ + public void openScreen(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || player.getVehicle() != this) { + return; + } + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new VehicleMenu(id, playerInventory, this), displayName(def))); + } + + /** + * {@code openInventory}: shows the cargo chest of a vehicle with an {@code InventorySize}. + * Server only. + * + * @return false when the vehicle has none + */ + public boolean openCargo(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || def.cargoRows() <= 0 || isDestroyed() || !inventory.stillValid(player)) { + return false; + } + int rows = Math.min(def.cargoRows(), 6); + MenuType type = switch (rows) { + case 1 -> MenuType.GENERIC_9x1; + case 2 -> MenuType.GENERIC_9x2; + case 3 -> MenuType.GENERIC_9x3; + case 4 -> MenuType.GENERIC_9x4; + case 5 -> MenuType.GENERIC_9x5; + default -> MenuType.GENERIC_9x6; + }; + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new ChestMenu(type, id, playerInventory, inventory.cargo(), rows), + displayName(def))); + return true; + } + + /** The vehicle's name as its item shows it. */ + private static Component displayName(AircraftDefinition def) { + return Component.translatableWithFallback("item.mcheli." + def.name(), def.displayName); + } + + /** + * {@code supplyAmmoToOtherAircraft}: an ammunition truck ({@code AmmoSupplyRange}) tops up the + * weapons of every vehicle within range that stands on something, a tenth of each weapon's + * maximum every two seconds. + */ + private void supplyAmmo(AircraftDefinition def) { + float range = def.ammoSupplyRange; + if (range <= 0.0F || ticksAlive % SupplyRules.AMMO_TRUCK_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + VehicleWeapons theirs = other.weapons(); + if (other == this || theirs == null || other.isDestroyed() || !other.canSupply()) { + continue; + } + for (WeaponSet set : theirs.sets()) { + int rounds = SupplyRules.roundsFromTruck(set.ammo() + set.restAmmo(), set.maxAmmo()); + if (rounds > 0) { + set.setRestAmmo(set.restAmmo() + rounds); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + } + } + } + } + + /** {@code repairOtherAircraft}: a repair vehicle ({@code RepairOtherVehicles}) mends those in range. */ + private void repairOthers(AircraftDefinition def) { + float range = def.repairOtherVehiclesRange; + if (range <= 0.0F || ticksAlive % SupplyRules.REPAIR_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + AircraftDefinition theirs = other.definition(); + if (other != this && theirs != null) { + other.repair(SupplyRules.repair(other.hp(), theirs.maxHp, def.repairOtherVehiclesValue)); + } + } + } + + /** Whether the flight model holds a hover: in hover mode, or while the pilot mans the guns. */ + public boolean isHovering() { + return isHoveringMode() || isGunnerMode(); + } + + /** The hover mode the helicopter pilot switches with jump. */ + public boolean isHoveringMode() { + return entityData.get(DATA_HOVERING); + } + + protected void setHovering(boolean hovering) { + entityData.set(DATA_HOVERING, hovering); + } + + // Pilot gunner mode + + /** + * Pilot gunner mode ({@code EnableGunnerMode}): the pilot leaves the controls to man the + * weapons from the camera position. Helicopters hold a hover; planes level off and circle. + */ + public boolean isGunnerMode() { + return entityData.get(DATA_GUNNER_MODE); + } + + /** {@code canSwitchGunnerMode}: the kinds add their own attitude and speed limits. */ + public boolean canSwitchGunnerMode() { + AircraftDefinition def = definition(); + if (def == null || !def.gunnerMode || !isCanopyClosed() || isDestroyed() || isHoveringMode()) { + return false; + } + // Without ConcurrentGunnerMode a player in the gunner seat has the guns already. + return def.concurrentGunnerMode || !(occupant(1) instanceof Player); + } + + /** Server side. Leaving gunner mode is always allowed; entering it only when {@link #canSwitchGunnerMode}. */ + public void setGunnerMode(boolean on) { + if (!on || canSwitchGunnerMode()) { + entityData.set(DATA_GUNNER_MODE, on); + } + } + + /** Whether {@code entity} is the pilot and currently in gunner mode. */ + public boolean isGunnerMode(Entity entity) { + return isGunnerMode() && seatOf(entity) == 0; + } + + /** Drops gunner mode once its conditions no longer hold. Server side, every tick. */ + private void checkGunnerMode(AircraftDefinition def) { + if (isGunnerMode() && (occupant(0) == null || isDestroyed() + || (!def.concurrentGunnerMode && occupant(1) instanceof Player))) { + entityData.set(DATA_GUNNER_MODE, false); + } + } + + // Throttle, roll and input + + /** Throttle for animation and sound, valid on every side. */ + public float visibleThrottle() { + return isSimulatingHere() ? (float) throttle : entityData.get(DATA_THROTTLE); + } + + public float visibleReverse() { + return isSimulatingHere() ? reverse : entityData.get(DATA_REVERSE); + } + + public float roll() { + return isSimulatingHere() ? roll : entityData.get(DATA_ROLL); + } + + public float roll(float partialTicks) { + return Mth.rotLerp(partialTicks, rollO, roll()); + } + + public void setRoll(float roll) { + this.roll = roll; + } + + protected boolean isSimulatingHere() { + return isLocalInstanceAuthoritative() && !isPassenger(); + } + + public ControlInput input() { + return input; + } + + /** + * Applies the pilot's view change for one frame. Only called on the pilot's client, and only + * for kinds that steer with the mouse. + * + * @param dYaw change of the player's yaw since the last frame, in degrees + * @param dPitch change of the player's pitch since the last frame, in degrees + */ + public void applyPilotLook(float dYaw, float dPitch) { + } + + /** Whether the pilot steers with the mouse, as in helicopters and planes. */ + public boolean steersWithMouse() { + return false; + } + + /** Server side entry point for the pilot's control packet. */ + public void acceptPilotState(float throttle, float reverse, float roll, boolean hovering, byte keys) { + this.throttle = Mth.clamp(throttle, 0.0F, 1.0F); + this.reverse = Mth.clamp(reverse, 0.0F, 1.0F); + this.roll = AircraftRotation.wrap(roll); + input.read(keys); + entityData.set(DATA_THROTTLE, (float) this.throttle); + entityData.set(DATA_REVERSE, this.reverse); + entityData.set(DATA_ROLL, this.roll); + entityData.set(DATA_HOVERING, hovering); + entityData.set(DATA_INPUT, keys); + } + + public float reverse() { + return reverse; + } + + // Tick + + /** + * Makes this a workshop test flight: it takes no damage, is not saved with the world, and goes + * away once its pilot gets out, putting them back where they started. + */ + public void startTestFlight(Vec3 start) { + testFlightStart = start; + } + + @Override + public boolean shouldBeSaved() { + return testFlightStart == null && super.shouldBeSaved(); + } + + @Override + public void tick() { + if (testFlightOver) { + discard(); + return; + } + rollO = roll(); + if (getHurtTime() > 0) { + setHurtTime(getHurtTime() - 1); + } + super.tick(); + interpolation.interpolate(); + ticksAlive++; + lastSeats.clear(); + remoteReturns.clear(); + if (modeSwitchCooldown > 0) { + modeSwitchCooldown--; + } + + AircraftDefinition def = definition(); + if (def == null) { + // Content for this vehicle no longer exists. Keep the entity inert rather than crash. + setDeltaMovement(Vec3.ZERO); + return; + } + if (timeSinceHit > 0) { + timeSinceHit--; + } + if (alertCount > 0) { + alertCount--; + } + boolean groundedBefore = onGround(); + double fallBefore = getDeltaMovement().y; + tickDestroyed(); + + if (!level().isClientSide()) { + supplyFuel(def); + supplyAmmo(def); + repairOthers(def); + burnFuel(def); + if (def.maxFuel > 0 && fuel() < def.maxFuel && !isDestroyed()) { + inventory.pourFuelCans(ticksAlive); + } + tickNightVision(); + tickStation(); + } + tickRopes(def); + + if (isSimulatingHere()) { + if (!(getControllingPassenger() instanceof Player) || isDestroyed()) { + input.clear(); + } + if (isRepelling()) { + // {@code MCH_AircraftClientTickHandler}: the pilot holds a hover while the ropes are out. + input.throttleUp = false; + input.throttleDown = false; + input.left = false; + input.right = false; + } + if (!hasFuel()) { + // An empty tank: the throttle runs down by itself, as with the pilot's hand off it. + input.throttleUp = false; + } + if (entityData.get(DATA_SINKING)) { + sink(); + } else { + simulate(); + } + float[] kick = weapons != null ? weapons.recoilKick() : null; + if (kick != null) { + setXRot(getXRot() + kick[0]); + roll += kick[1]; + } + if (!level().isClientSide()) { + entityData.set(DATA_THROTTLE, (float) throttle); + entityData.set(DATA_REVERSE, reverse); + entityData.set(DATA_ROLL, roll); + entityData.set(DATA_INPUT, input.write()); + } + } else { + if (!isLocalInstanceAuthoritative()) { + input.read(entityData.get(DATA_INPUT)); + } + setDeltaMovement(Vec3.ZERO); + } + for (PartState part : parts) { + part.tick(); + } + if (!level().isClientSide()) { + updatePartsServer(def); + checkGunnerMode(def); + } + animation.tick(this, def); + tickAnimation(); + if (weapons != null) { + weapons.tick(); + if (!level().isClientSide()) { + entityData.set(DATA_FLARE, weapons.flares.isActive()); + warnIncomingMissiles(); + } + } + if (level().isClientSide()) { + damageSmoke(def); + VehicleParticles.tick(this, def); + } else { + tickImpacts(def, groundedBefore, fallBefore); + } + } + + /** + * Server side crashes. A wreck explodes when it lands; an intact vehicle takes damage when it + * meets the ground steeply tilted ({@code onUpdate_CollisionGroundDamage}), sits under water + * without floats, flies into terrain, or rams another vehicle. + */ + private void tickImpacts(AircraftDefinition def, boolean groundedBefore, double fallBefore) { + Vec3 velocity = lastTickPos != null ? position().subtract(lastTickPos) : Vec3.ZERO; + Vec3 previous = lastVelocity; + lastTickPos = position(); + lastVelocity = velocity; + if (collisionCooldown > 0) { + collisionCooldown--; + } + double speed = Math.max(velocity.length(), previous.length()); + + if (isDestroyed()) { + boolean falling = fallBefore < -0.2 || previous.y < -0.2; + boolean landed = onGround() && !groundedBefore; + boolean smashed = horizontalCollision && previous.horizontalDistance() > 0.5; + if (!exploded && (landed && falling || smashed)) { + crashExplosion(); + } + return; + } + + if (isNearGround(3)) { + float roll = Math.abs(Mth.wrapDegrees(roll())); + float pitch = Math.abs(Mth.wrapDegrees(getXRot())); + if (roll > crashTilt() || pitch > crashTilt()) { + float damage = roll + pitch; + damage *= damage < 90.0F ? 0.4F * (float) velocity.length() : 0.4F; + if (damage > 1.0F && random.nextInt(4) == 0) { + hurtServer((ServerLevel) level(), damageSources().inWall(), damage); + } + } + } + if (ticksAlive % 30 == 0 && !def.floats && level().getFluidState( + BlockPos.containing(getX() + 0.5, getY() + 1.5 + def.submergedDamageHeight, getZ() + 0.5)).is(FluidTags.WATER)) { + hurtServer((ServerLevel) level(), damageSources().inWall(), Math.max(def.maxHp / 10, 1)); + } + + if (takesGroundImpacts()) { + groundImpacts(def, previous); + } + + // Not in the original: flying into terrain or another vehicle at speed is a crash. + if (horizontalCollision && previous.horizontalDistance() > TERRAIN_CRASH_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), crashDamage(previous.horizontalDistance())); + } + if (speed > VEHICLE_CRASH_SPEED && collisionCooldown == 0) { + // A blocked move can leave the hulls up to a tick of travel apart; when the vehicle + // stopped short this tick, look that far for what stopped it. + boolean stopped = velocity.length() < previous.length() * 0.5; + double reach = stopped ? 0.5 + previous.length() : 0.5; + for (AircraftEntity other : solidNeighbours(hullBounds().inflate(reach))) { + if (!hullsTouch(other, reach)) { + continue; + } + double closing = velocity.subtract(other.lastVelocity).length(); + closing = Math.max(closing, previous.subtract(other.lastVelocity).length()); + if (closing > VEHICLE_CRASH_SPEED && other.collisionCooldown == 0) { + collisionCooldown = 20; + other.collisionCooldown = 20; + float damage = crashDamage(closing); + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), damage); + other.hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), damage); + break; + } + } + } + } + + /** {@code getGiveDamageRot}: how far a vehicle may lean near the ground before it scrapes. */ + protected float crashTilt() { + return 40.0F; + } + + /** Crash damage grows with the square of the impact speed, like the energy involved. */ + private static float crashDamage(double speed) { + return (float) (speed * speed * 60.0); + } + + /** Whether hitting walls and falling hurt this vehicle. Tanks and cars, which live on the ground. */ + protected boolean takesGroundImpacts() { + return false; + } + + /** + * A ground vehicle that hits a wall or lands after a long drop takes a share of its hit points + * by the speed or the height, see {@link GroundImpact}. The same height costs a car and a tank + * the same share before armour. + */ + private void groundImpacts(AircraftDefinition def, Vec3 previous) { + if (horizontalCollision && previous.horizontalDistance() > GroundImpact.WALL_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), GroundImpact.wall(previous.horizontalDistance()) * def.maxHp); + } + if (isInWater() || isPassenger()) { + airborneFromY = Double.NaN; + } else if (!onGround()) { + airborneFromY = Double.isNaN(airborneFromY) ? getY() : Math.max(airborneFromY, getY()); + } else if (!Double.isNaN(airborneFromY)) { + double drop = airborneFromY - getY(); + airborneFromY = Double.NaN; + if (drop > GroundImpact.SAFE_DROP) { + hurtServer((ServerLevel) level(), damageSources().fall(), GroundImpact.drop(drop) * def.maxHp); + } + } + } + + /** {@code MCH_MissileDetector}: the crew hears a warning while a missile homes on them. */ + private void warnIncomingMissiles() { + if (alertCount > 0 || isDestroyed() || weapons == null || !weapons.flares.isFitted() || ticksAlive % 5 != 0) { + return; + } + boolean tracked = !level().getEntitiesOfClass(ProjectileEntity.class, getBoundingBox().inflate(60.0), + p -> p.target() != null && (p.target() == this || hasPassenger(p.target()))).isEmpty(); + if (tracked) { + alertCount = 20; + ModSounds.playAt(this, "alert", 1.0F, 1.0F); + } + } + + /** One tick of control and physics on the simulating side. */ + protected abstract void simulate(); + + /** Visual state that every side updates, for example rotor angles. */ + protected void tickAnimation() { + } + + /** Ticks since this entity instance was created, used to delay controls right after spawning. */ + protected int ticksAlive() { + return ticksAlive; + } + + // Moving parts + + int partBits() { + return entityData.get(DATA_PARTS); + } + + void setPartBit(int bit, boolean on) { + int bits = entityData.get(DATA_PARTS); + entityData.set(DATA_PARTS, on ? bits | (1 << bit) : bits & ~(1 << bit)); + } + + public @Nullable PartState canopy() { + return canopy; + } + + public @Nullable PartState landingGear() { + return landingGear; + } + + public @Nullable PartState hatch() { + return hatch; + } + + public boolean isCanopyClosed() { + return canopy == null || canopy.isOff(); + } + + protected void openCanopy() { + if (canopy != null && modeSwitchCooldown <= 0) { + canopy.set(true, true); + modeSwitchCooldown = 20; + } + } + + protected void closeCanopy() { + if (canopy != null && modeSwitchCooldown <= 0 && canopy.status()) { + canopy.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean isLandingGearUp() { + return landingGear != null && landingGear.status(); + } + + /** Gear can be raised once the vehicle is at least ten blocks above anything solid. */ + public boolean canRaiseLandingGear() { + return landingGear != null && landingGear.angle() < 1.0F && !landingGear.status() && !hasBlockBelow(3, 10, true); + } + + public boolean canLowerLandingGear() { + return landingGear != null && landingGear.angle() >= 89.0F && landingGear.status(); + } + + public void toggleLandingGear() { + if (landingGear == null || modeSwitchCooldown > 0) { + return; + } + if (canRaiseLandingGear()) { + landingGear.set(true, true); + modeSwitchCooldown = 20; + } else if (canLowerLandingGear()) { + landingGear.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean canFoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOff(); + } + + public boolean canUnfoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOn(); + } + + /** The fold key: hatches by default, planes fold or sweep their wings instead. */ + public void toggleFold() { + if (canFoldHatch()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldHatch()) { + hatch.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Throttle below which the landing gear comes down by itself near the ground. */ + protected float autoLowerGearThrottle() { + return 0.8F; + } + + /** {@code updateParts}: automatic landing gear, plus whatever a kind adds. Server only. */ + protected void updatePartsServer(AircraftDefinition def) { + if (landingGear == null) { + return; + } + if (!landingGear.status() && landingGear.factor() <= 0.1F) { + // Amphibians retract the gear when they settle on water. + boolean onWater = isInWater() || isWaterBelow(20); + if (def.floats && onWater && (throttle <= 0.8 || onGround() || hasBlockBelow(3, 20, true))) { + landingGear.set(true, true); + } + } else if (landingGear.status() && landingGear.factor() >= 0.9F) { + boolean ground = hasBlockBelow(3, 10, true); + if (throttle < autoLowerGearThrottle() && ground) { + if (!def.floats || !isWaterBelow(150)) { + landingGear.set(false, true); + } + } else if (vtolMode() == 2 && ground) { + landingGear.set(false, true); + } + } + } + + /** 0 off, 1 tilting, 2 fully in VTOL mode. Only planes have one. */ + public int vtolMode() { + return 0; + } + + // Seats + + /** Seat (or rack) index of {@code entity}, or -1. */ + public int seatOf(Entity entity) { + int id = entity.getId(); + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == id) { + return i; + } + } + return EMPTY; + } + + public @Nullable Entity occupant(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] == EMPTY) { + return null; + } + int id = seatIds[seat]; + for (Entity passenger : getPassengers()) { + if (passenger.getId() == id) { + return passenger; + } + } + return null; + } + + public @Nullable SeatDefinition seat(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && seat >= 0 && seat < def.seatCount() ? def.seats.get(seat) : null; + } + + private boolean isSeatFree(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] != EMPTY) { + return false; + } + AircraftDefinition def = definition(); + if (def != null) { + for (int[] group : def.exclusiveSeats) { + if (contains(group, seat)) { + for (int other : group) { + if (other != seat && other >= 0 && other < seatIds.length && seatIds[other] != EMPTY) { + return false; + } + } + } + } + } + return true; + } + + private static boolean contains(int[] values, int value) { + for (int v : values) { + if (v == value) { + return true; + } + } + return false; + } + + /** First crew seat a player boarding by hand would take. Drones keep their pilot seat for the remote. */ + private int firstFreeCrewSeat() { + AircraftDefinition def = definition(); + if (def == null) { + return EMPTY; + } + for (int i = def.uav ? 1 : 0; i < def.seatCount(); i++) { + if (isSeatFree(i)) { + return i; + } + } + return EMPTY; + } + + private void assignSeat(Entity entity, int seat) { + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == entity.getId()) { + seatIds[i] = EMPTY; + if (weapons != null) { + weapons.onLeft(i); + } + } + } + if (seat >= 0 && seat < seatIds.length) { + seatIds[seat] = entity.getId(); + } + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onSeated(entity, seat); + weapons.resendAmmo(); + } + } + + private String encodeSeats() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < seatIds.length; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(seatIds[i]); + } + return sb.toString(); + } + + private void decodeSeats(String encoded) { + if (encoded.isEmpty()) { + Arrays.fill(seatIds, EMPTY); + return; + } + String[] parts = encoded.split(","); + if (parts.length != seatIds.length) { + seatIds = new int[parts.length]; + } + for (int i = 0; i < parts.length; i++) { + try { + seatIds[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + seatIds[i] = EMPTY; + } + } + } + + /** Moves {@code entity} to the next free crew seat, wrapping around. Server only. */ + public void switchSeat(Entity entity) { + AircraftDefinition def = definition(); + int current = seatOf(entity); + if (def == null || current < 0 || current >= def.seatCount() || current == 0 && station != null) { + // A remote pilot stays at the controls; they are not really aboard. + return; + } + int count = def.seatCount(); + seatIds[current] = EMPTY; + for (int step = 1; step < count; step++) { + int seat = (current + step) % count; + if (def.uav && seat == 0) { + continue; + } + if (isSeatFree(seat)) { + assignSeat(entity, seat); + if (seat == 0) { + closeCanopy(); + } + return; + } + } + assignSeat(entity, current); + } + + // UAV stations + + /** Whether a player at a station could take this drone's controls now. */ + public boolean canTakeRemotePilot() { + AircraftDefinition def = definition(); + return def != null && def.uav && !def.targetDrone && !isDestroyed() && isSeatFree(0); + } + + /** + * Seats {@code player}, who sits at {@code from}, at this drone's controls. The pilot is hidden + * and takes no harm while flying, since in the original they never left the station. Server only. + */ + public boolean takeRemotePilot(ServerPlayer player, UavStationEntity from) { + if (!canTakeRemotePilot()) { + return false; + } + pendingSeat = 0; + boolean seated = player.startRiding(this, true, true); + pendingSeat = EMPTY; + if (!seated) { + return false; + } + station = from.getUUID(); + stationReturn = from.returnPosition(); + entityData.set(DATA_STATION, from.getId()); + player.addTag(REMOTE_PILOT_TAG); + player.setInvisible(true); + player.setInvulnerable(true); + return true; + } + + /** Sends the remote pilot back to the station, if one is flying. Server only. */ + public void releaseRemotePilot() { + Entity pilot = occupant(0); + if (station != null && pilot != null) { + pilot.stopRiding(); + } + } + + /** The station this drone is flown from, as the client sees it. */ + public @Nullable UavStationEntity stationEntity() { + return level().getEntity(entityData.get(DATA_STATION)) instanceof UavStationEntity s ? s : null; + } + + /** Flat distance to the station this drone is flown from, or -1 without one. */ + public double stationDistance() { + UavStationEntity from = stationEntity(); + if (from == null) { + return -1.0; + } + double dx = getX() - from.getX(); + double dz = getZ() - from.getZ(); + return Math.sqrt(dx * dx + dz * dz); + } + + /** Whether {@code entity} flies this drone from a station rather than sitting in it. */ + public boolean isRemotePilot(Entity entity) { + return (station != null || entityData.get(DATA_STATION) != 0) && occupant(0) == entity; + } + + private void endRemoteControl(Entity pilot) { + UavStationEntity from = level() instanceof ServerLevel server && station != null + && server.getEntity(station) instanceof UavStationEntity s ? s : null; + Vec3 home = from != null ? from.returnPosition() : stationReturn; + if (home != null) { + remoteReturns.put(pilot.getId(), home); + } + clearRemotePilot(pilot); + if (from != null) { + from.setLinked(false); + } + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + } + + /** Undoes what {@link #takeRemotePilot} did to the player. */ + public static void clearRemotePilot(Entity pilot) { + if (pilot.removeTag(REMOTE_PILOT_TAG)) { + pilot.setInvisible(false); + pilot.setInvulnerable(false); + } + } + + /** + * {@code updateUAV}: the link breaks when the station is gone, and flying out of its range + * loses the drone, as in the original. + */ + private void tickStation() { + if (station == null || !(level() instanceof ServerLevel server)) { + return; + } + Entity pilot = occupant(0); + if (pilot == null) { + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + return; + } + UavStationEntity from = server.getEntity(station) instanceof UavStationEntity s && s.isAlive() ? s : null; + if (from == null) { + pilot.stopRiding(); + return; + } + entityData.set(DATA_STATION, from.getId()); + if (!from.inRange(this) && !isDestroyed()) { + if (pilot instanceof Player player) { + player.displayClientMessage(Component.translatable("message.mcheli.uav.lost"), true); + } + pilot.stopRiding(); + hurtServer(server, damageSources().fellOutOfWorld(), hp() + 10.0F); + } + } + + @Override + public @Nullable LivingEntity getControllingPassenger() { + // A wreck is nobody's to fly; the server lets it fall, so its crash is seen the same way by everyone. + return occupant(0) instanceof Player player && !isDestroyed() ? player : null; + } + + @Override + protected boolean canAddPassenger(Entity passenger) { + AircraftDefinition def = definition(); + if (def == null || !def.canRide) { + return false; + } + if (pendingSeat >= 0) { + return isSeatFree(pendingSeat); + } + // Coming back aboard after a reload, a remote pilot included. + Integer saved = savedSeats.get(passenger.getUUID()); + if (saved != null && isSeatFree(saved)) { + return true; + } + // Cargo only boards through a rack, never into a crew seat. + return !(passenger instanceof AircraftEntity || passenger instanceof ContainerEntity) && firstFreeCrewSeat() >= 0; + } + + @Override + protected void addPassenger(Entity passenger) { + super.addPassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = pendingSeat; + pendingSeat = EMPTY; + if (seat < 0) { + Integer saved = savedSeats.remove(passenger.getUUID()); + seat = saved != null && isSeatFree(saved) ? saved : firstFreeCrewSeat(); + } + assignSeat(passenger, seat); + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = seatOf(passenger); + if (seat >= 0) { + if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(passenger); + } + lastSeats.put(passenger.getId(), seat); + seatIds[seat] = EMPTY; + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onLeft(seat); + } + } + if (seat == 0) { + input.clear(); + if (station != null) { + endRemoteControl(passenger); + } + if (testFlightStart != null) { + remoteReturns.put(passenger.getId(), testFlightStart); + testFlightOver = true; + } + } + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + AircraftDefinition def = definition(); + int seat = seatOf(passenger); + if (def == null || seat < 0 || seat >= def.seatAndRackCount()) { + return super.getPassengerAttachmentPoint(passenger, dimensions, scale); + } + Vector3f p = seatPosition(def, seat, 1.0F); + return new Vec3(p.x, p.y, p.z); + } + + /** World offset of a seat from the vehicle origin. Turret seats turn with the pilot's view. */ + public Vector3f seatPosition(AircraftDefinition def, int seat, float partialTicks) { + Vector3 pos; + boolean turret; + if (seat < def.seatCount()) { + SeatDefinition s = def.seats.get(seat); + pos = s.position(); + turret = s.rotatingSeat(); + } else { + RackDefinition rack = def.rack(seat); + pos = rack.position(); + turret = rack.rotatingSeat(); + } + return toWorldOffset(turret ? onTurret(def, pos, partialTicks) : pos); + } + + /** Rotates a hull-space point around {@code TurretPosition} by the pilot's view yaw. */ + public Vector3 onTurret(AircraftDefinition def, Vector3 pos, float partialTicks) { + Vector3 t = def.turretPosition; + Vector3f v = AircraftRotation.toWorld(animation.seatYaw(0, partialTicks), 0.0F, 0.0F, + pos.x() - t.x(), pos.y() - t.y(), pos.z() - t.z()); + return new Vector3(v.x + t.x(), v.y + t.y(), v.z + t.z()); + } + + /** Rotates a hull-space point into the world frame, relative to the vehicle origin. */ + public Vector3f toWorldOffset(Vector3 local) { + return AircraftRotation.toWorld(getYRot(), getXRot(), roll(), local.x(), local.y(), local.z()); + } + + public Vec3 toWorld(Vector3 local) { + Vector3f v = toWorldOffset(local); + return position().add(v.x, v.y, v.z); + } + + @Override + protected void positionRider(Entity passenger, MoveFunction moveFunction) { + super.positionRider(passenger, moveFunction); + AircraftDefinition def = definition(); + if (def != null && passenger instanceof AircraftEntity cargo) { + RackDefinition rack = def.rack(seatOf(passenger)); + float yaw = rack != null ? rack.yaw() : 0.0F; + float pitch = rack != null ? rack.pitch() : 0.0F; + cargo.setYRot(getYRot() + yaw); + cargo.setXRot(getXRot() + pitch); + cargo.roll = roll(); + cargo.setDeltaMovement(Vec3.ZERO); + } else if (def != null && passenger instanceof ContainerEntity container) { + RackDefinition rack = def.rack(seatOf(passenger)); + container.setYRot(getYRot() + (rack != null ? rack.yaw() : 0.0F)); + container.setDeltaMovement(Vec3.ZERO); + } + } + + @Override + public Vec3 getDismountLocationForPassenger(LivingEntity passenger) { + Vec3 home = remoteReturns.remove(passenger.getId()); + if (home != null) { + return home; + } + AircraftDefinition def = definition(); + Integer seat = lastSeats.get(passenger.getId()); + if (def != null) { + Vector3 local; + if (def.unmountPosition != null) { + local = def.unmountPosition; + } else { + Vector3 seatPos = seat != null && seat < def.seatCount() ? def.seats.get(seat).position() : def.seats.getFirst().position(); + double x = seatPos.x() >= 0.0 ? seatPos.x() + 3.0 : seatPos.x() - 3.0; + local = new Vector3(x, 2.0, seatPos.z()); + } + Vec3 at = toWorld(local); + AABB box = passenger.getDimensions(Pose.STANDING).makeBoundingBox(at); + if (level().noCollision(passenger, box)) { + return at; + } + } + return super.getDismountLocationForPassenger(passenger); + } + + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + ItemStack held = player.getItemInHand(hand); + if (held.getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (definition() != null && (held.getItem() instanceof FuelItem || held.getItem() instanceof WrenchItem)) { + if (level().isClientSide()) { + return InteractionResult.SUCCESS; + } + return held.getItem() instanceof FuelItem ? FuelItem.pour(held, this, player) : WrenchItem.useOn(held, this, player, hand); + } + InteractionResult result = super.interact(player, hand); + AircraftDefinition def = definition(); + if (result == InteractionResult.PASS && player.isSecondaryUseActive() && def != null && def.cargoRows() > 0 + && !isDestroyed() && player.getVehicle() != this) { + // Sneaking opens the cargo chest from outside, as in the original. + if (player instanceof ServerPlayer server) { + openCargo(server); + } + return InteractionResult.SUCCESS; + } + if (result != InteractionResult.PASS || player.isSecondaryUseActive()) { + return result; + } + if (def == null || !def.canRide || player.getVehicle() == this) { + return InteractionResult.PASS; + } + boolean boardingPilot = !def.uav && isSeatFree(0); + if (boardingPilot && canopy != null && isCanopyClosed()) { + // The first click opens the canopy, the next one climbs in. + if (!level().isClientSide()) { + openCanopy(); + } + return InteractionResult.SUCCESS; + } + if (boardingPilot && modeSwitchCooldown > 0) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + if (!player.startRiding(this)) { + return InteractionResult.PASS; + } + if (seatOf(player) == 0) { + closeCanopy(); + boardNearbyMobs(); + } + } + return InteractionResult.SUCCESS; + } + + // Racks + + /** Loads {@code cargo} into the given rack seat. Server only. */ + private boolean loadCargo(Entity cargo, int seat) { + if (!isSeatFree(seat) || cargo.isPassenger()) { + return false; + } + pendingSeat = seat; + boolean loaded = cargo.startRiding(this, true, true); + pendingSeat = EMPTY; + if (loaded) { + level().playSound(null, getX(), getY(), getZ(), SoundEvents.UI_BUTTON_CLICK, SoundSource.NEUTRAL, 1.0F, 1.0F); + } + return loaded; + } + + /** Whether {@code cargo} may ride rack {@code seat} of this vehicle. */ + public boolean acceptsCargo(AircraftEntity cargo, int seat) { + AircraftDefinition def = definition(); + AircraftDefinition cargoDef = cargo.definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + if (rack == null || cargoDef == null || cargo == this || isPassenger() || cargo.isPassenger()) { + return false; + } + // A vehicle that carries cargo itself cannot be loaded. + for (Entity p : cargo.getPassengers()) { + if (p instanceof AircraftEntity) { + return false; + } + } + if (rack.accepts(cargoDef.name(), cargoDef.kind())) { + return true; + } + for (RideRack rr : cargoDef.rideRacks) { + if (rr.carrier().equalsIgnoreCase(def.name()) && def.seatCount() + rr.rack() - 1 == seat) { + return true; + } + } + return false; + } + + /** Whether rack {@code seat} carries containers, {@code AddRack} naming {@code container}. */ + public boolean acceptsContainer(ContainerEntity container, int seat) { + AircraftDefinition def = definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + return rack != null && !isPassenger() && !container.isPassenger() + && rack.accepts(ContainerEntity.CARGO_NAME, ContainerEntity.CARGO_NAME); + } + + private boolean isNearRackEntry(AircraftEntity cargo, RackDefinition rack) { + Vec3 entry = toWorld(rack.entry()); + float r = rack.range(); + return Math.abs(cargo.getX() - entry.x) <= r && Math.abs(cargo.getY() - entry.y) <= r && Math.abs(cargo.getZ() - entry.z) <= r; + } + + /** Carrier pilot loads every nearby vehicle or container that fits a free rack. Server only. */ + public void loadNearbyCargo() { + AircraftDefinition def = definition(); + if (def == null || def.racks.isEmpty() || throttle > 0.3 || !hasBlockBelow(1, 3, true)) { + return; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (!isSeatFree(seat)) { + continue; + } + RackDefinition rack = def.rack(seat); + Vec3 entry = toWorld(rack.entry()); + AABB area = new AABB(entry, entry).inflate(rack.range()); + boolean loaded = false; + for (AircraftEntity cargo : level().getEntitiesOfClass(AircraftEntity.class, area)) { + if (acceptsCargo(cargo, seat) && loadCargo(cargo, seat)) { + loaded = true; + break; + } + } + if (!loaded) { + for (ContainerEntity container : level().getEntitiesOfClass(ContainerEntity.class, area)) { + if (acceptsContainer(container, seat) && loadCargo(container, seat)) { + break; + } + } + } + } + } + + /** Drops the last loaded piece of cargo at its rack's entry point. Server only. */ + public void unloadCargo() { + AircraftDefinition def = definition(); + if (def == null) { + return; + } + for (int seat = def.seatAndRackCount() - 1; seat >= def.seatCount(); seat--) { + Entity cargo = occupant(seat); + if (cargo == null) { + continue; + } + RackDefinition rack = def.rack(seat); + Vector3 drop = rack.entry(); + if (cargo instanceof AircraftEntity) { + // Aircraft roll out well clear of the carrier's tail or nose. + drop = drop.add(0.0, 0.0, drop.z() >= centreZ(def) ? 12.0 : -12.0); + } + Vec3 at = toWorld(drop); + cargo.stopRiding(); + cargo.snapTo(at.x, at.y, at.z, getYRot() + rack.yaw(), 0.0F); + // As in the original, with no ground in the 20 blocks below it goes down under a canopy. + if (!(cargo instanceof AircraftEntity) && !hasBlockBelow(3, 20, false)) { + ParachuteEntity.open(level(), cargo, ParachuteEntity.Kind.DROP); + } + return; + } + } + + /** Leaves the rack this vehicle rides on, at the rack entry point. Server only. */ + public void leaveRack() { + if (!(getVehicle() instanceof AircraftEntity carrier)) { + return; + } + AircraftDefinition carrierDef = carrier.definition(); + RackDefinition rack = carrierDef != null ? carrierDef.rack(carrier.seatOf(this)) : null; + Vec3 at = rack != null ? carrier.toWorld(rack.entry()) : position(); + stopRiding(); + snapTo(at.x, at.y, at.z, getYRot(), 0.0F); + } + + /** + * Pilot of a vehicle near a free rack of a carrier boards that rack. Server only. + * + * @return whether a rack was found + */ + public boolean boardNearbyRack() { + if (isPassenger()) { + return false; + } + for (AircraftEntity carrier : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(60.0))) { + AircraftDefinition def = carrier.definition(); + if (carrier == this || def == null) { + continue; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (carrier.isSeatFree(seat) && carrier.acceptsCargo(this, seat) && carrier.isNearRackEntry(this, def.rack(seat)) + && carrier.loadCargo(this, seat)) { + return true; + } + } + } + return false; + } + + private static double centreZ(AircraftDefinition def) { + if (def.boundingBoxes.isEmpty()) { + return 0.0; + } + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + for (var box : def.boundingBoxes) { + min = Math.min(min, box.offset().z() - box.width() / 2.0); + max = Math.max(max, box.offset().z() + box.width() / 2.0); + } + return (min + max) / 2.0; + } + + // Ejection + + /** Seats zero and one of vehicles with {@code EnableEjectionSeat} can bail out. */ + public boolean canEject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && def.ejectionSeat && seat >= 0 && seat < 2 && !(seat == 0 && def.uav); + } + + /** + * {@code ejectSeat}: throws the occupant clear of the aircraft in an ejection seat, whose + * parachute opens once it starts to fall. Each ejection takes a parachute from the parachute + * slots of the vehicle screen; without one the seat stays put. Creative players need none. + * Server only. + */ + public void eject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + if (def == null || !canEject(entity)) { + return; + } + boolean creative = entity instanceof Player player && player.getAbilities().instabuild; + if (!creative && !inventory.takeParachute()) { + if (entity instanceof ServerPlayer player) { + ModSounds.playTo(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.no_parachute"), true); + } + return; + } + Vector3 seatPos = def.seats.get(seat).position(); + Vec3 out = toWorld(seatPos.add(0.0, 2.0, 0.0)); + Vector3f push = toWorldOffset(new Vector3(0.0, 2.0, 0.0)); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement().add(push.x + (random.nextFloat() - 0.5) * 0.1, push.y, + push.z + (random.nextFloat() - 0.5) * 0.1)); + entity.needsSync = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.EJECTION_SEAT); + if (canopy != null && !canopy.status()) { + canopy.set(true, false); + modeSwitchCooldown = 40; + } + ModSounds.playAt(entity, "eject_seat", 5.0F, 1.0F); + } + + // Search lights + + public boolean isSearchLightOn() { + AircraftDefinition def = definition(); + return def != null && !def.searchLights.isEmpty() && entityData.get(DATA_SEARCH_LIGHT); + } + + /** {@code canSwitchSearchLight}: the pilot or the first crew seat, in vehicles that have one. */ + public boolean canSwitchSearchLight(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && !def.searchLights.isEmpty() && seat >= 0 && seat <= 1; + } + + /** Server only. */ + public void toggleSearchLight(Entity entity) { + if (canSwitchSearchLight(entity)) { + entityData.set(DATA_SEARCH_LIGHT, !entityData.get(DATA_SEARCH_LIGHT)); + } + } + + /** + * Where a turning search light points: the first crew seat's view, else the pilot's, as + * {@code renderLight} took them. Null with nobody aboard to aim it. + */ + public @Nullable Entity searchLightAimer() { + Entity crew = occupant(1); + return crew != null ? crew : occupant(0); + } + + // Troops: parachute drops and ropes + + public boolean isRepelling() { + return entityData.get(DATA_REPELLING); + } + + /** How far the ropes hang below their hooks, as a negative number. */ + public float ropeLength() { + return ropeLength; + } + + /** Where rope {@code index} hangs from, in the world. */ + public Vec3 ropePosition(int index) { + AircraftDefinition def = definition(); + return def != null && index >= 0 && index < def.repellingHooks.size() ? toWorld(def.repellingHooks.get(index).position()) : position(); + } + + /** + * {@code onUpdate_Repelling} and {@code onUpdate_UnmountCrew}: the ropes run out towards the + * ground, then the mobs aboard slide down them one per hook interval; a parachute drop lets one + * passenger out per {@code MobDropOption} interval while the hatch is open. + */ + private void tickRopes(AircraftDefinition def) { + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + int clear = airBelow(50); + if (ropeLength > -50.0F && ropeLength > -clear) { + ropeLength -= level().isClientSide() ? 0.3F : 0.25F; + } + } else { + ropeLength = 0.0F; + } + } + if (level().isClientSide()) { + return; + } + if (isRepelling() && isDestroyed()) { + entityData.set(DATA_REPELLING, false); + } + if (!isRepelling()) { + repellingTicks = 0; + } else if (repellingTicks < 60) { + repellingTicks++; + } else { + for (int rope = 0; rope < def.repellingHooks.size(); rope++) { + if (ticksAlive % def.repellingHooks.get(rope).interval() != 0) { + continue; + } + for (int seat = 1; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + rappel(mob, rope); + break; + } + } + } + } + if (droppingTroops) { + if (hasBlockBelow(3, 10, false) || isDestroyed()) { + droppingTroops = false; + } else if ((hatch == null || hatch.isOn()) && ticksAlive % def.dropInterval == 0 && !dropNextTroop(def)) { + droppingTroops = false; + } + } + } + + /** Air blocks straight below, up to {@code max}. */ + private int airBelow(int max) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + int i = 0; + while (i < max && pos.getY() - i > level().getMinY() && level().getBlockState(pos.setY(blockPosition().getY() - i - 1)).isAir()) { + i++; + } + return i; + } + + /** + * {@code canStartRepelling}: a helicopter in hover mode, level and holding still. The original + * compared against where it was a few ticks before. + */ + private boolean canStartRepelling() { + return this instanceof HelicopterEntity && isHoveringMode() && Math.abs(getXRot()) < 3.0F && Math.abs(roll()) < 3.0F + && lastTickPos != null && position().distanceTo(lastTickPos) < 0.05; + } + + /** + * The pilot's drop key, {@code unmountCrew}: with ropes, lets them down from a steady hover + * or pulls them up again; with {@code EnableParachuting}, starts or stops dropping the mobs + * aboard under parachutes; otherwise, or close to the ground, simply lets the mobs out. + * Server only. + * + * @return false if nothing could be done, so the pilot hears it + */ + public boolean dropCrew() { + AircraftDefinition def = definition(); + if (def == null || isDestroyed()) { + return false; + } + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + entityData.set(DATA_REPELLING, false); + return true; + } + if (hasBlockBelow(3, 4, false)) { + return unloadMobs(); + } + if (!canStartRepelling()) { + return false; + } + entityData.set(DATA_REPELLING, true); + repellingTicks = 0; + return true; + } + if (droppingTroops) { + droppingTroops = false; + return true; + } + if (def.parachuting && !hasBlockBelow(3, 10, false)) { + droppingTroops = true; + if (hatch != null && hatch.isOff()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } + return true; + } + return unloadMobs(); + } + + /** Lets every mob in a crew seat out, as in {@code unmountCrew(false)}. */ + private boolean unloadMobs() { + AircraftDefinition def = definition(); + boolean any = false; + for (int seat = 1; def != null && seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + mob.stopRiding(); + any = true; + } + } + return any; + } + + /** The next mob behind the second seat leaves at the drop point under a parachute. */ + private boolean dropNextTroop(AircraftDefinition def) { + for (int seat = 2; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + parachuteOut(mob, def); + return true; + } + } + return false; + } + + private void parachuteOut(Entity entity, AircraftDefinition def) { + Vec3 out = toWorld(def.dropPosition); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement()); + entity.needsSync = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.DROP); + } + + /** + * A passenger's own drop key, {@code MCH_ClientSeatTickHandler}: slide down a rope once the + * ropes are out, or jump with a parachute from behind the second seat of a vehicle with + * {@code EnableParachuting} and room below. Server only. + * + * @return false if neither is possible right now + */ + public boolean jumpOut(Player player) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (def == null || seat <= 0 || seat >= def.seatCount() || isDestroyed()) { + return false; + } + if (isRepelling() && repellingTicks > 50 && !def.repellingHooks.isEmpty()) { + lastRope = (lastRope + 1) % def.repellingHooks.size(); + rappel(player, lastRope); + return true; + } + if (def.parachuting && seat >= 2 && !hasBlockBelow(3, 13, false)) { + parachuteOut(player, def); + return true; + } + return false; + } + + /** {@code unmountEntityRepelling}: two blocks under the hook, onto a rope rider that lets them down. */ + private void rappel(Entity entity, int rope) { + Vec3 at = ropePosition(rope).add(0.0, -2.0, 0.0); + entity.stopRiding(); + entity.teleportTo(at.x, at.y, at.z); + entity.setDeltaMovement(Vec3.ZERO); + entity.resetFallDistance(); + RappelEntity rider = new RappelEntity(ModEntities.RAPPEL.get(), level()); + rider.attach(this, rope); + rider.setPos(at); + level().addFreshEntity(rider); + entity.startRiding(rider, true, true); + lastRope = rope; + } + + /** + * {@code mountMobToSeats}: when the pilot climbs in, mobs standing close by take the free crew + * seats, so villagers and the like can be flown somewhere. Server only. + */ + private void boardNearbyMobs() { + AircraftDefinition def = definition(); + if (def == null || def.uav) { + return; + } + for (Mob mob : level().getEntitiesOfClass(Mob.class, getBoundingBox().inflate(3.0, 2.0, 3.0), m -> !m.isPassenger() && !m.isVehicle())) { + for (int seat = 1; seat < def.seatCount(); seat++) { + if (isSeatFree(seat)) { + pendingSeat = seat; + mob.startRiding(this, true, true); + pendingSeat = EMPTY; + break; + } + } + } + } + + // Terrain helpers + + /** True if any solid block is within {@code depth} blocks below the vehicle. */ + protected boolean isNearGround(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(0, -1, 0); + if (!level().getBlockState(pos).getCollisionShape(level(), pos).isEmpty()) { + return true; + } + } + return false; + } + + /** + * Port of {@code MCH_Lib.getBlockY}: scans a {@code size} x {@code size} column from the + * vehicle's block down {@code depth} layers (the first layer being the vehicle's own). + * + * @param solidOnly ignore blocks without collision; fluids still count when this is false + */ + public boolean hasBlockBelow(int size, int depth, boolean solidOnly) { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < depth; y++) { + for (int x = -size / 2; x <= size / 2; x++) { + for (int z = -size / 2; z <= size / 2; z++) { + pos.set(px + x, py - y, pz + z); + BlockState state = level().getBlockState(pos); + if (state.isAir()) { + continue; + } + if (!solidOnly || !state.getCollisionShape(level(), pos).isEmpty() || !state.getFluidState().isEmpty()) { + return true; + } + } + } + } + return false; + } + + /** True if the first non-air block within {@code depth} below is water. */ + protected boolean isWaterBelow(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + BlockState state = level().getBlockState(pos); + if (!state.isAir()) { + return !state.getFluidState().isEmpty(); + } + pos.move(0, -1, 0); + } + return false; + } + + /** {@code getBlockY(this, 3, -2, false)}: standing on something that is neither air nor water. */ + protected boolean isOnSolidGround() { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < 2; y++) { + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + BlockState state = level().getBlockState(pos.set(px + x, py - y, pz + z)); + if (!state.isAir()) { + return state.getFluidState().isEmpty(); + } + } + } + } + return false; + } + + /** + * Port of {@code getWaterDepth}: the share of the hit box that is in water, from 0 to 1, sampled + * in five horizontal slices. {@code FloatOffset} raises the slices, so a hull with an offset rides + * higher. The float physics only pushes up once this reaches 1, which sets the waterline. + * + *

The original drew the model {@value #LEGACY_MODEL_LIFT} above the bottom of the hit box + * (its {@code yOffset}, half the 0.7 default height); this port draws it at the bottom. The + * slices are lowered by the same amount so boats sit in the water as they did in 1.0.3. + */ + protected double waterDepth() { + if (entityData.get(DATA_SINKING)) { + return 0.0; + } + AircraftDefinition def = definition(); + float offset = (def != null ? def.floatOffset : 0.0F) - LEGACY_MODEL_LIFT; + AABB box = getBoundingBox(); + double height = box.maxY - box.minY; + double depth = 0.0; + for (int i = 0; i < 5; i++) { + double bottom = box.minY + height * i / 5.0 - 0.125 + offset; + double top = box.minY + height * (i + 1) / 5.0 - 0.125 + offset; + AABB slice = new AABB(box.minX, bottom, box.minZ, box.maxX, top, box.maxZ); + if (level().getBlockStates(slice).anyMatch(state -> state.getFluidState().is(FluidTags.WATER))) { + depth += 1.0 / 5.0; + } + } + return depth; + } + + /** {@code applyOnGroundPitch}: eases pitch towards {@code OnGroundPitch} and roll towards level. */ + protected void applyOnGroundPitch(float factor) { + AircraftDefinition def = definition(); + if (def != null) { + float ground = def.onGroundPitch; + setXRot((getXRot() - ground) * factor + ground); + } + roll *= factor; + } + + /** {@code StepHeight}: how tall a ledge the vehicle drives up without jumping. */ + @Override + public float maxUpStep() { + AircraftDefinition def = definition(); + return def != null ? def.stepHeight : 0.0F; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith(@Nullable Entity other) { + return true; + } + + @Override + public boolean isPushable() { + return false; + } + + // Weapons + + public @Nullable VehicleWeapons weapons() { + definition(); + return weapons; + } + + public void publishWeaponSelection() { + if (weapons != null && !level().isClientSide()) { + entityData.set(DATA_WEAPON_SELECTION, weapons.encodeSelection()); + } + } + + public void publishWeaponUse(int stat) { + entityData.set(DATA_WEAPON_USE, stat); + } + + public int syncedWeaponUse() { + return entityData.get(DATA_WEAPON_USE); + } + + /** + * Whether {@code set} fires along the user's view instead of the hull: the shot direction and + * the muzzle positions are worked out as if the vehicle faced where the user looks. Static + * mounts do this; see {@link GroundVehicleEntity#aimsWithUser}. + */ + public boolean aimsWithUser(WeaponSet set) { + return false; + } + + /** {@code isInfinityAmmo}: creative players never run dry, nor does anyone when the server config says so. */ + public boolean hasInfiniteAmmo(@Nullable Entity user) { + return ServerConfig.infiniteAmmo() || user instanceof Player player && player.getAbilities().instabuild; + } + + /** Where the pilot looks relative to the hull: the turret's traverse. */ + public float turretYaw() { + return animation.seatYaw(0, 1.0F); + } + + public float stealth() { + AircraftDefinition def = definition(); + return def != null ? def.stealth : 0.0F; + } + + /** The TV missile the crew is flying, if it still exists. */ + public @Nullable TvMissileEntity tvMissile() { + if (level().isClientSide()) { + return level().getEntity(entityData.get(DATA_TV_MISSILE)) instanceof TvMissileEntity m && !m.isRemoved() ? m : null; + } + return tvMissile != null && !tvMissile.isRemoved() ? tvMissile : null; + } + + public void setTvMissile(@Nullable TvMissileEntity missile) { + tvMissile = missile; + entityData.set(DATA_TV_MISSILE, missile != null ? missile.getId() : 0); + } + + /** Whether the flares are burning right now, for the crew's display. */ + public boolean isFlareActive() { + return entityData.get(DATA_FLARE); + } + + /** Someone is locking on to this vehicle; its crew hears the warning. Server only. */ + public void warnLocked() { + if (alertCount == 0 && !isDestroyed() && weapons != null && weapons.flares.isFitted()) { + alertCount = 10; + for (int seat = 0; seat < 2; seat++) { + if (occupant(seat) instanceof ServerPlayer player) { + ModSounds.playTo(player, "locked", 1.0F, 1.0F); + } + } + } + } + + // Hit boxes + + /** + * Clips a ray against the main hit box and every {@code BoundingBox}, remembering the damage + * factor of the box hit first, as the original's composite bounding box did. + */ + public Optional clipHitBoxes(Vec3 from, Vec3 to, double inflate) { + hitBoxFactor = 1.0F; + Optional best = clip(getBoundingBox().inflate(inflate), from, to); + double bestDistance = best.map(from::distanceToSqr).orElse(Double.MAX_VALUE); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + Optional hit = clip(hullBox(box).inflate(inflate), from, to); + if (hit.isPresent() && from.distanceToSqr(hit.get()) < bestDistance) { + best = hit; + bestDistance = from.distanceToSqr(hit.get()); + hitBoxFactor = box.damageFactor(); + } + } + } + return best; + } + + /** + * One {@code BoundingBox} in world space. Like the original's, it stays axis aligned: the + * centre turns with the hull, the box itself does not. + */ + private AABB hullBox(BoundingBoxDefinition box) { + Vec3 c = toWorld(box.offset()); + double hw = box.width() / 2.0; + double hh = box.height() / 2.0; + return new AABB(c.x - hw, c.y - hh, c.z - hw, c.x + hw, c.y + hh, c.z + hw); + } + + /** The whole hull: the main hit box, every {@code BoundingBox} and the generated boxes, in world space. */ + public List hullBoxes() { + AircraftDefinition def = definition(); + if (def == null || def.hull().isEmpty()) { + return List.of(getBoundingBox()); + } + List boxes = new ArrayList<>(def.hull().size() + 1); + boxes.add(getBoundingBox()); + for (BoundingBoxDefinition box : def.hull()) { + boxes.add(hullBox(box)); + } + return boxes; + } + + /** The smallest box around the whole hull. */ + private AABB hullBounds() { + AABB bounds = getBoundingBox(); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + bounds = bounds.minmax(hullBox(box)); + } + } + return bounds; + } + + /** Whether any part of this hull touches any part of {@code other}'s, grown by {@code inflate}. */ + private boolean hullsTouch(AircraftEntity other, double inflate) { + // Hulls generated from big models run to a thousand boxes, so skip ours that are nowhere near. + AABB around = other.hullBounds().inflate(inflate); + List theirs = null; + for (AABB mine : hullBoxes()) { + AABB grown = mine.inflate(inflate); + if (!grown.intersects(around)) { + continue; + } + if (theirs == null) { + theirs = other.hullBoxes(); + } + for (AABB box : theirs) { + if (grown.intersects(box)) { + return true; + } + } + } + return false; + } + + /** + * Vehicles whose hulls block this one: everything nearby except its own carrier or cargo, and + * vehicles either of the two could load onto a rack, which have to drive into each other. + */ + private List solidNeighbours(AABB area) { + return level().getEntitiesOfClass(AircraftEntity.class, area.inflate(HULL_REACH), other -> other != this && !other.isRemoved() + && other.definition() != null && !other.isPassenger() && !isPassengerOfSameVehicle(other) + && other.getVehicle() != this && getVehicle() != other && !canCarry(other) && !other.canCarry(this)); + } + + /** Whether a free rack of this vehicle would take {@code cargo}. */ + private boolean canCarry(AircraftEntity cargo) { + AircraftDefinition def = definition(); + if (def == null) { + return false; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (acceptsCargo(cargo, seat)) { + return true; + } + } + return false; + } + + /** + * Not in the original, where only the main box at a vehicle's feet collided: the whole hull + * blocks other vehicles. A move that would push this hull into another vehicle's is undone, + * sideways first and then completely, so a helicopter can also set down on a parked truck. + * Hulls that already overlapped, say after two vehicles were placed into each other, may move + * apart. + */ + @Override + public void move(MoverType type, Vec3 delta) { + if (type != MoverType.SELF || isPassenger() || definition() == null) { + super.move(type, delta); + return; + } + List near = solidNeighbours(hullBounds().expandTowards(delta)); + if (near.isEmpty()) { + super.move(type, delta); + return; + } + List overlapping = new ArrayList<>(); + for (AircraftEntity other : near) { + if (hullsTouch(other, 0.0)) { + overlapping.add(other); + } + } + Vec3 before = position(); + super.move(type, delta); + if (!entersHull(near, overlapping)) { + return; + } + Vec3 m = getDeltaMovement(); + setPos(before.x, getY(), before.z); + setDeltaMovement(0.0, m.y, 0.0); + if (entersHull(near, overlapping)) { + setPos(before); + setDeltaMovement(Vec3.ZERO); + } + } + + private boolean entersHull(List near, List overlapping) { + for (AircraftEntity other : near) { + if (!overlapping.contains(other) && hullsTouch(other, 0.0)) { + return true; + } + } + return false; + } + + /** A ray that starts inside a box hits it where it starts, as it does for vanilla entity picking. */ + private static Optional clip(AABB box, Vec3 from, Vec3 to) { + return box.contains(from) ? Optional.of(from) : box.clip(from, to); + } + + public float consumeHitBoxFactor() { + float f = hitBoxFactor; + hitBoxFactor = 1.0F; + return f; + } + + // Damage + + public boolean isDestroyed() { + return definition() != null && hp() <= 0; + } + + /** + * Port of the original's {@code attackEntityFrom}. Armour caps a hit at {@code ArmorMaxDamage}, + * scales it by {@code ArmorDamageFactor} and the struck box's factor, then subtracts + * {@code ArmorMinDamage}. A player punching an empty vehicle packs it up instead. + */ + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + float boxFactor = consumeHitBoxFactor(); + if (testFlightStart != null || isRemoved() || isInvulnerableToBase(source) || timeSinceHit > 0) { + return false; + } + if (source.is(DamageTypes.IN_FIRE) || source.is(DamageTypes.CACTUS)) { + return false; + } + boolean lava = source.is(DamageTypes.LAVA); + boolean onFire = source.is(DamageTypes.ON_FIRE); + if (lava) { + damage *= random.nextInt(8) + 2; + timeSinceHit = 2; + } + if (source.is(DamageTypeTags.IS_EXPLOSION)) { + timeSinceHit = 1; + } else if (source.getEntity() != null && hasPassenger(source.getEntity())) { + return false; + } + if (onFire) { + timeSinceHit = 10; + } + + boolean creative = false; + boolean sneaking = false; + boolean packUp = false; + if (source.getEntity() instanceof Player player) { + creative = player.getAbilities().instabuild; + sneaking = player.isShiftKeyDown(); + if (source.is(DamageTypes.PLAYER_ATTACK)) { + packUp = creative || !hasPlayerPassenger(); + } + ModSounds.playAt(this, "hit", 1.0F, 1.0F); + } else { + ModSounds.playAt(this, "helidmg", 1.0F, 0.9F + random.nextFloat() * 0.1F); + } + + if (!isDestroyed()) { + if (!packUp) { + AircraftDefinition def = definition(); + if (def != null && !lava && !onFire) { + damage = Math.min(damage, def.armorMaxDamage); + if (boxFactor <= 1.0F) { + damage *= boxFactor; + } + damage *= def.armorDamageFactor; + damage -= def.armorMinDamage; + if (damage <= 0.0F) { + return false; + } + if (boxFactor > 1.0F) { + damage *= boxFactor; + } + } + damage *= ServerConfig.vehicleDamageFactor(); + entityData.set(DATA_HP, Math.max(hp() - (int) damage, 0)); + } + setHurtDir(-getHurtDir()); + // No markHurt(): it sends the server's motion to every client, which snaps a vehicle + // its pilot is flying back a step and looks like knockback on every hit. + setHurtTime(10); + if (hp() <= 0 || packUp) { + if (!packUp) { + entityData.set(DATA_HP, 0); + destroyVehicle(); + timeSinceHit = 20; + if (isAfloat() || isHeavy() && isNearGround(3)) { + startCookOff(source.getEntity()); + } else if (source.is(DamageTypes.IN_WALL) || source.is(DamageTypes.FLY_INTO_WALL) || source.is(DamageTypes.FALL)) { + crashExplosion(); + } else { + Explosions.explode(level, getX(), getY(), getZ(), + new Explosions.Params(null, source.getEntity(), 2.0F, 2.0F, true, true, 5, false, null)); + } + } else { + // In creative the item only drops when sneaking, as with the original's defaults. + if (!creative || sneaking) { + spawnAtLocation(level, getPickResult() != null ? getPickResult() : ItemStack.EMPTY); + } + discard(); + } + } + } else if (packUp && creative) { + discard(); + } + return true; + } + + private boolean hasPlayerPassenger() { + for (Entity passenger : getPassengers()) { + if (passenger instanceof Player) { + return true; + } + } + return false; + } + + /** {@code destroyAircraft}: the wreck tumbles, burns and disappears after 30 seconds. Server only. */ + private void destroyVehicle() { + AircraftDefinition def = definition(); + despawnCount = def != null && def.targetDrone ? 50 : WRECK_TICKS; + setHovering(false); + if (def != null && def.uav) { + Entity remote = occupant(0); + if (remote != null) { + remote.stopRiding(); + } + } + for (int seat = 0; seat < 2; seat++) { + Entity crew = occupant(seat); + if (crew != null && canEject(crew)) { + eject(crew); + } + } + } + + /** {@code explosionByCrash}: a wreck hitting the ground goes up in a fireball sized by its fuel. */ + private void crashExplosion() { + if (!(level() instanceof ServerLevel level)) { + return; + } + if (isHeavy()) { + startCookOff(null); + return; + } + exploded = true; + AircraftDefinition def = definition(); + float size = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + Vec3 at = def != null && def.fuelTankPosition != null ? toWorld(def.fuelTankPosition) : position(); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, null, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + if (!isInWater()) { + Explosions.crashFireball(level, at.x, at.y, at.z, size); + } + } + + /** + * Airliners, bombers and transports ({@code MaxFuel} of {@value HEAVY_FUEL} or more): like a + * boat, they go up in three blasts rather than one fireball. + */ + private boolean isHeavy() { + AircraftDefinition def = definition(); + return def != null && def.maxFuel >= HEAVY_FUEL; + } + + /** A floating vehicle sitting on the water, such as a patrol boat or a float plane at rest. */ + private boolean isAfloat() { + AircraftDefinition def = definition(); + return def != null && def.floats && waterDepth() > 0.0; + } + + /** + * A boat destroyed on the water, or a heavy aircraft wrecked on the ground, does not go up in + * one crash fireball; its fuel and magazine cook off in three blasts over about five seconds, + * then a hull in the water sinks. Not in the original, which only had the crash explosion. + */ + private void startCookOff(@Nullable Entity cause) { + exploded = true; + cookOffBlasts = 3; + cookOffDelay = 1; + cookOffCause = cause; + } + + private void tickCookOff(ServerLevel level) { + if (cookOffBlasts <= 0 || --cookOffDelay > 0) { + return; + } + AircraftDefinition def = definition(); + int blast = 4 - cookOffBlasts--; + float fuel = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + // The fuel tank first, then whatever the magazine still holds, then the hull goes up. + float size = switch (blast) { + case 1 -> fuel * 0.6F; + case 2 -> Math.max(munitionSize(), fuel * 0.4F); + default -> fuel; + }; + size = Mth.clamp(size, 1.0F, 15.0F); + Vec3 at = cookOffPoint(def); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, cookOffCause, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + // The blasts happen at the waterline, but the fire is above it, so show the full fireball. + Explosions.crashFireball(level, at.x, at.y, at.z, size); + if (cookOffBlasts > 0) { + // Three blasts inside five seconds: about 1.5 to 2.5 seconds apart. + cookOffDelay = 30 + random.nextInt(20); + } else { + // A boat or a flying boat goes under; a heavy aircraft on land stays where it burns. + if (isInWater() || waterDepth() > 0.0) { + entityData.set(DATA_SINKING, true); + } + cookOffCause = null; + } + } + + /** + * A cooked-off wreck going under, in place of the vehicle's own physics: it settles slowly, + * stern first with the bow raised and listing to one side, drifts to a stop and comes to rest + * on the bottom. Out of the water it simply falls. + */ + private void sink() { + Vec3 m = getDeltaMovement(); + double my = isInWater() ? Math.max(m.y - 0.003, -SINK_SPEED) : m.y - 0.04; + Vec3 motion = new Vec3(m.x * 0.9, my, m.z * 0.9); + move(MoverType.SELF, motion); + setDeltaMovement(onGround() ? motion.multiply(0.5, 0.0, 0.5) : motion); + if (!onGround()) { + // Which way it lists only has to be the same on every side, so take it from the id. + float list = (getId() & 1) == 0 ? SINK_LIST : -SINK_LIST; + setXRot(Mth.approach(getXRot(), -SINK_PITCH, 0.12F)); + roll = Mth.approach(roll, list, 0.15F); + } + } + + /** + * How big the ammunition aboard goes up: each weapon's {@code Explosion} scaled by how full its + * magazine and reserve still are. Guns and empty racks add nothing. + */ + private float munitionSize() { + if (weapons == null) { + return 0.0F; + } + float size = 0.0F; + for (WeaponSet set : weapons.sets()) { + int rounds = set.ammo() + set.restAmmo(); + if (rounds > 0 && set.definition().explosion > 0) { + size += set.definition().explosion * Math.min(1.0F, (float) rounds / Math.max(set.maxAmmo(), 1)); + } + } + return size; + } + + /** A random spot on the hull: one of its hit boxes, or the middle of the body. */ + private Vec3 cookOffPoint(@Nullable AircraftDefinition def) { + if (def == null || def.boundingBoxes.isEmpty() || random.nextInt(def.boundingBoxes.size() + 1) == 0) { + return position().add(0.0, def != null ? def.bodyHeight / 2.0 : 0.5, 0.0); + } + return toWorld(def.boundingBoxes.get(random.nextInt(def.boundingBoxes.size())).offset()); + } + + /** Destroyed state each tick: the wreck spins, sets its crew alight and explodes on impact. */ + private void tickDestroyed() { + if (!isDestroyed()) { + return; + } + input.clear(); + throttle *= 0.98; + crashSpin(); + if (!(level() instanceof ServerLevel level)) { + return; + } + tickCookOff(level); + if (ticksAlive % 20 == 0) { + AircraftDefinition def = definition(); + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null && !(seat == 0 && def != null && def.uav)) { + crew.igniteForSeconds(5.0F); + } + } + } + if (despawnCount > 0 && --despawnCount <= 1) { + ejectPassengers(); + discard(); + } + } + + /** + * Not in the original: a shot down plane rolls into a nose-down spiral and a helicopter spins + * with some wobble, until it is close to the ground. Tanks and cars just stop. + */ + private void crashSpin() { + if (!(this instanceof HelicopterEntity) && !(this instanceof PlaneEntity) || isNearGround(3) || isInWater()) { + return; + } + int direction = CrashSpin.direction(getId()); + int ticks = spinTicks++; + if (this instanceof HelicopterEntity) { + setYRot(getYRot() + CrashSpin.helicopterYawRate(ticks, direction)); + setXRot(Mth.approach(getXRot(), CrashSpin.helicopterWobblePitch(ticks, getId()), 2.0F)); + roll = Mth.approach(roll, CrashSpin.helicopterWobbleRoll(ticks, getId()), 3.0F); + } else { + setYRot(getYRot() + CrashSpin.planeYawRate(ticks, direction)); + setXRot(CrashSpin.planePitch(getXRot())); + roll = AircraftRotation.wrap(roll + CrashSpin.planeRollRate(ticks, direction)); + } + } + + /** {@code onUpdate_Particle2}: smoke from a damaged hull, thicker the lower its hit points. */ + private void damageSmoke(AircraftDefinition def) { + float ratio = (float) hp() / def.maxHp; + if (ratio > 0.5F) { + return; + } + int sources = 1 + def.boundingBoxes.size(); + int pick = random.nextInt(sources); + Vec3 at = pick == 0 ? position().add(0.0, def.bodyHeight / 2.0, 0.0) : toWorld(def.boundingBoxes.get(pick - 1).offset()); + if (ratio < random.nextFloat() / 2.0F) { + level().addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, at.x, at.y, at.z, + (random.nextDouble() - 0.5) * 0.3, random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.3); + } + if (isDestroyed()) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, at.x, at.y, at.z, 0.0, 0.07, 0.0); + if (random.nextInt(3) == 0) { + level().addParticle(ParticleTypes.FLAME, at.x + random.nextGaussian() * 0.5, at.y, at.z + random.nextGaussian() * 0.5, 0.0, 0.05, 0.0); + } + wreckFire(def); + } + } + + /** + * Not in the original: the wreck keeps burning until it disappears, with a fire glow that + * lights up the smoke at night and dies down over the last few seconds. Nothing burns under + * water. Client only. + */ + private void wreckFire(AircraftDefinition def) { + wreckTicks++; + if (isInWater()) { + return; + } + float left = Math.clamp((float) (WRECK_TICKS - wreckTicks) / WRECK_FADE_TICKS, 0.15F, 1.0F); + Vec3 centre = def.enginePosition != null ? toWorld(def.enginePosition) : position().add(0.0, def.bodyHeight / 2.0, 0.0); + if (wreckTicks % 8 == 0) { + // Each glow outlives the gap to the next, so the fire never goes dark in between. + float size = Math.clamp(def.bodyWidth * 0.8F, 2.0F, 8.0F) * left; + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size, 24), true, + centre.x, centre.y, centre.z, 0.0, 0.02, 0.0); + } + if (random.nextFloat() < left) { + double spread = def.bodyWidth * 0.4; + level().addParticle(ParticleTypes.FLAME, centre.x + random.nextGaussian() * spread, centre.y, + centre.z + random.nextGaussian() * spread, 0.0, 0.04, 0.0); + } + if (random.nextInt(10) == 0) { + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 20 + random.nextInt(20)), true, centre.x, centre.y, centre.z, + (random.nextDouble() - 0.5) * 0.15, 0.1 + random.nextDouble() * 0.15, (random.nextDouble() - 0.5) * 0.15); + } + } + + /** A vehicle that is destroyed, packed up or killed spills what it carried; one that unloads keeps it. */ + @Override + public void remove(RemovalReason reason) { + if (reason.shouldDestroy() && !level().isClientSide() && !isRemoved()) { + inventory.dropAll(); + } + super.remove(reason); + } + + @Override + protected Item getDropItem() { + AircraftDefinition def = definition(); + return def != null ? ModItems.forKind(def.kind()) : Items.AIR; + } + + @Override + public @Nullable ItemStack getPickResult() { + AircraftDefinition def = definition(); + return def != null ? AircraftItem.stackOf(def) : null; + } + + // Dimensions and syncing + + @Override + public EntityDimensions getDimensions(Pose pose) { + AircraftDefinition def = definition(); + return def != null ? EntityDimensions.scalable(def.bodyWidth, def.bodyHeight) : super.getDimensions(pose); + } + + @Override + public void onSyncedDataUpdated(EntityDataAccessor accessor) { + super.onSyncedDataUpdated(accessor); + if (DATA_TYPE.equals(accessor)) { + refreshDimensions(); + definition(); + } else if (DATA_SEATS.equals(accessor) && level().isClientSide()) { + decodeSeats(entityData.get(DATA_SEATS)); + } else if (DATA_WEAPON_SELECTION.equals(accessor) && level().isClientSide() && weapons() != null) { + weapons.decodeSelection(entityData.get(DATA_WEAPON_SELECTION), ClientAccess.localPlayer()); + } + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putString("Type", typeName()); + output.putString("Texture", texture()); + output.putInt("Hp", hp()); + output.putInt("Fuel", fuel()); + output.putFloat("Roll", roll); + output.putBoolean("Hovering", isHoveringMode()); + output.putBoolean("GunnerMode", isGunnerMode()); + output.putInt("Parts", partBits()); + output.putInt("DespawnCount", despawnCount); + output.putInt("CookOff", cookOffBlasts); + output.putBoolean("Sinking", entityData.get(DATA_SINKING)); + output.putBoolean("SearchLight", entityData.get(DATA_SEARCH_LIGHT)); + if (weapons != null) { + output.putIntArray("WeaponsAmmo", weapons.saveAmmo()); + } + inventory.save(output); + if (station != null && stationReturn != null) { + output.store("Station", UUIDUtil.CODEC, station); + output.store("StationReturn", Vec3.CODEC, stationReturn); + } + ValueOutput.TypedOutputList seats = output.list("Seats", Codec.STRING); + for (int i = 0; i < seatIds.length; i++) { + Entity e = occupant(i); + seats.add(e != null ? e.getUUID().toString() : ""); + } + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + entityData.set(DATA_TYPE, input.getStringOr("Type", "")); + entityData.set(DATA_TEXTURE, input.getStringOr("Texture", "")); + entityData.set(DATA_PARTS, input.getIntOr("Parts", 0)); + AircraftDefinition def = definition(); + entityData.set(DATA_HP, input.getIntOr("Hp", def != null ? def.maxHp : 1)); + // 1.0.0 and the earlier betas had no fuel; their vehicles come back with a full tank. + entityData.set(DATA_FUEL, input.getIntOr("Fuel", def != null ? def.maxFuel : 0)); + roll = input.getFloatOr("Roll", 0.0F); + setHovering(input.getBooleanOr("Hovering", false)); + entityData.set(DATA_GUNNER_MODE, input.getBooleanOr("GunnerMode", false)); + despawnCount = input.getIntOr("DespawnCount", 0); + if (hp() <= 0 && despawnCount <= 0) { + despawnCount = WRECK_TICKS; + } + cookOffBlasts = input.getIntOr("CookOff", 0); + cookOffDelay = cookOffBlasts > 0 ? 20 : 0; + entityData.set(DATA_SINKING, input.getBooleanOr("Sinking", false)); + entityData.set(DATA_SEARCH_LIGHT, input.getBooleanOr("SearchLight", false)); + exploded |= cookOffBlasts > 0 || entityData.get(DATA_SINKING); + input.getIntArray("WeaponsAmmo").ifPresent(ammo -> { + if (weapons != null) { + weapons.loadAmmo(ammo); + } else { + savedAmmo = ammo; + } + }); + inventory.load(input); + station = input.read("Station", UUIDUtil.CODEC).orElse(null); + stationReturn = input.read("StationReturn", Vec3.CODEC).orElse(null); + savedSeats.clear(); + List seats = input.listOrEmpty("Seats", Codec.STRING).stream().toList(); + for (int i = 0; i < seats.size(); i++) { + if (!seats.get(i).isEmpty()) { + try { + savedSeats.put(UUID.fromString(seats.get(i)), i); + } catch (IllegalArgumentException ignored) { + // A hand edited save; that occupant simply takes the first free seat. + } + } + } + for (PartState part : parts) { + part.force(part.status()); + } + refreshDimensions(); + } + + /** Raw pilot input, filled in by the client controller every tick. */ + public static final class ControlInput { + public boolean throttleUp; + public boolean throttleDown; + public boolean left; + public boolean right; + public boolean brake; + /** The pilot is looking around instead of steering; planes and tanks yaw with A/D then. */ + public boolean freeLook; + + public void clear() { + throttleUp = false; + throttleDown = false; + left = false; + right = false; + brake = false; + freeLook = false; + } + + public byte write() { + return (byte) ((throttleUp ? 1 : 0) | (throttleDown ? 2 : 0) | (left ? 4 : 0) | (right ? 8 : 0) + | (brake ? 16 : 0) | (freeLook ? 32 : 0)); + } + + public void read(byte bits) { + throttleUp = (bits & 1) != 0; + throttleDown = (bits & 2) != 0; + left = (bits & 4) != 0; + right = (bits & 8) != 0; + brake = (bits & 16) != 0; + freeLook = (bits & 32) != 0; + } + } +} From 83c3fe2f997729da10db2fdcf0f823ce550dafd0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:00 +0800 Subject: [PATCH 0723/1461] Add entity/AnimationState.java to the 1.21.11 shared sources --- .../mcheli/entity/AnimationState.java | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java new file mode 100644 index 00000000..eda5ed7b --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java @@ -0,0 +1,202 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.RackDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; + +/** + * Purely visual state that every side advances once per tick: wheels, tracks, spinning parts and + * where each crew member is looking relative to the hull. + * + *

Ported from {@code updatePartWheel}, {@code updatePartCrawlerTrack} and + * {@code onUpdate_PartRotation}. Nothing in here affects physics, so it is never synced; each + * client derives it from the synced throttle, input and crew rotations. + */ +public final class AnimationState { + /** Road wheel roll in degrees. */ + public float wheelRoll; + public float prevWheelRoll; + /** Steering from -1 (full right) to 1 (full left). */ + public float steer; + public float prevSteer; + /** Track roller angle per side, index 1 is positive X. */ + public final float[] roller = new float[2]; + public final float[] prevRoller = new float[2]; + /** Track run per side, in link spacings from 0 to 1. */ + public final float[] track = new float[2]; + public final float[] prevTrack = new float[2]; + private final float[] trackSpeed = new float[2]; + public float[] rotatingParts = new float[0]; + public float[] prevRotatingParts = new float[0]; + public float throttle; + public float prevThrottle; + /** Per seat and rack: view yaw and pitch of the occupant relative to the hull. */ + public float[] seatYaw = new float[0]; + public float[] prevSeatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public float[] prevSeatPitch = new float[0]; + + void resize(AircraftDefinition def) { + rotatingParts = new float[def.rotatingParts.size()]; + prevRotatingParts = new float[def.rotatingParts.size()]; + int seats = def.seatAndRackCount(); + seatYaw = new float[seats]; + prevSeatYaw = new float[seats]; + seatPitch = new float[seats]; + prevSeatPitch = new float[seats]; + for (int i = def.seatCount(); i < seats; i++) { + RackDefinition rack = def.rack(i); + seatYaw[i] = prevSeatYaw[i] = rack.yaw(); + seatPitch[i] = prevSeatPitch[i] = rack.pitch(); + } + } + + void tick(AircraftEntity vehicle, AircraftDefinition def) { + AircraftEntity.ControlInput input = vehicle.input(); + float visibleThrottle = vehicle.visibleThrottle(); + float reverse = vehicle.visibleReverse(); + prevThrottle = throttle; + throttle = visibleThrottle; + + tickWheels(def, input, visibleThrottle, reverse); + tickTracks(def, input, visibleThrottle, reverse); + tickRotatingParts(def, vehicle.getControllingPassenger() != null); + tickCrewAim(vehicle, def); + } + + private void tickWheels(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + prevWheelRoll = wheelRoll; + prevSteer = steer; + if (def.enableBack && reverse > 0.01F && throttle <= 0.0) { + throttle = -reverse * 15.0F; + } + if (input.left && !input.right) { + steer = Math.min(steer + 0.1F, 1.0F); + } else if (input.right && !input.left) { + steer = Math.max(steer - 0.1F, -1.0F); + } else { + steer *= 0.9F; + } + wheelRoll += (float) (throttle * def.partWheelRot); + if (wheelRoll >= 360.0F) { + wheelRoll -= 360.0F; + prevWheelRoll -= 360.0F; + } else if (wheelRoll < 0.0F) { + wheelRoll += 360.0F; + prevWheelRoll += 360.0F; + } + } + + private void tickTracks(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + System.arraycopy(roller, 0, prevRoller, 0, 2); + System.arraycopy(track, 0, prevTrack, 0, 2); + double pivot = def.pivotTurnThrottle <= 0.0F ? 1.0 : def.pivotTurnThrottle * 0.1F; + boolean left = input.left; + boolean right = input.right; + int dir = 1; + if (def.enableBack && reverse > 0.0F && throttle <= 0.0) { + throttle = -reverse * 5.0F; + // Reversing swaps which track leads a turn. + if (left != right) { + boolean t = left; + left = right; + right = t; + dir = -1; + } + } + if (left && !right) { + double t = 0.2 * dir; + trackSpeed[0] += (float) t; + trackSpeed[1] -= (float) (pivot * t); + } else if (right && !left) { + double t = 0.2 * dir; + trackSpeed[0] -= (float) (pivot * t); + trackSpeed[1] += (float) t; + } else { + float t = (float) Mth.clamp(throttle, -0.2, 0.2); + trackSpeed[0] += t; + trackSpeed[1] += t; + } + + for (int i = 0; i < 2; i++) { + trackSpeed[i] = Mth.clamp(trackSpeed[i], -0.72F, 0.72F); + roller[i] += trackSpeed[i] * def.trackRollerRot; + if (roller[i] >= 360.0F) { + roller[i] -= 360.0F; + prevRoller[i] -= 360.0F; + } else if (roller[i] < 0.0F) { + roller[i] += 360.0F; + prevRoller[i] += 360.0F; + } + track[i] -= trackSpeed[i]; + while (track[i] >= 1.0F) { + track[i]--; + prevTrack[i]--; + } + while (track[i] < 0.0F) { + track[i]++; + } + while (prevTrack[i] < 0.0F) { + prevTrack[i]++; + } + trackSpeed[i] *= 0.75F; + } + } + + private void tickRotatingParts(AircraftDefinition def, boolean piloted) { + for (int i = 0; i < rotatingParts.length && i < def.rotatingParts.size(); i++) { + RotatingPart part = def.rotatingParts.get(i); + prevRotatingParts[i] = rotatingParts[i]; + if (part.always() || piloted) { + float r = rotatingParts[i] + part.speed(); + if (r < 0.0F) { + r += 360.0F; + prevRotatingParts[i] += 360.0F; + } else if (r >= 360.0F) { + r -= 360.0F; + prevRotatingParts[i] -= 360.0F; + } + rotatingParts[i] = r; + } + } + } + + private void tickCrewAim(AircraftEntity vehicle, AircraftDefinition def) { + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + float hullRoll = vehicle.roll(); + for (int i = 0; i < seatYaw.length && i < def.seatCount(); i++) { + prevSeatYaw[i] = seatYaw[i]; + prevSeatPitch[i] = seatPitch[i]; + Entity crew = vehicle.occupant(i); + if (crew == null) { + continue; + } + float yaw = Mth.wrapDegrees(crew.getYRot() - hullYaw); + double rad = Math.toRadians(yaw); + // The hull tilt along the crew member's view direction, as in the original tank code. + float tilt = (float) (hullPitch * Math.cos(rad) + hullRoll * Math.sin(rad)); + float pitch = crew.getXRot() - tilt; + // Keep the previous value on the same side of the seam so interpolation never spins. + prevSeatYaw[i] = yaw + Mth.wrapDegrees(prevSeatYaw[i] - yaw); + seatYaw[i] = yaw; + seatPitch[i] = pitch; + } + } + + public float seatYaw(int seat, float partialTicks) { + if (seat < 0 || seat >= seatYaw.length) { + return 0.0F; + } + return prevSeatYaw[seat] + (seatYaw[seat] - prevSeatYaw[seat]) * partialTicks; + } + + public float seatPitch(int seat, float partialTicks) { + if (seat < 0 || seat >= seatPitch.length) { + return 0.0F; + } + return prevSeatPitch[seat] + (seatPitch[seat] - prevSeatPitch[seat]) * partialTicks; + } +} From d0edbfda752a9dbb3443523862d41f68c7765191 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:01 +0800 Subject: [PATCH 0724/1461] Add entity/ChainEntity.java to the 1.21.11 shared sources --- .../mcheli/entity/ChainEntity.java | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java new file mode 100644 index 00000000..4aaab44d --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java @@ -0,0 +1,193 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * A tow chain, {@code MCH_EntityChain}: it hangs above the vehicle that pulls and drags the towed + * one along, letting it drift up to the chain's length away and never more than two blocks past + * that. It goes when either end does. + */ +public class ChainEntity extends Entity { + public static final int MIN_LENGTH = 3; + public static final int MAX_LENGTH = 15; + /** Ticks a chain loaded from disk waits for both ends to load before it gives up. */ + private static final int SEARCH_TICKS = 50; + + private static final EntityDataAccessor DATA_TOW = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TOWED = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + + private @Nullable UUID towId; + private @Nullable UUID towedId; + private @Nullable Entity tow; + private @Nullable Entity towed; + private int length = 4; + private int searchTicks = SEARCH_TICKS; + + public ChainEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + /** Chains {@code towed} to {@code tow}, at the distance they are apart now. Server only. */ + public static ChainEntity connect(Level level, Entity towed, Entity tow) { + ChainEntity chain = new ChainEntity(ModEntities.CHAIN.get(), level); + chain.setPos(tow.position().add(towed.position()).scale(0.5)); + chain.length = Mth.clamp((int) towed.distanceTo(tow), MIN_LENGTH, MAX_LENGTH); + chain.attach(towed, tow); + level.addFreshEntity(chain); + ModSounds.playAt(chain.tow, "chain_ct", 1.0F, 1.0F); + return chain; + } + + /** Whether {@code entity} can pull the other end: only vehicles can. */ + public static boolean canTow(Entity entity) { + return entity instanceof AircraftEntity; + } + + /** + * Only a vehicle pulls. Hooked the other way round, a container would hold a helicopter down, + * so the ends swap whichever order they were clicked in (and chains saved that way are fixed + * when they load). + */ + private void attach(Entity towed, Entity tow) { + if (!canTow(tow) && canTow(towed)) { + Entity swap = tow; + tow = towed; + towed = swap; + } + this.towed = towed; + this.tow = tow; + this.towedId = towed.getUUID(); + this.towId = tow.getUUID(); + entityData.set(DATA_TOWED, towed.getId()); + entityData.set(DATA_TOW, tow.getId()); + searchTicks = 0; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_TOW, -1); + builder.define(DATA_TOWED, -1); + } + + /** The end that pulls; on a client, as far as it has loaded. */ + public @Nullable Entity tow() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOW)) : tow; + } + + /** The end that is pulled. */ + public @Nullable Entity towed() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOWED)) : towed; + } + + /** Whether {@code entity} is either end of this chain. */ + public boolean holds(Entity entity) { + return entity == tow() || entity == towed(); + } + + /** Takes the chain off, with the sound of it coming loose. Server only. */ + public void disconnect() { + ModSounds.playAt(this, "chain_ct", 1.0F, 1.0F); + discard(); + } + + /** The chain on {@code entity}, at either end, if there is one within reach. */ + public static @Nullable ChainEntity on(Entity entity) { + for (ChainEntity chain : entity.level().getEntitiesOfClass(ChainEntity.class, entity.getBoundingBox().inflate(25.0))) { + if (chain.holds(entity)) { + return chain; + } + } + return null; + } + + @Override + public void tick() { + super.tick(); + if (!(level() instanceof ServerLevel server)) { + return; + } + if (searchTicks > 0) { + searchTicks--; + Entity foundTowed = towedId != null ? server.getEntity(towedId) : null; + Entity foundTow = towId != null ? server.getEntity(towId) : null; + if (foundTowed != null && foundTow != null) { + attach(foundTowed, foundTow); + } + return; + } + if (tow == null || towed == null || tow.isRemoved() || towed.isRemoved() || tow == towed) { + disconnect(); + return; + } + towed.resetFallDistance(); + setPos(tow.getX(), tow.getY() + 2.0, tow.getZ()); + pull(tow, towed); + } + + /** {@code updateTowingEntityPosRot}: turns the towed end to face along the chain and drags it in. */ + private void pull(Entity tow, Entity towed) { + Vec3 d = towed.position().subtract(tow.position()); + double dist = d.length(); + if (dist <= length) { + return; + } + towed.setYRot((float) Math.toDegrees(Math.atan2(d.z, d.x)) + 90.0F); + double slack = dist - length; + towed.setDeltaMovement(towed.getDeltaMovement().subtract(d.scale(0.001 / slack))); + double max = length + 2.0; + if (dist > max) { + Vec3 at = tow.position().add(d.scale(max / dist)); + towed.setPos(at.x, at.y, at.z); + } + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + towId = input.read("Tow", UUIDUtil.CODEC).orElse(null); + towedId = input.read("Towed", UUIDUtil.CODEC).orElse(null); + length = Mth.clamp(input.getIntOr("Length", 4), MIN_LENGTH, MAX_LENGTH); + searchTicks = SEARCH_TICKS; + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + if (towId != null && towedId != null) { + output.store("Tow", UUIDUtil.CODEC, towId); + output.store("Towed", UUIDUtil.CODEC, towedId); + } + output.putInt("Length", length); + } +} From 24f03824d60ca3e463847c599c52d9ecc45bc408 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:01 +0800 Subject: [PATCH 0725/1461] Add entity/ClientAccess.java to the 1.21.11 shared sources --- .../mcheli/entity/ClientAccess.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java new file mode 100644 index 00000000..28f14851 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * The few client facts common code needs, such as who the local player is. The client installs + * the real supplier at startup; a dedicated server keeps the empty default and never loads a + * client class through here. + */ +public final class ClientAccess { + private static Supplier<@Nullable Entity> localPlayer = () -> null; + private static PackReceiver packs = new PackReceiver() { + @Override + public void onPackList(PackListPacket packet) { + } + + @Override + public void onPackData(PackDataPacket packet) { + } + + @Override + public void onPackSound(PackSoundPacket packet) { + } + }; + + private ClientAccess() { + } + + public static void install(Supplier<@Nullable Entity> player, PackReceiver receiver) { + localPlayer = player; + packs = receiver; + } + + public static @Nullable Entity localPlayer() { + return localPlayer.get(); + } + + /** Where the server's content pack packets go. Called on the client's main thread. */ + public static PackReceiver packs() { + return packs; + } + + /** The client half of the content pack sync. */ + public interface PackReceiver { + void onPackList(PackListPacket packet); + + void onPackData(PackDataPacket packet); + + void onPackSound(PackSoundPacket packet); + } +} From d9ce33aa3af6c9fac35df7d42620586d2dc90574 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:01 +0800 Subject: [PATCH 0726/1461] Take clicks through interactAt and run the interpolation handler in ContainerEntity on 1.21.11 --- .../mcheli/entity/ContainerEntity.java | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java new file mode 100644 index 00000000..c3ee5bc9 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java @@ -0,0 +1,152 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A cargo container, {@code MCH_EntityContainer}: a crate with a large chest's worth of room that + * sits on the ground, floats, can be pushed around, and rides the racks of the vehicles that carry + * {@code container}. Dropped from a rack high enough it comes down under a parachute. + */ +public class ContainerEntity extends Entity { + /** What racks call it in their {@code AddRack} lines. */ + public static final String CARGO_NAME = "container"; + private static final int SLOTS = 54; + /** Punches add up; the original broke it past 40 with 20 per point of damage. */ + private static final int BREAK_DAMAGE = 40; + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private final SimpleContainer items = new SimpleContainer(SLOTS); + private int damageTaken; + + public ContainerEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide() || isPassenger()) { + return; + } + if (damageTaken > 0) { + damageTaken--; + } + Vec3 m = getDeltaMovement(); + double my = m.y; + if (isInWater() || isInLava()) { + // Bobs up to the surface rather than sinking, as the original's boat physics did. + my = (my < 0.0 ? my / 2.0 : my) + (isEyeInFluid(FluidTags.WATER) ? 0.04 : 0.007); + } else { + my -= 0.04; + } + double mx = m.x; + double mz = m.z; + if (onGround()) { + mx *= 0.9; + mz *= 0.9; + } + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + if (player.getItemInHand(hand).getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (player.isSecondaryUseActive()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer serverPlayer) { + serverPlayer.openMenu(new SimpleMenuProvider((id, inventory, p) -> ChestMenu.sixRows(id, inventory, items), + Component.translatable("entity.mcheli.container"))); + } + return InteractionResult.SUCCESS; + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved() || !(source.getEntity() instanceof Player player)) { + // Only players take it apart; explosions and stray rounds leave the cargo alone. + return false; + } + ModSounds.playAt(this, "hit", 1.0F, 1.3F); + damageTaken += (int) (damage * 20.0F); + boolean creative = player.getAbilities().instabuild; + if (creative || damageTaken > BREAK_DAMAGE) { + if (!creative) { + spawnAtLocation(level, ModItems.CONTAINER.get()); + } + Containers.dropContents(level, this, items); + discard(); + } + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean isPushable() { + return !isPassenger(); + } + + @Override + public boolean canBeCollidedWith(@Nullable Entity other) { + return !isPassenger(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.CONTAINER.get()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + items.clearContent(); + ContainerHelper.loadAllItems(input, items.getItems()); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + ContainerHelper.saveAllItems(output, items.getItems(), false); + } +} From be31b7689202c4f27c6b83a6937143198eb44133 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:02 +0800 Subject: [PATCH 0727/1461] Add entity/EngineSoundSource.java to the 1.21.11 shared sources --- .../mcheli/entity/EngineSoundSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java new file mode 100644 index 00000000..6b25a388 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.entity; + +/** + * A vehicle with an engine loop. The client sound player polls these values every tick; the + * definition's {@code SoundVolume} and {@code SoundPitch} are applied on top. + */ +public interface EngineSoundSource { + /** Loudness before the definition's multiplier. Zero silences the loop. */ + float engineVolume(); + + /** Pitch before the definition's multiplier. */ + float enginePitch(); +} From d484710c9c7c0a9f34aed5c03fb5347bcc993622 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:02 +0800 Subject: [PATCH 0728/1461] Take clicks through interactAt and run the interpolation handler in GltdEntity on 1.21.11 --- .../jnhprojects/mcheli/entity/GltdEntity.java | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java new file mode 100644 index 00000000..53ba6b32 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java @@ -0,0 +1,213 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.weapon.CasStrike; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The ground laser target designator, {@code MCH_EntityGLTD}: a tripod a player sits behind. Its + * sight looks up to 70 degrees either side of where it was set up, zooms and has night vision, + * and marking a spot calls in an A-10 for three gun runs over it. + */ +public class GltdEntity extends Entity { + /** How far the sight turns from the tripod's heading, either way, and up or down. */ + public static final float VIEW_LIMIT = 70.0F; + /** The CAS weapon the A-10s fire; the original hard coded it too. */ + private static final String WEAPON = "a10gau8"; + /** Added to the weapon's own {@code Delay} between two calls. */ + private static final int EXTRA_WAIT = 150; + private static final double SIGHT_RANGE = 300.0; + + private static final EntityDataAccessor DATA_WAIT = SynchedEntityData.defineId(GltdEntity.class, EntityDataSerializers.INT); + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private final CasStrike strike = new CasStrike(WEAPON); + private boolean nightVision; + + public GltdEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WAIT, 0); + } + + /** Ticks until the next A-10 can be called; the sight shows it. */ + public int waitTicks() { + return entityData.get(DATA_WAIT); + } + + /** The eye of the sight, a little in front of and above the tripod. */ + public Vec3 sightPosition(float partialTicks) { + float yaw = getYRot(partialTicks) * Mth.DEG_TO_RAD; + Vec3 pos = getPosition(partialTicks); + return new Vec3(pos.x - Mth.sin(yaw) * 0.6, pos.y + 0.7, pos.z + Mth.cos(yaw) * 0.6); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.04, 0.0); + if (onGround()) { + m = m.scale(0.5); + } + move(MoverType.SELF, m); + setDeltaMovement(m.x * 0.99, m.y * 0.95, m.z * 0.99); + + if (waitTicks() > 0) { + entityData.set(DATA_WAIT, waitTicks() - 1); + } + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (def != null && level() instanceof ServerLevel server) { + strike.tick(server, def); + } + Entity viewer = getFirstPassenger(); + if (viewer == null) { + nightVision = false; + } else if (nightVision) { + NightVision.topUp(viewer); + } + } + + /** + * {@code useCurrentWeapon}: marks the block {@code player} looks at through the sight and calls + * the A-10 on it. Server only. + * + * @return whether the strike was called; the sight may be waiting, or see nothing in range + */ + public boolean callStrike(ServerPlayer player) { + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (getFirstPassenger() != player || waitTicks() > 0 || def == null || !(level() instanceof ServerLevel server)) { + return false; + } + Vec3 from = sightPosition(1.0F); + Vec3 to = from.add(player.getLookAngle().scale(SIGHT_RANGE)); + BlockHitResult hit = server.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + strike.start(hit.getLocation(), player.getYRot(), this, player); + entityData.set(DATA_WAIT, def.delay + EXTRA_WAIT); + ModSounds.playAt(server, getX(), getY(), getZ(), "gltd", 0.5F, 1.0F); + if (def.sound != null) { + ModSounds.playTo(player, def.sound, 1.0F, 1.0F); + } + return true; + } + + public void setNightVision(Player player, boolean on) { + if (getFirstPassenger() != player) { + return; + } + nightVision = on; + if (!on) { + NightVision.end(player); + } + } + + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + if (player.isSecondaryUseActive() || isVehicle()) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + player.setYRot(getYRot()); + player.setXRot(0.0F); + player.startRiding(this); + } + return InteractionResult.SUCCESS; + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (!level().isClientSide() && nightVision) { + NightVision.end(passenger); + nightVision = false; + } + } + + /** The viewer sits behind the tripod. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, -0.5).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + /** The original clamped the viewer's head the same way every tick. */ + @Override + public void onPassengerTurned(Entity passenger) { + float yaw = getYRot() + Mth.clamp(Mth.wrapDegrees(passenger.getYRot() - getYRot()), -VIEW_LIMIT, VIEW_LIMIT); + passenger.setYRot(yaw); + passenger.setYHeadRot(yaw); + passenger.setXRot(Mth.clamp(passenger.getXRot(), -VIEW_LIMIT, VIEW_LIMIT)); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved()) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + if (!creative) { + spawnAtLocation(level, ModItems.GLTD.get()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), "hit", 1.0F, 1.0F); + discard(); + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.GLTD.get()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } +} From 14d28f513713290cb154b72a3ce98debaadff952 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:03 +0800 Subject: [PATCH 0729/1461] Add entity/GroundVehicleEntity.java to the 1.21.11 shared sources --- .../mcheli/entity/GroundVehicleEntity.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java new file mode 100644 index 00000000..30290141 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java @@ -0,0 +1,130 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Static guns and launchers, ported from {@code MCH_EntityVehicle}. + * + *

The mount itself does not turn; the gunner looks around and the {@code AddPart} tree turns + * with the view. {@code CanMove} lets W/S push the mount and A/D turn it, for the few that set it. + */ +public class GroundVehicleEntity extends AircraftEntity implements EngineSoundSource { + private static final double MIN_SPEED = 0.07; + private double currentSpeed = MIN_SPEED; + + public GroundVehicleEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable VehicleDefinition lookup(String name) { + return MCHeli.content().vehicle(name); + } + + @Override + public @Nullable VehicleDefinition definition() { + return (VehicleDefinition) super.definition(); + } + + /** + * {@code MCH_EntityVehicle.useCurrentWeapon}: the mount stays put while its {@code AddPart} + * tree turns with the gunner's view, so a weapon fires where the gunner looks. Only weapons + * with both yaw limits set aim like turrets instead. The original checked the first + * {@code AddWeapon} line of that weapon, and so does this. + */ + @Override + public boolean aimsWithUser(WeaponSet set) { + WeaponMount mount = set.mounts().getFirst(); + return mount.minYaw() == 0.0F || mount.maxYaw() == 0.0F; + } + + @Override + protected void simulate() { + VehicleDefinition def = definition(); + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + + if (getControllingPassenger() == null) { + throttle = Math.max(throttle - 0.00125, 0.0); + } else if (def.canMove) { + double x = 0.0; + double z = 0.0; + double yaw = Math.toRadians(getYRot()); + if (input.throttleUp) { + x += Math.sin(yaw); + z += Math.cos(yaw); + } + if (input.throttleDown) { + x -= Math.sin(yaw); + z -= Math.cos(yaw); + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + double d = Math.sqrt(x * x + z * z); + if (d > 1.0E-6) { + mx -= x / d * 0.03; + mz += z / d * 0.03; + } + } + if (isInWater()) { + setXRot(getXRot() * 0.9F); + } + + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public float engineVolume() { + return visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return visibleThrottle() * 0.5F; + } +} From 1c9c516b8fda33c1ee62869a26cc4a00f38df65b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:03 +0800 Subject: [PATCH 0730/1461] Add entity/HelicopterEntity.java to the 1.21.11 shared sources --- .../mcheli/entity/HelicopterEntity.java | 553 ++++++++++++++++++ 1 file changed, 553 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java new file mode 100644 index 00000000..957bf9ef --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java @@ -0,0 +1,553 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A helicopter, ported from {@code MCH_EntityHeli}. + * + *

The flight model is the original one: throttle produces lift that weakens as the aircraft + * tilts, and pitching the nose down converts throttle into forward speed through a cubic curve. + * Hover mode holds full throttle, levels the aircraft and lets W/A/S/D slide it around instead. + * + *

Two deliberate differences from 1.7.10: + *

    + *
  • The original ran roll damping once per rendered frame, so it depended on frame rate. It + * now runs once per tick with constants tuned to match the feel at 60 FPS.
  • + *
  • Blades fold and unfold one after another at the same pace on both sides, from the + * synced fold state, instead of each client stepping its own copy.
  • + *
+ */ +public class HelicopterEntity extends AircraftEntity implements EngineSoundSource { + /** + * Rotation per mouse count. The original used {@code 0.06 * partialTicks} with the partial tick + * low-pass filtered to roughly 0.5. + */ + private static final float LOOK_SCALE = 0.03F; + /** Vanilla turns the view 0.15 degrees per mouse count, so this recovers the raw count. */ + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final float ROLL_DAMPING = 0.06F; + private static final float ROLL_DAMPING_HOVER = 0.1F; + private static final float ROLL_RATE = 1.8F; + private static final double MIN_SPEED = 0.07; + + private static final EntityDataAccessor DATA_FOLD = SynchedEntityData.defineId(HelicopterEntity.class, EntityDataSerializers.BYTE); + // DATA_FOLD values, as the original numbered them. + private static final byte FOLDED = 0; + private static final byte FOLDING = 1; + private static final byte UNFOLDED = 2; + private static final byte UNFOLDING = 3; + /** {@code MCH_Blade}: degrees a blade swings per tick while folding. */ + private static final float FOLD_SPEED = 2.0F; + private static final int FOLD_COOLDOWN = 40; + + private double currentSpeed = MIN_SPEED; + private float rotorAngle; + public float rotorAngleO; + /** Per rotor, per blade: where each blade points while they fold, and whether that rotor is folded. */ + private float[][] blades = new float[0][]; + private float[][] bladesO = new float[0][]; + private boolean[] rotorFolded = new boolean[0]; + private int foldCooldown; + private byte lastFold = -1; + + public HelicopterEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable HelicopterDefinition lookup(String name) { + return MCHeli.content().helicopter(name); + } + + @Override + public @Nullable HelicopterDefinition definition() { + return (HelicopterDefinition) super.definition(); + } + + @Override + public boolean steersWithMouse() { + return true; + } + + @Override + public float engineVolume() { + HelicopterDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return 0.2F + visibleThrottle() * 0.2F; + } + + /** Hovering or slow helicopters drop their gear sooner than ones passing over at speed. */ + @Override + protected float autoLowerGearThrottle() { + HelicopterDefinition def = definition(); + double dx = getX() - xo; + double dy = getY() - yo; + double dz = getZ() - zo; + float slow = def != null ? def.speed / 3.5F : 0.0F; + return dx * dx + dy * dy + dz * dz <= slow ? 0.8F : 0.3F; + } + + public float rotorAngle(float partialTicks) { + float prev = rotorAngleO; + if (rotorAngle - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - rotorAngle < -180.0F) { + prev += 360.0F; + } + return prev + (rotorAngle - prev) * partialTicks; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_FOLD, UNFOLDED); + } + + // Blade folding + + private byte foldState() { + return entityData.get(DATA_FOLD); + } + + private boolean hasFoldingRotors() { + HelicopterDefinition def = definition(); + return def != null && def.foldableBlades && !def.rotors.isEmpty(); + } + + /** {@code isFoldBlades}: folded, or on the way. */ + public boolean isFoldedBlades() { + return hasFoldingRotors() && foldState() == FOLDED; + } + + /** {@code canUseBlades}: the blades are out and none is still swinging into place. */ + public boolean canUseBlades() { + if (foldState() != UNFOLDED) { + return false; + } + for (int r = 0; r < rotorFolded.length; r++) { + if (rotorFolded[r]) { + return false; + } + } + return true; + } + + /** {@code canSwitchFoldBlades}: with the engine stopped and the last switch finished. */ + private boolean canSwitchFoldBlades() { + byte state = foldState(); + return hasFoldingRotors() && visibleThrottle() <= 0.01F && foldCooldown == 0 && (state == UNFOLDED || state == FOLDED); + } + + /** The fold key folds the blades of helicopters that can, and works the hatches otherwise. */ + @Override + public void toggleFold() { + if (canSwitchFoldBlades()) { + entityData.set(DATA_FOLD, foldState() == FOLDED ? UNFOLDING : FOLDING); + foldCooldown = FOLD_COOLDOWN; + return; + } + super.toggleFold(); + } + + /** + * {@code onUpdate_Rotor} with {@code MCH_Rotor.update} and {@code MCH_Blade}: spinning blades + * follow the rotor; folding ones swing one by one onto the tail, each a few degrees further + * than the one before, and swing out again until they meet the spinning rotor's angle. + */ + private void tickBlades(HelicopterDefinition def) { + int rotors = def.rotors.size(); + if (blades.length != rotors) { + blades = new float[rotors][]; + bladesO = new float[rotors][]; + rotorFolded = new boolean[rotors]; + for (int r = 0; r < rotors; r++) { + int count = Math.max(def.rotors.get(r).blades(), 1); + blades[r] = new float[count]; + bladesO[r] = new float[count]; + } + if (foldState() == FOLDED || foldState() == FOLDING) { + forceFold(def); + } + } + byte state = foldState(); + if (state != lastFold) { + lastFold = state; + foldCooldown = FOLD_COOLDOWN; + } else if (foldCooldown > 0) { + foldCooldown--; + } + boolean target = state == FOLDING || state == FOLDED; + boolean settled = true; + for (int r = 0; r < rotors; r++) { + RotorDefinition rotor = def.rotors.get(r); + boolean wanted = target && rotor.foldable(); + boolean done = true; + for (int b = 0; b < blades[r].length; b++) { + float base = b * Math.max(rotor.bladeStep(), 1); + float fold = 5.0F + b * 3.0F; + bladesO[r][b] = blades[r][b]; + if (!rotorFolded[r]) { + if (!wanted) { + blades[r][b] = wrap(rotorAngle + base); + done = false; + } else if (!foldBlade(r, b, fold)) { + done = false; + } + } else if (wanted) { + done = false; + } else if (!unfoldBlade(r, b, base)) { + done = false; + } + } + if (done) { + rotorFolded[r] = wanted; + } + if (rotorFolded[r] != wanted) { + settled = false; + } + } + if (settled && !level().isClientSide()) { + if (state == FOLDING) { + entityData.set(DATA_FOLD, FOLDED); + } else if (state == UNFOLDING) { + entityData.set(DATA_FOLD, UNFOLDED); + } + } + } + + /** {@code MCH_Blade.folding}: towards the nearer side of the tail. */ + private boolean foldBlade(int r, int b, float fold) { + float angle = blades[r][b]; + if (angle > fold && angle < 360.0F - fold) { + blades[r][b] = wrap(angle < 180.0F ? angle - FOLD_SPEED : angle + FOLD_SPEED); + return false; + } + return true; + } + + /** {@code MCH_Blade.unfolding}: back round until the blade meets its place on the turning rotor. */ + private boolean unfoldBlade(int r, int b, float base) { + float target = wrap(rotorAngle + base); + float before = blades[r][b]; + if (target <= 180.0F) { + blades[r][b] = wrap(before + FOLD_SPEED); + if (blades[r][b] >= target && before <= target) { + blades[r][b] = target; + return true; + } + } else { + blades[r][b] = wrap(before - FOLD_SPEED); + if (blades[r][b] <= target && before >= target) { + blades[r][b] = target; + return true; + } + } + return false; + } + + /** {@code forceFold}: a helicopter loaded with its blades folded starts with them folded. */ + private void forceFold(HelicopterDefinition def) { + for (int r = 0; r < blades.length; r++) { + if (!def.rotors.get(r).foldable()) { + continue; + } + for (int b = 0; b < blades[r].length; b++) { + float fold = 5.0F + b * 3.0F; + float angle = wrap(rotorAngle + b * Math.max(def.rotors.get(r).bladeStep(), 1)); + if (angle > fold && angle < 360.0F - fold) { + angle = angle < 180.0F ? fold : 360.0F - fold; + } + blades[r][b] = angle; + bladesO[r][b] = angle; + } + rotorFolded[r] = true; + } + } + + private static float wrap(float degrees) { + float d = degrees % 360.0F; + return d < 0.0F ? d + 360.0F : d; + } + + /** Where blade {@code blade} of rotor {@code rotor} points, for drawing. */ + public float bladeAngle(int rotor, int blade, int step, float partialTicks) { + if (rotor >= blades.length || blade >= blades[rotor].length) { + return rotorAngle(partialTicks) + blade * step; + } + float prev = bladesO[rotor][blade]; + float now = blades[rotor][blade]; + if (now - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - now < -180.0F) { + prev += 360.0F; + } + return prev + (now - prev) * partialTicks; + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + super.addAdditionalSaveData(output); + output.putBoolean("FoldBlade", foldState() == FOLDED || foldState() == FOLDING); + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + super.readAdditionalSaveData(input); + entityData.set(DATA_FOLD, input.getBooleanOr("FoldBlade", false) ? FOLDED : UNFOLDED); + } + + /** Pilot toggles hover mode. Only allowed while fairly level and not in gunner mode, as in the original. */ + public void toggleHovering() { + if (isHoveringMode()) { + setHovering(false); + } else if (!isGunnerMode() && isLevelEnough() && canUseBlades()) { + setHovering(true); + } + } + + private boolean isLevelEnough() { + return Math.abs(Mth.wrapDegrees(roll())) < 40.0F && Math.abs(Mth.wrapDegrees(getXRot())) < 40.0F; + } + + @Override + public boolean canSwitchGunnerMode() { + return super.canSwitchGunnerMode() && isLevelEnough() && canUseBlades(); + } + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + HelicopterDefinition def = definition(); + if (def == null) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + + float yaw = 0.0F; + float pitch = 0.0F; + float rollDelta = 0.0F; + if (!isNearGround(2)) { + yaw = clamp(x, 40.0F * def.mobilityYaw) * LOOK_SCALE; + } + // Standing on its skids or wheels a helicopter stays level; the mouse only tilts it in the air. + if (!isHovering() && !onGround()) { + pitch = clamp(y, 40.0F * def.mobilityPitch) * LOOK_SCALE; + rollDelta = clamp(x, 40.0F * def.mobilityRoll) * LOOK_SCALE; + } + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + // Keep yaw continuous so the pilot's own yaw never jumps across the +-180 seam. + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + @Override + protected void simulate() { + HelicopterDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + + updateThrottle(def, piloted); + updateAttitude(def); + updateMotion(def); + } + + /** {@code onUpdate_Control} and the hover/non-hover branches. */ + private void updateThrottle(HelicopterDefinition def, boolean piloted) { + if (piloted) { + Vec3 motion = getDeltaMovement(); + if (!canUseBlades()) { + // Folded blades give no lift; on the ground A/D taxi the helicopter round slowly. + throttle = Math.max(0.0, throttle - 0.02); + if (onGround() && isFoldedBlades() && !isDestroyed()) { + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + } + return; + } + if (!isHovering()) { + if (input.throttleUp) { + throttle = Math.min(1.0, throttle + 0.02 * def.throttleUpDown); + } else if (input.throttleDown) { + throttle = Math.max(0.0, throttle - (1.0 / 70.0) * def.throttleUpDown); + } + motion = strafe(motion, false, input.left, input.right, 0.02 * def.speed); + } else { + throttle = Math.min(1.0, throttle + 1.0 / 30.0); + motion = strafe(motion, input.throttleUp, input.left, input.right, 0.01 * def.speed); + if (input.throttleDown) { + motion = push(motion, getYRot() - 180.0F, 0.01 * def.speed); + } + } + setDeltaMovement(motion); + } else { + throttle = Math.max(0.0, throttle - 0.00125); + if (isHoveringMode() && !level().isClientSide()) { + setHovering(false); + } + } + } + + private Vec3 strafe(Vec3 motion, boolean forward, boolean left, boolean right, double amount) { + if (forward) { + motion = push(motion, getYRot(), amount); + } + if (left && !right) { + motion = push(motion, getYRot() - 90.0F, amount); + } else if (right && !left) { + motion = push(motion, getYRot() + 90.0F, amount); + } + return motion; + } + + /** Adds {@code amount} of horizontal motion towards {@code yaw}, Minecraft style (0 = south). */ + private static Vec3 push(Vec3 motion, float yaw, double amount) { + double rad = Math.toRadians(yaw); + return motion.add(-Math.sin(rad) * amount, 0.0, Math.cos(rad) * amount); + } + + /** Per-tick version of {@code onUpdateAngles}: roll levels out, A/D bank in the air. */ + private void updateAttitude(HelicopterDefinition def) { + float damping = 1.0F - (isHovering() ? ROLL_DAMPING_HOVER : ROLL_DAMPING); + if (Math.abs(roll) > 0.1F && Math.abs(roll) < 65.0F) { + roll *= damping; + } + if (getControllingPassenger() != null && !isNearGround(3)) { + if (input.left && !input.right) { + roll -= ROLL_RATE; + } else if (input.right && !input.left) { + roll += ROLL_RATE; + } + } + if (isHovering() && Math.abs(getXRot()) < 70.0F) { + setXRot(getXRot() * 0.95F); + } + if (def.limitRotation) { + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + /** {@code onUpdate_Server}: gravity, lift, forward thrust, speed limit and ground friction. */ + private void updateMotion(HelicopterDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + float groundPitch = def.onGroundPitch; + + if (!isHovering()) { + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + double yawRad = Math.toRadians(getYRot()); + float pitch = getXRot(); + if (isNearGround(3)) { + pitch -= groundPitch; + } + double thrust = 0.1 * currentSpeed * (pitch * pitch * pitch / 30000.0F) * throttle; + mx += Math.sin(yawRad) * -thrust; + mz += Math.cos(yawRad) * thrust; + + double tilt = (Math.abs(getXRot()) + Math.abs(roll)) * 0.6; + double lift = tilt <= 50.0 ? 1.0 - tilt / 50.0 : 0.0; + my += (lift * 0.025 + 0.03) * throttle; + } else { + levelOut(groundPitch); + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my *= 0.7; + } + my += 0.007; + } + } + } else { + if (random.nextInt(50) == 0) { + mx += (random.nextDouble() - 0.5) / 30.0; + } + if (random.nextInt(50) == 0) { + my += (random.nextDouble() - 0.5) / 50.0; + } + if (random.nextInt(50) == 0) { + mz += (random.nextDouble() - 0.5) / 30.0; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + double limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + levelOut(groundPitch); + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + private void levelOut(float groundPitch) { + if (Math.abs(getXRot()) < 40.0F) { + setXRot((getXRot() - groundPitch) * 0.9F + groundPitch); + } + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + } + + @Override + protected void tickAnimation() { + HelicopterDefinition def = definition(); + rotorAngleO = rotorAngle; + float spool = 1.0F - visibleThrottle(); + rotorAngle = (rotorAngle + (1.0F - spool * spool * spool) * def.rotorSpeed) % 360.0F; + tickBlades(def); + } +} From 8b509c82b342f968b482b0183dc9cd1291cef923 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:04 +0800 Subject: [PATCH 0731/1461] Add entity/NightVision.java to the 1.21.11 shared sources --- .../mcheli/entity/NightVision.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java new file mode 100644 index 00000000..f52c4227 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; + +/** + * Night vision through a sight, {@code MCH_Camera.updateViewer}: a short effect kept topped up while + * the viewer has it switched on, and taken away again when they switch it off or leave. + */ +public final class NightVision { + private static final int TICKS = 300; + + private NightVision() { + } + + /** Called every tick for each viewer. It never runs below ten seconds, where vanilla starts to flicker the view. */ + public static void topUp(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current == null || current.getDuration() < TICKS - 40) { + living.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, TICKS, 0, true, false, false)); + } + } + } + + /** Removes ours, leaving a longer one from a potion alone. */ + public static void end(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current != null && current.getDuration() <= TICKS) { + living.removeEffect(MobEffects.NIGHT_VISION); + } + } + } +} From bf4664f3e1015c0c15b870fee0283e2c41cdcbed Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:04 +0800 Subject: [PATCH 0732/1461] Add entity/ParachuteEntity.java to the 1.21.11 shared sources --- .../mcheli/entity/ParachuteEntity.java | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java new file mode 100644 index 00000000..f7a7db39 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java @@ -0,0 +1,188 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A parachute with someone hanging under it, {@code MCH_EntityParachute}. It sinks at about two + * blocks a second, the rider steers it with the forward key, and it folds away a moment after it + * touches the ground or the water. + */ +public class ParachuteEntity extends Entity { + /** What the parachute looks like and how it opens, the original's model type. */ + public enum Kind { + /** From the parachute item: open from the start. */ + ITEM, + /** An ejection seat: the rider falls in the seat and the canopy opens once it drops. */ + EJECTION_SEAT, + /** Troops dropped from a rack or a cargo door: open from the start, no seat. */ + DROP + } + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(ParachuteEntity.class, EntityDataSerializers.INT); + private static final double MAX_SPEED = 0.35; + private static final double MIN_STEER = 0.07; + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private double steer = MIN_STEER; + private int onGroundCount; + /** Who boards on the next server tick; boarding in the same tick as the spawn can be lost. */ + private @Nullable Entity user; + + public ParachuteEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Opens a parachute above {@code user}, carrying over their speed, and puts them under it. + * Server only. + */ + public static ParachuteEntity open(Level level, Entity user, Kind kind) { + ParachuteEntity parachute = new ParachuteEntity(ModEntities.PARACHUTE.get(), level); + double lift = kind == Kind.ITEM ? 3.5 : 0.0; + parachute.setPos(user.getX(), user.getY() + lift, user.getZ()); + parachute.setYRot(user.getYRot()); + parachute.setDeltaMovement(user.getDeltaMovement()); + parachute.entityData.set(DATA_KIND, kind.ordinal()); + parachute.user = user; + user.resetFallDistance(); + level.addFreshEntity(parachute); + return parachute; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, Kind.ITEM.ordinal()); + } + + public Kind kind() { + Kind[] kinds = Kind.values(); + int i = entityData.get(DATA_KIND); + return i >= 0 && i < kinds.length ? kinds[i] : Kind.ITEM; + } + + /** An ejection seat only opens its canopy once it has started to fall. */ + public boolean isOpen() { + return kind() != Kind.EJECTION_SEAT || getDeltaMovement().y < -0.1; + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide()) { + return; + } + if (user != null) { + if (!user.isPassenger() && user.isAlive()) { + user.startRiding(this, true, true); + setYRot(user.getYRot()); + } + user = null; + } + Entity rider = getFirstPassenger(); + if (rider == null && tickCount > 20 || isInWater()) { + ejectPassengers(); + discard(); + return; + } + if (rider != null) { + rider.resetFallDistance(); + } + simulate(rider); + } + + /** {@code onUpdateServer}. */ + private void simulate(@Nullable Entity rider) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevSpeed = Math.sqrt(mx * mx + mz * mz); + boolean seatLaunch = kind() == Kind.EJECTION_SEAT && tickCount < 20; + my -= onGround() || seatLaunch ? 0.01 : 0.03; + + if (isOpen()) { + if (rider instanceof ServerPlayer player && player.getLastClientInput().forward()) { + double yaw = Math.toRadians(rider.getYRot()); + mx += -Math.sin(yaw) * steer * 0.05; + mz += Math.cos(yaw) * steer * 0.05; + } + double speed = Math.sqrt(mx * mx + mz * mz); + if (speed > MAX_SPEED) { + mx *= MAX_SPEED / speed; + mz *= MAX_SPEED / speed; + speed = MAX_SPEED; + } + // Holding course builds up speed; letting go settles back to a slow drift. + if (speed > prevSpeed) { + steer = Math.min(steer + (MAX_SPEED - steer) / 35.0, MAX_SPEED); + } else { + steer = Math.max(steer - (steer - MIN_STEER) / 35.0, MIN_STEER); + } + } + + if (onGround() && ++onGroundCount > 5) { + ejectPassengers(); + discard(); + return; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + my *= seatLaunch ? 0.95 : 0.9; + double drag = isOpen() ? 0.95 : 0.99; + setDeltaMovement(mx * drag, my, mz * drag); + if (rider != null) { + setYRot(rider.getYRot()); + } + } + + /** + * The server moves the parachute; clients ease towards each position it sends over three + * ticks, as for the vehicles. Without this a slow descent steps from one update to the next. + */ + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + entityData.set(DATA_KIND, input.getIntOr("Kind", Kind.ITEM.ordinal())); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putInt("Kind", kind().ordinal()); + } +} From 9e597c4ca4d5aa96d4e3325b9b96a2cd8cfb609f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:05 +0800 Subject: [PATCH 0733/1461] Add entity/PartState.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/entity/PartState.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/PartState.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/PartState.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/PartState.java new file mode 100644 index 00000000..54755d56 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/PartState.java @@ -0,0 +1,122 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; + +/** + * Open/closed state of a group of moving parts (canopy, landing gear, hatches, wings, VTOL nozzles). + * Ported from {@code MCH_Parts}. + * + *

The target state is one bit of a synched integer, so only the server changes it and every + * side sees the same value. The angle runs from 0 to {@link #max} at {@link #speed} degrees per tick + * and is advanced locally on every side, which keeps the animation smooth without sending angles. + */ +public final class PartState { + private final AircraftEntity owner; + private final int bit; + private final float max; + private final float speed; + private final Sounds sounds; + + private float angle; + private float prevAngle; + + PartState(AircraftEntity owner, int bit, float speed, Sounds sounds) { + this.owner = owner; + this.bit = bit; + this.max = 90.0F; + this.speed = speed; + this.sounds = sounds; + } + + /** Target state: {@code true} means open, folded, retracted or tilted, depending on the part. */ + public boolean status() { + return (owner.partBits() & (1 << bit)) != 0; + } + + /** Fully back in the default position. */ + public boolean isOff() { + return !status() && angle <= 0.02F; + } + + /** Fully in the switched position. */ + public boolean isOn() { + return status() && angle >= max - 0.02F; + } + + public float angle() { + return angle; + } + + public float angle(float partialTicks) { + return prevAngle + (angle - prevAngle) * partialTicks; + } + + /** Progress from 0 to 1. */ + public float factor() { + return angle / max; + } + + public float factor(float partialTicks) { + return angle(partialTicks) / max; + } + + /** Changes the target state. Server only; clients follow through synched data. */ + void set(boolean on, boolean playSound) { + if (owner.level().isClientSide() || status() == on) { + return; + } + owner.setPartBit(bit, on); + if (playSound) { + play(sounds.switching); + play(on ? sounds.startOn : sounds.startOff); + } + tick(); + } + + /** Jumps straight to {@code on} without animating, used for parts that start deployed. */ + void force(boolean on) { + if (!owner.level().isClientSide()) { + owner.setPartBit(bit, on); + } + angle = prevAngle = on ? max : 0.0F; + } + + void tick() { + prevAngle = angle; + if (status()) { + if (angle < max) { + angle = Math.min(angle + speed, max); + if (angle >= max) { + play(sounds.endOn); + } + } + } else if (angle > 0.0F) { + angle = Math.max(angle - speed, 0.0F); + if (angle <= 0.0F) { + play(sounds.endOff); + } + } + } + + private void play(Cue cue) { + if (cue != null && !owner.level().isClientSide()) { + ModSounds.playAt(owner, cue.sound(), 1.0F, cue.pitch()); + } + } + + /** One sound from {@code sounds.json} with its pitch. */ + record Cue(String sound, float pitch) { + } + + /** What plays when the part starts and finishes moving each way; {@code null} entries stay silent. */ + record Sounds(Cue startOn, Cue endOn, Cue startOff, Cue endOff, Cue switching) { + private static final Cue CLICK = new Cue("plane_cc", 1.0F); + private static final Cue CLICK_LOW = new Cue("plane_cc", 0.5F); + + static final Sounds HATCH = new Sounds(null, CLICK, null, CLICK, new Cue("plane_cv", 0.5F)); + static final Sounds CANOPY = new Sounds(null, CLICK, null, CLICK, null); + static final Sounds LANDING_GEAR = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.75F)); + static final Sounds NOZZLE = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.5F)); + static final Sounds WING = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, null); + } +} From ab8e269f8fb15b7c8d23b9bc1779d1d2caab21d3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:05 +0800 Subject: [PATCH 0734/1461] Add entity/PlaneEntity.java to the 1.21.11 shared sources --- .../mcheli/entity/PlaneEntity.java | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java new file mode 100644 index 00000000..b80171b8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java @@ -0,0 +1,516 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A fixed wing aircraft, ported from {@code MCP_EntityPlane}. + * + *

Throttle pushes the plane along its nose, tilted ten degrees up, and the vertical part of + * that push is what keeps it in the air: below full throttle it sinks. VTOL capable planes tilt + * their nozzles (and tilt rotors) towards straight down, which turns the push into lift. + * + *

The original applied its attitude rules once per rendered frame with the partial tick + * filtered to about 0.5. Here they run three times per tick with that value, which is what a + * 60 FPS client did, so the handling is the same without depending on the frame rate. + */ +public class PlaneEntity extends AircraftEntity implements EngineSoundSource { + private static final float LOOK_SCALE = 0.03F; + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private @Nullable PartState nozzle; + private @Nullable PartState wing; + + private double currentSpeed = MIN_SPEED; + private float keyRoll; + private float rotorAngle; + public float rotorAngleO; + private float soundLevel; + + public PlaneEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable PlaneDefinition lookup(String name) { + return MCHeli.content().plane(name); + } + + @Override + public @Nullable PlaneDefinition definition() { + return (PlaneDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + PlaneDefinition def = (PlaneDefinition) definition; + nozzle = def.hasNozzleState() ? addPart(PART_NOZZLE, 1.5F, PartState.Sounds.NOZZLE) : null; + wing = def.wings.isEmpty() ? null : addPart(PART_WING, 2.5F, PartState.Sounds.WING); + if (nozzle != null && def.defaultVtol && !nozzle.status()) { + nozzle.force(true); + } + } + + @Override + public boolean steersWithMouse() { + return true; + } + + public @Nullable PartState nozzle() { + return nozzle; + } + + public @Nullable PartState wing() { + return wing; + } + + public float nozzleAngle() { + return nozzle != null ? nozzle.angle() : 0.0F; + } + + @Override + public int vtolMode() { + if (nozzle == null) { + return 0; + } + if (!nozzle.status()) { + return nozzle.angle() <= 0.005F ? 0 : 1; + } + return nozzle.angle() >= 89.995F ? 2 : 1; + } + + public float rotorAngle(float partialTicks) { + return rotorAngleO + (rotorAngle - rotorAngleO) * partialTicks; + } + + // Pilot actions, server side + + public boolean canSwitchVtol() { + PlaneDefinition def = definition(); + if (def == null || !def.vtol || nozzle == null || modeSwitchCooldown > 0 || vtolMode() == 1) { + return false; + } + return Math.abs(roll) <= 30.0F && !(onGround() && def.defaultVtol); + } + + public void toggleVtol() { + if (canSwitchVtol()) { + modeSwitchCooldown = 20; + setVtol(!nozzle.status()); + } + } + + private void setVtol(boolean on) { + PlaneDefinition def = definition(); + if (nozzle == null || def == null || (def.defaultVtol && onGround() && !on)) { + return; + } + nozzle.set(on, true); + } + + public boolean canFoldWing() { + PlaneDefinition def = definition(); + if (wing == null || def == null || modeSwitchCooldown > 0) { + return false; + } + if (def.variableSweepWing) { + // Sweep back only when fast and well clear of the ground, spread again only when slow. + if (!onGround() && !hasBlockBelow(3, 20, true)) { + if (throttle < 0.7F) { + return false; + } + } else if (throttle > 0.1F) { + return false; + } + } else { + if (!onGround() && !hasBlockBelow(3, 3, true)) { + return false; + } + if (throttle > 0.01F) { + return false; + } + } + return wing.isOff(); + } + + public boolean canUnfoldWing() { + return wing != null && modeSwitchCooldown <= 0 && wing.isOn(); + } + + @Override + public void toggleFold() { + PlaneDefinition def = definition(); + if (def == null) { + return; + } + if (!def.hatches.isEmpty()) { + super.toggleFold(); + } else if (canFoldWing()) { + wing.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldWing()) { + wing.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Folded wings ground the plane; swept wings only below 20% throttle. */ + private boolean canUseWing(PlaneDefinition def) { + if (wing == null) { + return true; + } + if (def.variableSweepWing) { + return throttle >= 0.2 || wing.isOff(); + } + return wing.isOff(); + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void updatePartsServer(AircraftDefinition definition) { + super.updatePartsServer(definition); + PlaneDefinition def = (PlaneDefinition) definition; + // Swept wings spread out again when the plane slows down or gets close to the ground. + if (wing != null && def.variableSweepWing && wing.isOn() && throttle >= 0.2F + && (throttle < 0.5 || hasBlockBelow(1, 10, true))) { + wing.set(false, true); + } + if (nozzle != null && def.defaultVtol && onGround() && vtolMode() == 0) { + nozzle.set(true, true); + } + } + + // Mouse + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + PlaneDefinition def = definition(); + if (def == null || ticksAlive() < 30 || isPassenger() || hasBlockBelow(3, 2, true)) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + boolean vtol = vtolMode() > 0; + float yawFactor = (vtol ? def.vtolYaw : 1.0F) * 0.8F; + float pitchFactor = (vtol ? def.vtolPitch : 1.0F) * 0.8F; + float rollInput = vtolMode() == 0 ? x * 0.5F : x; + + float yaw = clamp(x, 40.0F * def.mobilityYaw) * yawFactor * LOOK_SCALE; + float pitch = clamp(y, 40.0F * def.mobilityPitch) * pitchFactor * LOOK_SCALE; + float rollDelta = clamp(rollInput, 40.0F * def.mobilityRoll) * yawFactor * LOOK_SCALE; + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + // Simulation + + @Override + protected void simulate() { + PlaneDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + if (def.targetDrone && !piloted) { + input.throttleUp = true; + } + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + } + updateMotion(def); + } + + /** + * Gunner mode needs a plane in level flight with throttle up and nothing solid within five + * blocks below, so the autopilot has room to circle. + */ + @Override + public boolean canSwitchGunnerMode() { + if (!super.canSwitchGunnerMode()) { + return false; + } + float r = Math.abs(Mth.wrapDegrees(roll())); + float p = Math.abs(Mth.wrapDegrees(getXRot())); + return r <= 40.0F && p <= 40.0F && visibleThrottle() > 0.6F && !hasBlockBelow(3, 5, false); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlNotHovering}. */ + private void updateThrottle(PlaneDefinition def, boolean piloted) { + reverse *= 0.8F; + boolean canFly = piloted && isCanopyClosed() && canUseWing(def); + if (canFly && isGunnerMode()) { + // The autopilot holds the throttle the pilot left it at. + return; + } + if (canFly || def.targetDrone) { + float upDown = def.throttleUpDown; + boolean turning = input.left != input.right; + boolean up = input.throttleUp; + if (turning && throttle < def.pivotTurnThrottle && !up && !input.throttleDown) { + up = true; + upDown *= 2.0F; + } + if (up) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * def.throttleUpDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}. In gunner mode the autopilot levels out and circles by {@code AutoPilotRot}. */ + private void updateAttitude(PlaneDefinition def, float pt) { + boolean gunner = isGunnerMode(); + if (gunner) { + setXRot(getXRot() * 0.95F); + setYRot(getYRot() + def.autoPilotRot * 0.2F); + if (Math.abs(roll) > 20.0F) { + roll *= 0.95F; + } + } + boolean flying = !hasBlockBelow(3, 3, true); + if (!flying || input.freeLook || gunner || (def.floats && waterDepth() > 0.0)) { + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.6F * yawRate * pt); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.6F * yawRate * pt); + } + } else { + // A/D bank the plane; in VTOL mode the keys do nothing in the air. + float rate = vtolMode() != 0 ? 0.0F : 0.2F; + if (input.left && !input.right) { + keyRoll -= rate * pt; + } else if (input.right && !input.left) { + keyRoll += rate * pt; + } + roll += keyRoll * 0.5F * def.mobilityRoll; + } + keyRoll *= 1.0F - 0.1F * pt; + if (!flying && Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.97F); + } + if (nozzleAngle() > 0.001F) { + setXRot(getXRot() * (1.0F - 0.03F * pt)); + roll *= 1.0F - 0.1F * pt; + } + roll = AircraftRotation.wrap(roll); + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), def.minRotationPitch, def.maxRotationPitch)); + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + private float maxSpeed(PlaneDefinition def) { + float extra = 0.0F; + if (def.variableSweepWing) { + if (wing != null) { + extra = (def.sweepWingSpeed - def.speed) * wing.factor(); + } else if (hatch != null) { + extra = (def.sweepWingSpeed - def.speed) * hatch.factor(); + } + } + return def.speed + extra; + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(PlaneDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + // The gunner mode autopilot holds its altitude. + boolean levelOff = isGunnerMode(); + + if (depth == 0.0) { + if (def.targetDrone) { + levelOff = targetDroneAutopilot(def) || levelOff; + } + if (!levelOff) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else { + my *= 0.8; + } + } else { + setXRot(getXRot() * 0.8F); + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + } + + double push = throttle / 10.0; + float nozzleAngle = nozzleAngle(); + Vec3 v; + if (nozzleAngle > 0.001F) { + setXRot(getXRot() * 0.95F); + v = direction(getYRot(), getXRot() - nozzleAngle); + if (nozzleAngle >= 90.0F) { + v = new Vec3(v.x * 0.8, v.y, v.z * 0.8); + } + } else { + v = direction(getYRot(), getXRot() - 10.0F); + } + if (!levelOff) { + my += v.y * push / (nozzleAngle <= 0.01F ? 2.0 : 8.0); + } + + boolean canMove = def.canMoveOnGround || !isOnSolidGround(); + if (canMove) { + if (def.enableBack && reverse > 0.0F) { + mx -= v.x * reverse; + mz -= v.z * reverse; + } else { + mx += v.x * push; + mz += v.z * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = maxSpeed(def); + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + if (Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.8F); + } + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** Target drones circle at a safe height on their own. Returns whether to hold altitude. */ + private boolean targetDroneAutopilot(PlaneDefinition def) { + if (hasBlockBelow(3, 40, true)) { + if (!hasBlockBelow(3, 5, true)) { + setYRot(getYRot() + def.autoPilotRot * 2.0F); + if (getXRot() > -20.0F) { + setXRot(getXRot() - 0.5F); + } + } + return false; + } + setYRot(getYRot() + def.autoPilotRot); + setXRot(getXRot() * 0.95F); + if (!level().isClientSide() && canRaiseLandingGear() && modeSwitchCooldown <= 0) { + toggleLandingGear(); + } + return true; + } + + /** {@code MCH_Lib.Rot2Vec3}: unit vector along a yaw and pitch, Minecraft conventions. */ + private static Vec3 direction(float yaw, float pitch) { + double y = Math.toRadians(yaw); + double p = Math.toRadians(pitch); + return new Vec3(-Math.sin(y) * Math.cos(p), -Math.sin(p), Math.cos(y) * Math.cos(p)); + } + + // Animation and sound + + @Override + protected void tickAnimation() { + PlaneDefinition def = definition(); + float throttle = visibleThrottle(); + rotorAngleO = rotorAngle; + rotorAngle += throttle * def.rotorSpeed; + if (rotorAngle > 360.0F) { + rotorAngle -= 360.0F; + rotorAngleO -= 360.0F; + } else if (rotorAngle < 0.0F) { + rotorAngle += 360.0F; + rotorAngleO += 360.0F; + } + + float target = throttle; + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + PlaneDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return 0.6F + visibleThrottle() * 0.4F; + } +} From aa3028d41242927ad6ca356f891458a3b6d3d00c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:06 +0800 Subject: [PATCH 0735/1461] Add entity/RappelEntity.java to the 1.21.11 shared sources --- .../mcheli/entity/RappelEntity.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java new file mode 100644 index 00000000..e5781817 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java @@ -0,0 +1,106 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Carries someone down a helicopter's rope, {@code MCH_EntityHide}. It stays under its hook while + * the helicopter drifts, slides down at a steady pace and lets its rider off at the ground or at + * the end of the rope. Pulling the ropes in drops whoever is still on them. + */ +public class RappelEntity extends Entity { + private static final EntityDataAccessor DATA_VEHICLE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_ROPE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + + public RappelEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_VEHICLE, 0); + builder.define(DATA_ROPE, -1); + } + + public void attach(AircraftEntity vehicle, int rope) { + entityData.set(DATA_VEHICLE, vehicle.getId()); + entityData.set(DATA_ROPE, rope); + } + + private @Nullable AircraftEntity vehicle() { + return level().getEntity(entityData.get(DATA_VEHICLE)) instanceof AircraftEntity a && a.isAlive() ? a : null; + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + AircraftEntity vehicle = vehicle(); + Entity rider = getFirstPassenger(); + if (rider != null) { + rider.resetFallDistance(); + } + if (level().isClientSide()) { + return; + } + if (vehicle == null || !vehicle.isRepelling() || rider == null) { + arrive(); + return; + } + Vec3 hook = vehicle.ropePosition(entityData.get(DATA_ROPE)); + Vec3 m = getDeltaMovement(); + double fall = (m.y - (onGround() ? 0.01 : 0.03)) * 0.9; + setPos(hook.x, getY(), hook.z); + if (onGround()) { + arrive(); + return; + } + move(MoverType.SELF, new Vec3(0.0, fall, 0.0)); + setDeltaMovement(0.0, fall, 0.0); + if (Math.abs(getY() - hook.y) > Math.abs(vehicle.ropeLength()) + 5.0F) { + arrive(); + } + } + + private void arrive() { + ejectPassengers(); + discard(); + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } +} From 2e3d023b0851bafa8f649e442f9ca69ac67b41e4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:06 +0800 Subject: [PATCH 0736/1461] Add entity/TankEntity.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/entity/TankEntity.java | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java new file mode 100644 index 00000000..e729c7f5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java @@ -0,0 +1,267 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.physics.GroundImpact; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Tanks and cars, ported from {@code MCH_EntityTank}. + * + *

The drive model is the plane's minus lift: throttle pushes along the hull, A/D turn it, + * S brakes and then reverses when {@code EnableBack} is set, and Space is a hand brake. The hull + * does not follow the mouse. Instead a {@link TerrainFollower} reads the terrain under the + * wheels and the hull eases towards that pitch and roll, while the crew look around freely and + * the turret follows their view. + */ +public class TankEntity extends AircraftEntity implements EngineSoundSource { + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private final TerrainFollower terrain = new TerrainFollower(this); + private double currentSpeed = MIN_SPEED; + /** + * Squared horizontal distance covered by last tick's move. The original read + * {@code posX - prevPosX} here, but vanilla resets {@code xo} before {@link #simulate()} runs, + * which left vehicles with {@code PivotTurnThrottle} unable to steer. + */ + private double lastStepSq; + private float soundLevel; + private float turnSound; + + public TankEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable TankDefinition lookup(String name) { + return MCHeli.content().tank(name); + } + + @Override + public @Nullable TankDefinition definition() { + return (TankDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + TankDefinition def = (TankDefinition) definition; + terrain.create(def.groundWheels, new Vector3(0.0, -0.35, def.weightedCenterZ)); + } + + /** Walls and long drops hurt tanks and cars, scaled by speed and height. */ + @Override + protected boolean takesGroundImpacts() { + return true; + } + + /** Tanks climb and tilt on slopes all the time; only rolling over damages them. */ + @Override + protected float crashTilt() { + return 91.0F; + } + + @Override + protected void simulate() { + TankDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), -90.0F, 90.0F)); + roll = Mth.clamp(roll, -90.0F, 90.0F); + } + } + updateMotion(def, piloted); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlSub}. */ + private void updateThrottle(TankDefinition def, boolean piloted) { + reverse *= 0.8F; + float upDown = def.throttleUpDown; + if (input.brake) { + reverse *= 0.5F; + throttle = Math.max(throttle - 0.02 * upDown, 0.0); + } + if ((piloted && isCanopyClosed()) || def.targetDrone) { + if (input.throttleUp || (def.targetDrone && !piloted)) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * upDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}: ease onto the terrain, steer with A/D. */ + private void updateAttitude(TankDefinition def, float pt) { + setXRot(getXRot() + (terrain.targetPitch - getXRot()) * pt); + roll += (terrain.targetRoll - roll) * pt; + + boolean flying = !hasBlockBelow(3, 3, true); + if (flying && !(def.floats && waterDepth() > 0.0)) { + return; + } + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + float pivot = def.pivotTurnThrottle; + double dist = lastStepSq; + // With PivotTurnThrottle set the vehicle has to be rolling before it can turn, like a car. + if (pivot <= 0.0F || throttle >= pivot || reverse >= pivot / 10.0F || dist > reverse * 0.01) { + float speedScale = pivot <= 0.0F ? 1.0F : (float) Math.sqrt(Math.min(dist, 1.0)); + float direction = !input.throttleUp && input.throttleDown && throttle < pivot + 0.05 ? -1.0F : 1.0F; + float step = 0.6F * yawRate * pt * direction * speedScale; + if (input.left && !input.right) { + setYRot(getYRot() - step); + } else if (input.right && !input.left) { + setYRot(getYRot() + step); + } + } + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(TankDefinition def, boolean piloted) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + + if (depth == 0.0) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double push = throttle / 10.0; + double yawRad = Math.toRadians(getYRot()); + double pitchRad = Math.toRadians(getXRot() - 10.0F); + double vx = -Math.sin(yawRad) * Math.cos(pitchRad); + double vy = -Math.sin(pitchRad); + double vz = Math.cos(yawRad) * Math.cos(pitchRad); + my += vy * push / 8.0; + + if (def.canMoveOnGround || !isOnSolidGround()) { + if (def.enableBack && reverse > 0.0F) { + mx -= vx * reverse; + mz -= vz * reverse; + } else { + mx += vx * push; + mz += vz * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + } + + if (!terrain.isEmpty()) { + Vec3 slide = terrain.move(mx, mz); + mx += slide.x; + mz += slide.z; + if (!piloted) { + // Nobody is easing the hull in per frame, so take the attitude straight away. + setXRot(terrain.targetPitch); + roll = terrain.targetRoll; + } + } + + double startX = getX(); + double startZ = getZ(); + move(MoverType.SELF, new Vec3(mx, my, mz)); + double stepX = getX() - startX; + double stepZ = getZ() - startZ; + lastStepSq = stepX * stepX + stepZ * stepZ; + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** The original's {@code applyOnGroundPitch} is a no-op for tanks; the terrain decides. */ + @Override + protected void applyOnGroundPitch(float factor) { + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void tickAnimation() { + float target = visibleThrottle(); + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + // Turning on the spot still revs the engine. + if (!input.left && !input.right && !input.throttleDown) { + turnSound *= 0.8F; + } else { + turnSound = Math.min(turnSound + 0.1F, 0.75F); + } + target = Math.max(target, turnSound); + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + TankDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return Math.max(0.5F + visibleThrottle() * 0.5F, 0.5F + turnSound * 0.5F); + } +} From 68c0d0cf00eb4bc9648652aec150d0937aba3b88 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:07 +0800 Subject: [PATCH 0737/1461] Add entity/TerrainFollower.java to the 1.21.11 shared sources --- .../mcheli/entity/TerrainFollower.java | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java new file mode 100644 index 00000000..92e8e59e --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java @@ -0,0 +1,225 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * Works out the pitch and roll of a ground vehicle from where its wheels touch the terrain. + * Ported from {@code MCH_WheelManager}. + * + *

The original spawned an invisible entity per wheel and let each one fall onto the ground + * under its mount point. Here every wheel is a one block collision box moved with vanilla's + * collision code, which gives the same result without extra entities to sync. Each left/right + * pair spans a plane with the centre of gravity; the weighted average of their normals is the + * ground normal the hull leans towards. + */ +final class TerrainFollower { + private static final double STEP_HEIGHT = 1.5; + private static final double MAX_DRIFT = 2.0; + private static final double SNAP_DISTANCE = 8.0; + + private final AircraftEntity vehicle; + private Probe[] probes = new Probe[0]; + private Vector3 centre = Vector3.ZERO; + private double wheelBase = 1.0; + float targetPitch; + float targetRoll; + + TerrainFollower(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + /** + * @param wheels positive X wheel positions, front to back; the mirrored wheel is added for each + * @param centre centre of gravity in hull space + */ + void create(List wheels, Vector3 centre) { + this.centre = centre; + probes = new Probe[wheels.size() * 2]; + double minZ = Double.MAX_VALUE; + double maxZ = -Double.MAX_VALUE; + for (int i = 0; i < probes.length; i++) { + Vector3 w = wheels.get(i / 2); + Vector3 local = new Vector3(i % 2 == 0 ? w.x() : -w.x(), w.y(), w.z()); + Probe probe = new Probe(local, local.z() >= centre.z()); + Vec3 at = vehicle.toWorld(local); + probe.setPos(at.x, at.y + 1.0, at.z); + probes[i] = probe; + minZ = Math.min(minZ, local.z()); + maxZ = Math.max(maxZ, local.z()); + } + wheelBase = Math.max(maxZ - minZ, 1.0E-3); + } + + boolean isEmpty() { + return probes.length == 0; + } + + /** + * Drags the wheels along with the hull, lets them settle on the terrain and derives the target + * attitude from them. + * + * @return extra horizontal motion from sliding down steep slopes + */ + Vec3 move(double mx, double mz) { + for (Probe probe : probes) { + Vec3 target = vehicle.toWorld(probe.local); + if (target.distanceToSqr(probe.x, probe.y, probe.z) > SNAP_DISTANCE * SNAP_DISTANCE) { + // Teleported, or created before the vehicle was placed: start over under the hull. + probe.setPos(target.x, target.y + 1.0, target.z); + } + double dx = target.x - probe.x + mx; + double dy = (target.y - probe.y) * 0.15; + double dz = target.z - probe.z + mz; + probe.move(dx, dy, dz); + probe.move(0.0, -0.1, 0.0); + } + + int pairs = probes.length / 2; + // If neither half touches the ground, the outermost pair of that half carries the hull, + // which makes the vehicle tip over a crest instead of floating level. + int rear = pairs - 1; + for (int i = 0; i < pairs; i++) { + if (!probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + rear = -1; + break; + } + } + if (rear >= 0) { + probes[rear * 2].onGround = true; + probes[rear * 2 + 1].onGround = true; + } + int front = 0; + for (int i = pairs - 1; i >= 0; i--) { + if (probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + front = -1; + break; + } + } + if (front >= 0 && pairs > 0) { + probes[front * 2].onGround = true; + probes[front * 2 + 1].onGround = true; + } + + Vec3 wc = vehicle.toWorld(centre).subtract(vehicle.position()); + double cx = vehicle.getX() + wc.x; + double cy = vehicle.getY() + centre.y(); + double cz = vehicle.getZ() + wc.z; + Vec3 normal = Vec3.ZERO; + for (int i = 0; i < pairs; i++) { + Probe w1 = probes[i * 2]; + Probe w2 = probes[i * 2 + 1]; + Vec3 v1 = new Vec3(w1.x - cx, w1.y - cy, w1.z - cz); + Vec3 v2 = new Vec3(w2.x - cx, w2.y - cy, w2.z - cz); + Vec3 n = (w1.local.z() >= 0.0 ? v2.cross(v1) : v1.cross(v2)).normalize(); + double weight = w1.onGround || w2.onGround ? Math.abs(w1.local.z() / wheelBase) : 0.0; + normal = normal.add(n.scale(weight)); + } + normal = normal.normalize(); + + Vec3 slide = Vec3.ZERO; + if (normal.y > 0.01 && normal.y < 0.7) { + slide = new Vec3(normal.x / 50.0, 0.0, normal.z / 50.0); + } + + // Into the hull's frame, then read pitch and roll off the normal. + double yaw = Math.toRadians(vehicle.getYRot()); + double c = Math.cos(yaw); + double s = Math.sin(yaw); + double lx = normal.x * c + normal.z * s; + double lz = normal.z * c - normal.x * s; + float pitch = (float) (90.0 - Math.toDegrees(Math.atan2(normal.y, lz))); + float roll = (float) -(90.0 - Math.toDegrees(Math.atan2(normal.y, lx))); + if (normal.lengthSqr() < 1.0E-6) { + pitch = vehicle.getXRot(); + roll = vehicle.roll; + } + + var def = vehicle.definition(); + float pitchStep = def != null ? def.onGroundPitchFactor : 0.0F; + float rollStep = def != null ? def.onGroundRollFactor : 0.0F; + targetPitch = Mth.clamp(pitch, vehicle.getXRot() - pitchStep, vehicle.getXRot() + pitchStep); + targetRoll = Mth.clamp(roll, vehicle.roll - rollStep, vehicle.roll + rollStep); + + // Wheels that fell behind (stuck on a ledge) are pulled back under the hull. + for (Probe probe : probes) { + Vec3 v = vehicle.position().add(new Vec3(AircraftRotation.toWorld(vehicle.getYRot(), targetPitch, targetRoll, + probe.local.x(), probe.local.y(), probe.local.z()))); + boolean pulled = false; + double x = probe.x; + double z = probe.z; + if (x > v.x + MAX_DRIFT || x < v.x - MAX_DRIFT) { + x = Mth.clamp(x, v.x - MAX_DRIFT, v.x + MAX_DRIFT); + pulled = true; + } + if (z > v.z + MAX_DRIFT || z < v.z - MAX_DRIFT) { + z = Mth.clamp(z, v.z - MAX_DRIFT, v.z + MAX_DRIFT); + pulled = true; + } + if (pulled) { + probe.setPos(x, v.y + STEP_HEIGHT / 2.0, z); + } + } + return slide; + } + + /** One wheel: a one block box that falls onto the terrain and climbs steps up to 1.5 high. */ + private final class Probe { + final Vector3 local; + /** In front of the centre of gravity. */ + final boolean front; + double x; + double y; + double z; + boolean onGround; + + Probe(Vector3 local, boolean front) { + this.local = local; + this.front = front; + } + + void setPos(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + private AABB box() { + return new AABB(x - 0.5, y, z - 0.5, x + 0.5, y + 1.0, z + 0.5); + } + + void move(double dx, double dy, double dz) { + AABB box = box(); + Vec3 wanted = new Vec3(dx, dy, dz); + Vec3 moved = collide(wanted, box); + boolean blockedSideways = moved.x != dx || moved.z != dz; + boolean wasGrounded = onGround || (moved.y != dy && dy < 0.0); + if (blockedSideways && wasGrounded) { + // Same step-up rule as the original wheel entity: climb, move, settle back down. + Vec3 up = collide(new Vec3(0.0, STEP_HEIGHT, 0.0), box); + AABB raised = box.move(up); + Vec3 across = collide(new Vec3(dx, 0.0, dz), raised); + AABB shifted = raised.move(across); + Vec3 down = collide(new Vec3(0.0, -STEP_HEIGHT, 0.0), shifted); + Vec3 stepped = up.add(across).add(down); + if (stepped.horizontalDistanceSqr() > moved.horizontalDistanceSqr()) { + moved = stepped; + } + } + x += moved.x; + y += moved.y; + z += moved.z; + onGround = dy < 0.0 && moved.y != dy; + } + + private Vec3 collide(Vec3 movement, AABB box) { + return Entity.collideBoundingBox(vehicle, movement, box, vehicle.level(), List.of()); + } + } +} From 5c4d6fa33fd83354dbeeeab576bd22aa0c4253f1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:07 +0800 Subject: [PATCH 0738/1461] Take clicks through interactAt and run the interpolation handler in UavStationEntity on 1.21.11 --- .../mcheli/entity/UavStationEntity.java | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java new file mode 100644 index 00000000..dd7e29b2 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java @@ -0,0 +1,339 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * The UAV station and the portable UAV controller, {@code MCH_EntityUavStation}. A player sits at + * it and puts a UAV in its slot: the drone appears at the station's launch offset and the player + * flies it from there. The portable controller only launches small UAVs and its lid opens and + * closes with Shift and right-click. + * + *

The original kept the player at the station and had the drone read its pilot from there. + * Here the pilot takes the drone's reserved seat for as long as they fly it, hidden and unhurt, + * so the drone uses the same controls, cameras, HUD and weapons as every other vehicle. Leaving + * the drone, or losing it, puts the pilot back beside the station. + */ +public class UavStationEntity extends Entity { + public static final int STATION = 1; + public static final int PORTABLE = 2; + /** Further out than this, measured flat, the link breaks and the drone is lost, as in the original. */ + public static final double RANGE = 123.0; + /** The launch offset goes up to this far from the station along each axis. */ + public static final int MAX_OFFSET = 50; + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_OPEN = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_OFFSET = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BLOCK_POS); + private static final EntityDataAccessor DATA_LINKED = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private final SimpleContainer slot = new SimpleContainer(1); + private @Nullable UUID lastUav; + /** The lid of the portable controller, 0 shut to 1 open. */ + private float cover; + private float previousCover; + + public UavStationEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, (byte) STATION); + builder.define(DATA_OPEN, true); + builder.define(DATA_OFFSET, new BlockPos(12, 2, 12)); + builder.define(DATA_LINKED, false); + } + + public int kind() { + return entityData.get(DATA_KIND); + } + + public void setKind(int kind) { + entityData.set(DATA_KIND, (byte) (kind == PORTABLE ? PORTABLE : STATION)); + } + + public boolean isOpen() { + return entityData.get(DATA_OPEN); + } + + /** Where a launched drone appears, relative to the station. */ + public BlockPos offset() { + return entityData.get(DATA_OFFSET); + } + + public void setOffset(BlockPos offset) { + entityData.set(DATA_OFFSET, new BlockPos(Mth.clamp(offset.getX(), -MAX_OFFSET, MAX_OFFSET), + Mth.clamp(offset.getY(), -MAX_OFFSET, MAX_OFFSET), Mth.clamp(offset.getZ(), -MAX_OFFSET, MAX_OFFSET))); + } + + /** Whether a drone is flown from here right now; the screen lights up. */ + public boolean isLinked() { + return entityData.get(DATA_LINKED); + } + + public void setLinked(boolean linked) { + entityData.set(DATA_LINKED, linked); + } + + public SimpleContainer slot() { + return slot; + } + + public float cover(float partialTicks) { + return Mth.lerp(partialTicks, previousCover, cover); + } + + /** + * {@code initUavPostion}: the first launch offset is twelve blocks out on the diagonal the + * station faces, two up. + */ + public void initOffset() { + int quarter = (int) (Mth.positiveModulo(getYRot() + 45.0F, 360.0F) / 90.0F) % 4; + int x = quarter == 0 || quarter == 3 ? 12 : -12; + int z = quarter == 0 || quarter == 1 ? 12 : -12; + setOffset(new BlockPos(x, 2, z)); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + previousCover = cover; + cover = Mth.clamp(cover + (isOpen() ? 0.1F : -0.1F), 0.0F, 1.0F); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.03, 0.0); + move(MoverType.SELF, new Vec3(0.0, m.y, 0.0)); + setDeltaMovement(0.0, onGround() ? 0.0 : m.y * 0.96, 0.0); + + Entity user = getFirstPassenger(); + if (user instanceof ServerPlayer player && !slot.getItem(0).isEmpty()) { + launch(player, slot.getItem(0)); + } + } + + /** What the slot holds, if it is a vehicle at all. */ + public static @Nullable AircraftDefinition definitionOf(ItemStack stack) { + String name = AircraftItem.vehicleOf(stack); + return name != null ? MCHeli.content().any(name) : null; + } + + /** Whether this station can put up the vehicle in {@code stack}. */ + public boolean accepts(@Nullable AircraftDefinition def) { + return def != null && def.uav && (kind() != PORTABLE || def.smallUav); + } + + /** {@code handleItem}: puts up the drone in the slot and hands it to {@code player}. */ + private void launch(ServerPlayer player, ItemStack stack) { + AircraftDefinition def = definitionOf(stack); + if (!accepts(def) || !(level() instanceof ServerLevel server)) { + return; + } + if (ServerConfig.isDisabled(def.name())) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName()), true); + return; + } + AircraftEntity uav = ModEntities.typeFor(def).create(server, EntitySpawnReason.TRIGGERED); + if (uav == null) { + return; + } + BlockPos o = offset(); + double y = Math.max(getY() + o.getY(), server.getMinY() + 2.0); + uav.setType(def); + uav.snapTo(getX() + o.getX(), y, getZ() + o.getZ(), getYRot() - 180.0F, 0.0F); + if (!server.noCollision(uav, uav.getBoundingBox().deflate(0.1))) { + player.displayClientMessage(Component.translatable("message.mcheli.uav.blocked"), true); + slot.setItem(0, ItemStack.EMPTY); + Containers.dropItemStack(server, player.getX(), player.getY(), player.getZ(), stack); + return; + } + slot.setItem(0, ItemStack.EMPTY); + uav.setFuelLevel(def.targetDrone ? uav.maxFuel() : (int) (uav.maxFuel() * 0.05F)); + server.addFreshEntity(uav); + if (!def.targetDrone) { + player.closeContainer(); + control(player, uav); + } + } + + /** {@code controlLastAircraft}: takes the last drone flown from here again. */ + public boolean continueControl(ServerPlayer player) { + AircraftEntity uav = lastUav(); + if (uav == null || getFirstPassenger() != player || !uav.canTakeRemotePilot()) { + return false; + } + player.closeContainer(); + return control(player, uav); + } + + public @Nullable AircraftEntity lastUav() { + if (lastUav == null || !(level() instanceof ServerLevel server)) { + return null; + } + Entity e = server.getEntity(lastUav); + return e instanceof AircraftEntity uav && uav.isAlive() && !uav.isDestroyed() && inRange(uav) ? uav : null; + } + + public boolean inRange(Entity uav) { + double dx = uav.getX() - getX(); + double dz = uav.getZ() - getZ(); + return dx * dx + dz * dz <= RANGE * RANGE; + } + + private boolean control(ServerPlayer player, AircraftEntity uav) { + player.stopRiding(); + if (!uav.takeRemotePilot(player, this)) { + return false; + } + lastUav = uav.getUUID(); + setLinked(true); + player.setYRot(uav.getYRot()); + return true; + } + + /** Where a pilot comes back to when the drone is left or lost: the seat in front of the station. */ + public Vec3 returnPosition() { + float yaw = getYRot() * Mth.DEG_TO_RAD; + return new Vec3(getX() - Mth.sin(yaw) * 0.9, getY(), getZ() + Mth.cos(yaw) * 0.9); + } + + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + if (isVehicle()) { + return InteractionResult.PASS; + } + if (kind() == PORTABLE && player.isSecondaryUseActive()) { + if (!level().isClientSide()) { + entityData.set(DATA_OPEN, !isOpen()); + } + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive() || kind() == PORTABLE && !isOpen()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer server) { + player.setYRot(getYRot() - 180.0F); + if (player.startRiding(this)) { + server.openMenu(new SimpleMenuProvider((id, inventory, p) -> new UavStationMenu(id, inventory, this), + Component.translatable(kind() == PORTABLE ? "item.mcheli.uav_station2" : "item.mcheli.uav_station"))); + } + } + return InteractionResult.SUCCESS; + } + + /** The original sat the player down in front of the console, lower for the portable one. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + double y = kind() == PORTABLE ? -0.6 : 0.35; + return new Vec3(0.0, y, 0.9).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved() || damage <= 0.0F) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + AircraftEntity uav = lastUav(); + if (uav != null) { + uav.releaseRemotePilot(); + } + Containers.dropContents(level, blockPosition(), slot); + if (!creative) { + spawnAtLocation(level, item()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), source.getEntity() instanceof Player ? "hit" : "helidmg", 1.0F, 1.0F); + discard(); + return true; + } + + private Item item() { + return kind() == PORTABLE ? ModItems.UAV_CONTROLLER.get() : ModItems.UAV_STATION.get(); + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith(@Nullable Entity other) { + return true; + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(item()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + setKind(input.getIntOr("Kind", STATION)); + entityData.set(DATA_OPEN, input.getBooleanOr("Open", true)); + setOffset(new BlockPos(input.getIntOr("UavX", 12), input.getIntOr("UavY", 2), input.getIntOr("UavZ", 12))); + lastUav = input.read("LastUav", UUIDUtil.CODEC).orElse(null); + slot.setItem(0, input.read("Item", ItemStack.CODEC).orElse(ItemStack.EMPTY)); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putInt("Kind", kind()); + output.putBoolean("Open", isOpen()); + BlockPos o = offset(); + output.putInt("UavX", o.getX()); + output.putInt("UavY", o.getY()); + output.putInt("UavZ", o.getZ()); + if (lastUav != null) { + output.store("LastUav", UUIDUtil.CODEC, lastUav); + } + if (!slot.getItem(0).isEmpty()) { + output.store("Item", ItemStack.CODEC, slot.getItem(0)); + } + } +} From 722b635215b78021fe0b2e991cf5da407c48a2f5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:08 +0800 Subject: [PATCH 0739/1461] Add entity/VehicleInventory.java to the 1.21.11 shared sources --- .../mcheli/entity/VehicleInventory.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java new file mode 100644 index 00000000..1fdc978e --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java @@ -0,0 +1,145 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.supply.SupplyRules; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +/** + * What a vehicle carries besides its crew, {@code MCH_AircraftInventory} and the container half of + * {@code W_EntityContainer}: the equipment slots of the vehicle screen (three fuel cans and a + * parachute per ejection seat) and the cargo chest of vehicles with an {@code InventorySize}. + * + *

The cargo chest always has room for the largest size, 54 slots, and the screen shows as many + * rows as the definition asks for. Items in rows a content reload took away are kept rather than + * lost, and still drop when the vehicle is destroyed. + */ +public final class VehicleInventory { + public static final int FUEL_SLOTS = 3; + public static final int PARACHUTE_SLOTS = 2; + public static final int EQUIPMENT_SLOTS = FUEL_SLOTS + PARACHUTE_SLOTS; + public static final int CARGO_SLOTS = 54; + /** How far from the vehicle's centre a player outside may keep a screen open, past its width. */ + private static final double REACH = 12.0; + + private final AircraftEntity vehicle; + private final SimpleContainer equipment = new GuardedContainer(EQUIPMENT_SLOTS); + private final SimpleContainer cargo = new GuardedContainer(CARGO_SLOTS); + + VehicleInventory(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + public SimpleContainer equipment() { + return equipment; + } + + public SimpleContainer cargo() { + return cargo; + } + + /** {@code isUseableByPlayer}: the crew, or someone standing next to the vehicle. */ + public boolean stillValid(Player player) { + if (vehicle.isRemoved() || vehicle.definition() == null) { + return false; + } + if (player.getVehicle() == vehicle) { + return true; + } + double reach = REACH + vehicle.definition().bodyWidth / 2.0; + return player.distanceToSqr(vehicle) <= reach * reach; + } + + // Fuel + + /** + * {@code updateFuel}: every half second, while the vehicle stands on something, each fuel can + * in a fuel slot pours up to 100 units into the tank. Server only. + * + * @return whether any fuel went in + */ + boolean pourFuelCans(int ticks) { + if (ticks % SupplyRules.FUEL_SLOT_INTERVAL != 0 || !vehicle.canSupply()) { + return false; + } + boolean poured = false; + for (int slot = 0; slot < FUEL_SLOTS; slot++) { + ItemStack can = equipment.getItem(slot); + if (!(can.getItem() instanceof FuelItem)) { + continue; + } + int amount = SupplyRules.fuelFromCan(vehicle.maxFuel() - vehicle.fuel(), FuelItem.contents(can)); + if (amount > 0) { + vehicle.refuel(amount); + can.setDamageValue(can.getDamageValue() + amount); + poured = true; + } + } + if (poured) { + equipment.setChanged(); + } + return poured; + } + + // Parachutes + + /** Parachute slots the vehicle screen offers: one per ejection seat. */ + public static int parachuteSlots(AircraftDefinition def) { + return Math.min(def.ejectionSeats(), PARACHUTE_SLOTS); + } + + /** + * {@code consumeParachute}: takes one parachute out of the parachute slots for a crew member + * who bails out. + * + * @return false when there is none left + */ + boolean takeParachute() { + for (int slot = FUEL_SLOTS; slot < EQUIPMENT_SLOTS; slot++) { + if (equipment.getItem(slot).getItem() instanceof ParachuteItem) { + equipment.removeItem(slot, 1); + return true; + } + } + return false; + } + + // Saving and dropping + + void save(ValueOutput output) { + ContainerHelper.saveAllItems(output.child("Equipment"), equipment.getItems(), false); + ContainerHelper.saveAllItems(output.child("Cargo"), cargo.getItems(), false); + } + + void load(ValueInput input) { + equipment.clearContent(); + cargo.clearContent(); + input.child("Equipment").ifPresent(in -> ContainerHelper.loadAllItems(in, equipment.getItems())); + input.child("Cargo").ifPresent(in -> ContainerHelper.loadAllItems(in, cargo.getItems())); + } + + /** {@code setDead}: everything aboard falls out where the vehicle was. Server only. */ + void dropAll() { + Containers.dropContents(vehicle.level(), vehicle, equipment); + Containers.dropContents(vehicle.level(), vehicle, cargo); + } + + /** A container that only stays open for players {@link #stillValid(Player)} allows. */ + private final class GuardedContainer extends SimpleContainer { + GuardedContainer(int size) { + super(size); + } + + @Override + public boolean stillValid(Player player) { + return VehicleInventory.this.stillValid(player); + } + } +} From 2251dbc580e9c6be289f21617e55aa7205223b11 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:08 +0800 Subject: [PATCH 0740/1461] Add entity/VehicleParticles.java to the 1.21.11 shared sources --- .../mcheli/entity/VehicleParticles.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java new file mode 100644 index 00000000..46ed88a8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java @@ -0,0 +1,180 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.ParticleSplash; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; + +/** + * The spray, dust and exhaust a vehicle throws up, all client side: + *

    + *
  • {@code onUpdate_ParticleSandCloud}: a helicopter's downwash raises a ring of dust off the + * ground or water below it; boats and amphibians with {@code EnableSeaSurfaceParticle} raise + * one off the sea while under way.
  • + *
  • {@code onUpdate_ParticleSplash}: each {@code AddParticleSplash} point sprays while it moves + * through water.
  • + *
  • {@code onUpdate_ParticleNozzle}: jet nozzles smoke with the throttle, more when the blast + * hits the ground, as on a Harrier in the hover.
  • + *
  • {@code onUpdate_ParticleLandingGear}: a plane rolling fast kicks up bits of the ground.
  • + *
+ */ +final class VehicleParticles { + private static final int WHITE = 0xFFFFFF; + + private VehicleParticles() { + } + + static void tick(AircraftEntity vehicle, AircraftDefinition def) { + if (vehicle.isDestroyed()) { + return; + } + switch (vehicle) { + case HelicopterEntity heli -> sandCloud(heli, def, false); + case PlaneEntity plane -> { + if (def instanceof PlaneDefinition planeDef) { + nozzles(plane, planeDef); + } + landingGear(plane); + splashes(plane, def); + sandCloud(plane, def, true); + } + case TankEntity tank -> { + splashes(tank, def); + sandCloud(tank, def, true); + } + default -> { + } + } + } + + private static void sandCloud(AircraftEntity vehicle, AircraftDefinition def, boolean seaOnly) { + if (seaOnly && !def.seaSurfaceParticles || def.particlesScale <= 0.01F) { + return; + } + Level level = vehicle.level(); + RandomSource random = vehicle.getRandom(); + float scale = def.particlesScale * 3.0F * (seaOnly ? 2.0F : 1.0F); + double throttle = Math.min(vehicle.visibleThrottle() * 2.0, 1.0); + // A boat needs some water under it before it sprays; the original counted water blocks. + int count = seaOnly ? (int) (scale * 7.0F) : 0; + int range = (int) (scale * 10.0F) + 1; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + int bx = Mth.floor(vehicle.getX() + 0.5); + int by = Mth.floor(vehicle.getY() + 0.5); + int bz = Mth.floor(vehicle.getZ() + 0.5); + int y = 0; + double groundY = 0.0; + boolean found = false; + for (; y < range && !found; y++) { + for (int x = -1; x <= 1 && !found; x++) { + for (int z = -1; z <= 1 && !found; z++) { + BlockState state = level.getBlockState(pos.set(bx + x, by - y, bz + z)); + if (state.isAir()) { + continue; + } + if (seaOnly && state.getFluidState().is(FluidTags.WATER)) { + count--; + } + if (count <= 0) { + groundY = vehicle.getY() + 1.0 + scale / 5.0F - y; + found = true; + } + } + } + } + if (!found) { + return; + } + double density = (range - y + 1) / (5.0 * scale) / 2.0; + for (int k = 0; k < (int) (throttle * 6.0 * density); k++) { + float angle = random.nextFloat() * Mth.TWO_PI; + double dx = Mth.cos(angle); + double dz = Mth.sin(angle); + level.addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, scale * 5.0F, (int) (10.0F * scale) + 1), true, + vehicle.getX() + dx * scale * 3.0, groundY + (random.nextDouble() - 0.5) * scale, vehicle.getZ() + dz * scale * 3.0, + scale * dx * 0.3, scale * -0.02, scale * dz * 0.3); + } + } + + private static void splashes(AircraftEntity vehicle, AircraftDefinition def) { + if (def.splashes.isEmpty()) { + return; + } + double mx = vehicle.getX() - vehicle.xo; + double mz = vehicle.getZ() - vehicle.zo; + double dist = Math.min(mx * mx + mz * mz, 1.0); + RandomSource random = vehicle.getRandom(); + for (ParticleSplash splash : def.splashes) { + for (int i = 0; i < splash.count(); i++) { + if (dist > 0.03 + random.nextFloat() * 0.1) { + splash(vehicle, splash, -mx * splash.acceleration(), -mz * splash.acceleration(), splash.size() * (0.5 + dist * 0.5)); + } + } + } + } + + private static void splash(AircraftEntity vehicle, ParticleSplash splash, double mx, double mz, double size) { + RandomSource random = vehicle.getRandom(); + Vec3 at = vehicle.toWorld(splash.position()).add(random.nextDouble() - 0.5, (random.nextDouble() - 0.5) * 0.5, random.nextDouble() - 0.5); + BlockPos block = BlockPos.containing(at.x + 0.5, at.y, at.z + 0.5); + if (!vehicle.level().getFluidState(block).is(FluidTags.WATER)) { + return; + } + int age = splash.age() + (int) (random.nextFloat() * 0.5F * splash.age()); + vehicle.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, (float) size * (random.nextFloat() * 0.2F + 0.8F), age), true, + at.x, at.y, at.z, mx + (random.nextFloat() - 0.5) * 0.7, splash.motionY(), mz + (random.nextFloat() - 0.5) * 0.7); + } + + private static void nozzles(PlaneEntity plane, PlaneDefinition def) { + float throttle = plane.visibleThrottle(); + if (def.nozzles.isEmpty() || throttle <= 0.1F) { + return; + } + RandomSource random = plane.getRandom(); + PartState nozzle = plane.nozzle(); + float tilt = nozzle != null ? nozzle.angle() * Mth.DEG_TO_RAD : 0.0F; + // Out of the back, turned down as the nozzles swing towards the hover. + Vector3f blast = plane.toWorldOffset(new Vector3(0.0, -Mth.sin(tilt), -Mth.cos(tilt))); + for (MovingPart part : def.nozzles) { + if (random.nextFloat() > throttle * 1.5F) { + continue; + } + Vec3 at = plane.toWorld(part.position()).add(blast.x, blast.y, blast.z); + BlockPos ahead = BlockPos.containing(at.x + blast.x * 3.0, at.y + blast.y * 3.0, at.z + blast.z * 3.0); + float spread = plane.level().getBlockState(ahead).isAir() ? 0.7F : 2.0F; + plane.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, 5.0F * def.particlesScale, 30), true, at.x, at.y, at.z, + blast.x + (random.nextFloat() - 0.5F) * spread, blast.y, blast.z + (random.nextFloat() - 0.5F) * spread); + } + } + + private static void landingGear(PlaneEntity plane) { + double dx = plane.getX() - plane.xo; + double dz = plane.getZ() - plane.zo; + if (!plane.onGround() || dx * dx + dz * dz <= 0.01) { + return; + } + BlockPos below = BlockPos.containing(plane.getX(), plane.getY() - 0.5, plane.getZ()); + BlockState state = plane.level().getBlockState(below); + if (state.getRenderShape() == RenderShape.INVISIBLE) { + return; + } + RandomSource random = plane.getRandom(); + float width = plane.getBbWidth(); + plane.level().addParticle(new BlockParticleOption(ParticleTypes.BLOCK, state), + plane.getX() + (random.nextFloat() - 0.5) * width, plane.getBoundingBox().minY + 0.1, plane.getZ() + (random.nextFloat() - 0.5) * width, + -dx * 4.0, 1.5, -dz * 4.0); + } +} From de9d3552a076f71715064be03b74f69aacf3de39 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:08 +0800 Subject: [PATCH 0741/1461] Add entity/projectile/A10Entity.java to the 1.21.11 shared sources --- .../mcheli/entity/projectile/A10Entity.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java new file mode 100644 index 00000000..e72e952e --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java @@ -0,0 +1,160 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import org.jspecify.annotations.Nullable; + +/** + * The A-10 that answers a close air support call, {@code MCH_EntityA10}. It dives in for a second, + * strafes the target with forty GAU-8 rounds and climbs away. + */ +public class A10Entity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_DIRECTION = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.INT); + private static final int LIFETIME = 70; + + private int despawnCount; + private int shotCount; + public int power = 32; + public int explosionPower = 1; + public float acceleration = 4.0F; + private @Nullable Entity shooter; + private @Nullable Entity user; + + public A10Entity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_DIRECTION, 0); + } + + /** + * @param direction 0 heading south (+Z), then clockwise in steps of 90 degrees + */ + public void setup(String weapon, int direction, double mx, double mz, @Nullable Entity shooter, @Nullable Entity user) { + entityData.set(DATA_WEAPON, weapon); + entityData.set(DATA_DIRECTION, direction); + setYRot(90 * direction); + yRotO = getYRot(); + setDeltaMovement(mx, 0.0, mz); + this.shooter = shooter; + this.user = user; + } + + public int direction() { + return entityData.get(DATA_DIRECTION); + } + + /** Hidden during the first second, while it is still out of sight. */ + public boolean isVisible() { + return despawnCount > 20; + } + + @Override + public void tick() { + super.tick(); + despawnCount++; + if (!level().isClientSide()) { + if (despawnCount > LIFETIME) { + discard(); + return; + } + if (shotCount < 40) { + for (int i = 0; i < 2; i++) { + fireGau8(shotCount); + shotCount++; + } + if (shotCount == 38) { + ModSounds.playAt(this, "gau-8_snd", 150.0F, 1.0F); + } + } + } + if (despawnCount <= 20) { + setDeltaMovement(getDeltaMovement().x, -0.3, getDeltaMovement().z); + } else { + setPos(getX() + getDeltaMovement().x, getY() + getDeltaMovement().y, getZ() + getDeltaMovement().z); + setDeltaMovement(getDeltaMovement().add(0.0, 0.02, 0.0)); + } + } + + private void fireGau8(int count) { + int dir = direction(); + double x = getX(); + double z = getZ(); + double tx = random.nextDouble() - 0.5; + double ty = -2.6; + double tz = random.nextDouble() - 0.5; + switch (dir) { + case 0 -> { + tz += 10.0; + z += count * 0.6; + } + case 1 -> { + tx -= 10.0; + x -= count * 0.6; + } + case 2 -> { + tz -= 10.0; + z -= count * 0.6; + } + default -> { + tx += 10.0; + x += count * 0.6; + } + } + BulletEntity round = ModEntities.BULLET.get().create(level(), EntitySpawnReason.TRIGGERED); + if (round == null) { + return; + } + round.launch(x, getY(), z, tx, ty, tz, 90 * dir, 30.0F, acceleration); + round.setWeapon(entityData.get(DATA_WEAPON)); + round.explosionPower = shotCount % 4 == 0 ? explosionPower : 0; + round.power = power; + round.setOwners(shooter, user); + level().addFreshEntity(round); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 512.0 * 512.0; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From e54264e110174c96002699da825981a45c576937 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:09 +0800 Subject: [PATCH 0742/1461] Add entity/projectile/AAMissileEntity.java to the 1.21.11 shared sources --- .../entity/projectile/AAMissileEntity.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java new file mode 100644 index 00000000..a9d41520 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** An air to air missile homing on its locked target, {@code MCH_EntityAAMissile}. */ +public class AAMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 3422500.0; + + public AAMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (age() > 4 && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize * 0.5F); + } + if (level().isClientSide() || w == null || isRemoved()) { + return; + } + Entity t = target(); + if (user() == null || t == null || t.isRemoved()) { + discard(); + return; + } + double d = distanceToSqr(t); + if (d > MAX_DISTANCE_SQ) { + discard(); + } else if (age() > w.rigidityTime) { + // The original compared the squared distance with the plain fuse distance. + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + setPos((t.getX() + getX()) / 2.0, (t.getY() + getY()) / 2.0, (t.getZ() + getZ()) / 2.0); + impact(new Impact(position(), t, null, Direction.UP), 1.0F); + } else { + guideTo(t.getX(), t.getY(), t.getZ()); + } + } + } + + @Override + public String defaultModel() { + return "aamissile"; + } +} From 8159494b2d840ae6b736754fa00e1d78769fefcd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:09 +0800 Subject: [PATCH 0743/1461] Add entity/projectile/ASMissileEntity.java to the 1.21.11 shared sources --- .../entity/projectile/ASMissileEntity.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java new file mode 100644 index 00000000..e5f46c96 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java @@ -0,0 +1,91 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * An air to surface missile flying to the block the gunner aimed at, {@code MCH_EntityASMissile}. + * Powered ones climb for half a second first; gliding bombs only steer sideways. + */ +public class ASMissileEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + + public ASMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public float gravity() { + return isBomblet() ? -0.03F : super.gravity(); + } + + @Override + public float gravityInWater() { + return isBomblet() ? -0.03F : super.gravityInWater(); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && !isBomblet()) { + trail(w.trajectoryParticle, 3, 10.0F * w.smokeSize * 0.5F); + } + if (w != null && !level().isClientSide() && !isBomblet() && !isRemoved()) { + BlockPos aim = BlockPos.containing(targetX, targetY, targetZ); + if (!level().getBlockState(aim).isAir()) { + double dist = Math.sqrt(distanceToSqr(targetX, targetY, targetZ)); + if (dist < w.proximityFuseDist) { + if (w.bomblet > 0) { + for (int i = 0; i < w.bomblet; i++) { + sprinkleBomblet(); + } + } else { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + } + discard(); + } else if (gravity() == 0.0F) { + double up = age() < 10 ? 20.0 : 0.0; + aimAlong(targetX - getX(), targetY + up - getY(), targetZ - getZ()); + } else { + double dx = targetX - getX(); + double dy = (targetY - getY()) * 0.3; + double dz = targetZ - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + Vec3 m = getDeltaMovement(); + setDeltaMovement(dx * acceleration / d, m.y, dz * acceleration / d); + } + } + } + faceMotion(); + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + ASMissileEntity child = spawnChild(ModEntities.AS_MISSILE.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x * 0.5 + (random.nextFloat() - 0.5F) * spread, + m.y * 0.5 / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z * 0.5 + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From 3fd89b7b7c0980c833cde28b27732841dbe44aca Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:10 +0800 Subject: [PATCH 0744/1461] Add entity/projectile/ATMissileEntity.java to the 1.21.11 shared sources --- .../entity/projectile/ATMissileEntity.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java new file mode 100644 index 00000000..d7031ecf --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java @@ -0,0 +1,84 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** + * An anti tank missile, {@code MCH_EntityATMissile}. In the default mode it flies straight at the + * locked target; in top attack mode it climbs 150 blocks above the shooter and dives. + */ +public class ATMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 2250000.0; + + /** 0 direct, 1 top attack. */ + public int guidanceType; + + public ATMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && tickCount >= w.trajectoryParticleStartTick) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + if (!level().isClientSide() && !isRemoved()) { + Entity t = target(); + if (user() != null && t != null && !t.isRemoved() && w != null) { + steer(w, t); + } else { + discard(); + } + } + faceMotion(); + } + + private void steer(WeaponDefinition w, Entity t) { + double x = t.getX() - getX(); + double y = t.getY() - getY(); + double z = t.getZ() - getZ(); + double d = x * x + y * y + z * z; + if (d > MAX_DISTANCE_SQ) { + discard(); + return; + } + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + return; + } + int rigidity = w.rigidityTime; + if (age() <= rigidity) { + return; + } + float af = age() < rigidity + w.trajectoryParticleStartTick ? 0.5F : 1.0F; + if (guidanceType == 1) { + Entity shooter = user(); + if (age() <= rigidity + 20) { + guideTo(t.getX(), shooter.getY() + 150.0, t.getZ()); + } else if (age() <= rigidity + 30) { + guideTo(t.getX(), shooter.getY(), t.getZ()); + } else { + if (age() == rigidity + 35) { + power = (int) (power * 1.2F); + if (explosionPower > 0) { + explosionPower++; + } + } + guideTo(t.getX(), t.getY(), t.getZ()); + } + } else { + d = Math.sqrt(d); + setDeltaMovement(x * acceleration / d * af, y * acceleration / d * af, z * acceleration / d * af); + } + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From 711c0a9e3017b2cfab699588411c7d7078cb387f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:10 +0800 Subject: [PATCH 0745/1461] Add entity/projectile/BombEntity.java to the 1.21.11 shared sources --- .../mcheli/entity/projectile/BombEntity.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java new file mode 100644 index 00000000..c758d75b --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** A free fall bomb, {@code MCH_EntityBomb}. Cluster bombs split into bomblets. */ +public class BombEntity extends ProjectileEntity { + public BombEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (!level().isClientSide() && w != null && !isRemoved()) { + Vec3 m = getDeltaMovement(); + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + float fuse = w.proximityFuseDist; + if (fuse > 0.1F && age() % 10 == 0) { + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(fuse), this::canHit)) { + if (entity instanceof LivingEntity) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + BombEntity child = spawnChild(ModEntities.BOMB.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + /** A cluster bomb that has not split yet is not drawn when it splits right away. */ + public boolean isVisible() { + WeaponDefinition w = weapon(); + return w == null || isBomblet() || w.bomblet <= 0 || w.bombletSTime > 0; + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From bf1f155e5ddfd52a5ccd8ade74a93d9df7d8537e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:11 +0800 Subject: [PATCH 0746/1461] Add entity/projectile/BulletEntity.java to the 1.21.11 shared sources --- .../entity/projectile/BulletEntity.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java new file mode 100644 index 00000000..b2b418ac --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** A gun round, {@code MCH_EntityBullet}. High explosive rounds may carry a proximity fuse. */ +public class BulletEntity extends ProjectileEntity { + public BulletEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return true; + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (isRemoved() || level().isClientSide() || age() <= 1 || w == null || explosionPower <= 0) { + return; + } + float fuse = w.proximityFuseDist; + if (fuse > 0.1F) { + fuse++; + float range = fuse + Math.abs(w.acceleration); + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(range), this::canHit)) { + if (entity.distanceToSqr(this) < fuse * fuse) { + setPos((entity.getX() + getX()) / 2.0, (entity.getY() + getY()) / 2.0, (entity.getZ() + getZ()) / 2.0); + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + + @Override + public String defaultModel() { + return "bullet"; + } +} From 9c7622672d768c58cc4a8ce9974a6c5b1a50b4ce Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:11 +0800 Subject: [PATCH 0747/1461] Add entity/projectile/DispensedItemEntity.java to the 1.21.11 shared sources --- .../projectile/DispensedItemEntity.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java new file mode 100644 index 00000000..69a2f674 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java @@ -0,0 +1,121 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseFireBlock; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A canister that uses an item on every block around where it lands, {@code MCH_EntityDispensedItem}: + * water bombers put out fires, crop dusters spread bone meal or seeds, and so on. + */ +public class DispensedItemEntity extends ProjectileEntity { + public DispensedItemEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize); + } + if (!level().isClientSide() && w != null) { + Vec3 m = getDeltaMovement(); + if (acceleration < 1.0E-4) { + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + } + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + } + tickBomblets(); + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + return; + } + WeaponDefinition w = weapon(); + Item item = w != null ? LegacyItems.item(w.dispenseItem, w.dispenseDamage) : null; + if (w != null && item != null && user() instanceof ServerPlayer player && !player.isRemoved()) { + BlockPos center = hit.block() != null ? hit.block() : blockPosition(); + int range = w.dispenseRange - 1; + for (int x = -range; x <= range; x++) { + for (int y = -range; y <= range; y++) { + for (int z = -range; z <= range; z++) { + int dist = x * x + y * y + z * z; + if (dist <= range * range && (dist <= 0.5 * range * range || random.nextInt(2) == 0)) { + useOn(server, center.offset(x, y, z), item); + } + } + } + } + } + discard(); + } + + private void useOn(ServerLevel server, BlockPos pos, Item item) { + if (pos.getY() < server.getMinY() || pos.getY() >= server.getMaxY()) { + return; + } + BlockState state = server.getBlockState(pos); + if (state.isAir()) { + return; + } + if (item == Items.WATER_BUCKET) { + // A water drop douses fire and quenches lava instead of placing water. + FluidState fluid = server.getFluidState(pos); + if (state.getBlock() instanceof BaseFireBlock) { + server.removeBlock(pos, false); + } else if (fluid.is(FluidTags.LAVA)) { + server.setBlockAndUpdate(pos, fluid.isSource() ? Blocks.OBSIDIAN.defaultBlockState() : Blocks.COBBLESTONE.defaultBlockState()); + } + return; + } + // Used from above with nobody holding it, the way a dispenser block would. + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(pos).add(0.0, 0.5, 0.0), Direction.UP, pos, false); + item.useOn(new UseOnContext(server, null, InteractionHand.MAIN_HAND, new ItemStack(item), hit)); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + DispensedItemEntity child = spawnChild(ModEntities.DISPENSED_ITEM.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From 09b5bfa11d1c1cc8784e70ef28d15c4e4b854a75 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:12 +0800 Subject: [PATCH 0748/1461] Add entity/projectile/FlareEntity.java to the 1.21.11 shared sources --- .../mcheli/entity/projectile/FlareEntity.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java new file mode 100644 index 00000000..a564a1fc --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java @@ -0,0 +1,161 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A burning flare, or with a fuse a smoke grenade that bursts into a cloud. Port of + * {@code MCH_EntityFlare}. Flares only look the part: decoying happens in the dispenser. + */ +public class FlareEntity extends Entity { + private static final EntityDataAccessor DATA_FUSE = SynchedEntityData.defineId(FlareEntity.class, EntityDataSerializers.INT); + + private double gravity = -0.013; + private double airResistance = 0.992; + private float size = 6.0F; + + public FlareEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_FUSE, 0); + } + + /** + * @param fuse ticks until a smoke grenade bursts, 0 for a flare + * @param slow the long hanging type 4 flare + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float size, int fuse, boolean slow) { + snapTo(x, y, z, random.nextFloat() * 360.0F, random.nextFloat() * 360.0F); + setDeltaMovement(mx, my, mz); + this.size = size; + entityData.set(DATA_FUSE, fuse); + if (slow) { + gravity *= 0.6; + airResistance = 0.995; + } + } + + private int fuse() { + return entityData.get(DATA_FUSE); + } + + /** A smoke grenade rather than a burning flare. */ + public boolean isSmoke() { + return fuse() > 0; + } + + @Override + public void tick() { + if (fuse() > 0 && tickCount >= fuse()) { + // Removal bursts the grenade on the client. + discard(); + return; + } + boolean server = !level().isClientSide(); + if (server && (!level().isLoaded(blockPosition()) || tickCount > 300)) { + discard(); + return; + } + super.tick(); + Vec3 m = getDeltaMovement(); + if (server) { + HitResult hit = level().clip(new ClipContext(position(), position().add(m), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.MISS) { + discard(); + return; + } + } else { + trail(); + } + setPos(getX() + m.x, getY() + m.y, getZ() + m.z); + setDeltaMovement(m.x * airResistance, m.y + gravity, m.z * airResistance); + if (server && (isInWater() || onGround())) { + discard(); + } + } + + private void trail() { + double x = (getX() - xo) / 2.0; + double y = (getY() - yo) / 2.0; + double z = (getZ() - zo) / 2.0; + for (int i = 0; i < 2; i++) { + if (size < 5.0F && random.nextInt(2) == 0) { + continue; + } + level().addAlwaysVisibleParticle(fuse() > 0 ? ParticleTypes.CLOUD : ParticleTypes.LARGE_SMOKE, true, + xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + if (fuse() == 0) { + level().addAlwaysVisibleParticle(ParticleTypes.FLAME, true, xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + } + } + } + + /** A smoke grenade bursting into a cloud that hangs around for ten seconds or more. */ + private void burst() { + if (!level().isClientSide()) { + return; + } + for (int i = 0; i < 20; i++) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + getX() + (random.nextDouble() - 0.5) * 10.0, + getY() + (random.nextDouble() - 0.5) * 10.0, + getZ() + (random.nextDouble() - 0.5) * 10.0, + (random.nextDouble() - 0.5) * 0.045, 0.005, (random.nextDouble() - 0.5) * 0.045); + } + } + + @Override + public void remove(RemovalReason reason) { + if (level().isClientSide() && fuse() > 0) { + burst(); + entityData.set(DATA_FUSE, 0); + } + super.remove(reason); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isInvulnerable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From 64f39bf1b665f1285561ec64d485c7f1eabe75b3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:12 +0800 Subject: [PATCH 0749/1461] Add entity/projectile/MarkerRocketEntity.java to the 1.21.11 shared sources --- .../entity/projectile/MarkerRocketEntity.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java new file mode 100644 index 00000000..eb723310 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java @@ -0,0 +1,139 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; + +/** + * A marking rocket, {@code MCH_EntityMarkerRocket}. Where it lands under open sky it pops red + * smoke, and five seconds later a stick of bombs falls on the mark. + */ +public class MarkerRocketEntity extends ProjectileEntity { + private static final EntityDataAccessor DATA_STATUS = SynchedEntityData.defineId(MarkerRocketEntity.class, EntityDataSerializers.BYTE); + + /** 0 inert, 1 flying, 2 marking. */ + private int countDown; + + public MarkerRocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_STATUS, (byte) 0); + } + + public void setStatus(int status) { + if (!level().isClientSide()) { + entityData.set(DATA_STATUS, (byte) status); + } + } + + public int status() { + return entityData.get(DATA_STATUS); + } + + @Override + public void tick() { + int status = status(); + if (level().isClientSide()) { + if (weapon() == null) { + super.tick(); + } + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && status != 0) { + if (status == 1) { + super.tick(); + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } else { + float gb = random.nextFloat() * 0.3F; + int rgb = (int) ((random.nextFloat() * 0.2F + 0.8F) * 255) << 16 | (int) (gb * 255) << 8 | (int) (gb * 255); + for (int i = 0; i < 5; i++) { + level().addAlwaysVisibleParticle(new DustParticleOptions(rgb, 4.0F), true, getX(), getY(), getZ(), + (random.nextFloat() - 0.5F) * 0.7F, 0.3F + random.nextFloat() * 0.3F, (random.nextFloat() - 0.5F) * 0.7F); + } + } + } + } else if (status == 0 || isInWater()) { + discard(); + } else if (status == 1) { + super.tick(); + } else if (countDown > 0) { + countDown--; + if (countDown == 40) { + dropBombs(); + } + } else { + discard(); + } + } + + private void dropBombs() { + int count = 6 + random.nextInt(2); + for (int i = 0; i < count; i++) { + BombEntity bomb = ModEntities.BOMB.get().create(level(), EntitySpawnReason.TRIGGERED); + if (bomb == null) { + continue; + } + bomb.launch(getX() + (random.nextFloat() - 0.5F) * 15.0F, 260.0F + random.nextFloat() * 10.0F + i * 30, + getZ() + (random.nextFloat() - 0.5F) * 15.0F, 0.0, -0.5, 0.0, 0.0F, 90.0F, 4.0); + bomb.setWeapon(weaponName()); + bomb.explosionPower = 3 + random.nextInt(2); + bomb.explosionPowerInWater = 0; + bomb.power = 30; + bomb.piercing = 0; + bomb.setOwners(shooter, user); + level().addFreshEntity(bomb); + } + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (level().isClientSide() || hit.entity() != null || hit.block() == null) { + return; + } + BlockPos mark = hit.block().relative(hit.side()); + if (!level().getBlockState(mark).isAir()) { + discard(); + return; + } + level().setBlockAndUpdate(mark, Blocks.FIRE.defaultBlockState()); + int solid = 0; + for (int y = mark.getY() + 1; y < level().getMaxY(); y++) { + if (!level().getBlockState(new BlockPos(mark.getX(), y, mark.getZ())).isAir() && ++solid >= 5) { + break; + } + } + if (solid < 5) { + setStatus(2); + setPos(mark.getX() + 0.5, mark.getY() + 1.1, mark.getZ() + 0.5); + xo = getX(); + yo = getY(); + zo = getZ(); + countDown = 100; + } else { + discard(); + } + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From 05a1deecd0a810ffce3968a390bb7b80c17392df Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:13 +0800 Subject: [PATCH 0750/1461] Add entity/projectile/ProjectileEntity.java to the 1.21.11 shared sources --- .../entity/projectile/ProjectileEntity.java | 790 ++++++++++++++++++ 1 file changed, 790 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java new file mode 100644 index 00000000..30f171c5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java @@ -0,0 +1,790 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Combat; +import com.jnhprojects.mcheli.weapon.Explosions; +import com.jnhprojects.mcheli.weapon.ModDamageTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Everything a weapon fires: rounds, rockets, missiles, bombs and torpedoes. A port of + * {@code MCH_EntityBaseBullet}. + * + *

Projectiles fly on both sides, the client only for looks; hits, fuses and guidance are decided + * on the server. They are never saved: like the original, one found in a loaded chunk vanishes. + */ +public abstract class ProjectileEntity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TARGET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BOMBLET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.BOOLEAN); + /** Who fired, for clients: their rounds must not stop on their own vehicle. */ + private static final EntityDataAccessor DATA_SHOOTER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_USER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + + /** Beyond 5820 blocks from whoever fired it, a projectile is dropped. */ + private static final double MAX_RANGE_SQ = 3.38724E7; + + protected @Nullable Entity shooter; + protected @Nullable Entity user; + protected @Nullable Entity target; + private @Nullable WeaponDefinition weapon; + + public int explosionPower; + public int explosionPowerInWater; + public int power = 1; + public int piercing; + public int delayFuse; + public int sprinkleTime; + public double acceleration = 1.0; + public double accelerationFactor = 1.0; + private int age; + /** Client only: the round hit something here and waits, hidden, for the server to remove it. */ + private boolean spent; + + public Vec3 prevMotion = Vec3.ZERO; + /** Position two ticks ago, used to spread trail particles over the distance covered. */ + protected double xo2; + protected double yo2; + protected double zo2; + + protected ProjectileEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_TARGET, 0); + builder.define(DATA_BOMBLET, false); + builder.define(DATA_SHOOTER, 0); + builder.define(DATA_USER, 0); + } + + /** + * The second constructor of the original: place the projectile and point it along + * {@code (mx, my, mz)} at {@code acceleration} blocks per tick, capped at 3.9. + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float yaw, float pitch, double acceleration) { + snapTo(x, y, z, yaw, pitch); + xo2 = x; + yo2 = y; + zo2 = z; + double a = Math.min(acceleration, 3.9); + double d = Math.sqrt(mx * mx + my * my + mz * mz); + Vec3 motion = d > 0.0 ? new Vec3(mx * a / d, my * a / d, mz * a / d) : Vec3.ZERO; + setDeltaMovement(motion); + prevMotion = motion; + this.acceleration = a; + } + + // Weapon + + public void setWeapon(String name) { + WeaponDefinition def = MCHeli.content().weapon(name); + if (def == null) { + return; + } + weapon = def; + if (!level().isClientSide()) { + entityData.set(DATA_WEAPON, name); + } + if (def.bomblet > 0) { + sprinkleTime = def.bombletSTime; + } + piercing = def.piercing; + if (boostsAcceleration() && def.acceleration > 4.0F) { + accelerationFactor = def.acceleration / 4.0F; + } + } + + /** Rounds and rockets faster than 4 blocks a tick move several steps per tick instead. */ + protected boolean boostsAcceleration() { + return false; + } + + public @Nullable WeaponDefinition weapon() { + return weapon; + } + + /** Model drawn when the weapon names none, one of the original's {@code MCH_DefaultBulletModels}. */ + public abstract String defaultModel(); + + /** The model to draw: {@code ModelBomblet} for bomblets, else {@code ModelBullet}, else the default. */ + public String model() { + if (weapon == null) { + return defaultModel(); + } + String name = isBomblet() ? weapon.bombletModel : weapon.bulletModel; + return name.isEmpty() ? defaultModel() : name; + } + + public String weaponName() { + return entityData.get(DATA_WEAPON); + } + + /** {@code setParameterFromWeapon}: power and charges from the weapon file, and who to credit. */ + public void setParameters(WeaponDefinition def, @Nullable Entity shooter, @Nullable Entity user) { + explosionPower = def.explosion; + explosionPowerInWater = def.explosionInWater; + power = def.power; + piercing = def.piercing; + setOwners(shooter, user); + } + + /** The same, inherited from a parent projectile, for bomblets. */ + public void setParameters(ProjectileEntity parent) { + explosionPower = parent.explosionPower; + explosionPowerInWater = parent.explosionPowerInWater; + power = parent.power; + piercing = parent.piercing; + setOwners(parent.shooter, parent.user); + } + + public void setOwners(@Nullable Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + entityData.set(DATA_SHOOTER, shooter != null ? shooter.getId() : 0); + entityData.set(DATA_USER, user != null ? user.getId() : 0); + } + + public @Nullable Entity user() { + return user; + } + + public @Nullable Entity shooter() { + return shooter; + } + + public boolean isBomblet() { + return entityData.get(DATA_BOMBLET); + } + + public void setBomblet() { + sprinkleTime = 0; + entityData.set(DATA_BOMBLET, true); + } + + public void setTarget(@Nullable Entity entity) { + target = entity; + if (!level().isClientSide()) { + entityData.set(DATA_TARGET, entity != null ? entity.getId() : 0); + } + } + + public @Nullable Entity target() { + return target; + } + + /** A flare seduced this missile: it loses its target and is gone. */ + public void decoy() { + target = null; + discard(); + } + + public int age() { + return age; + } + + public float gravity() { + return weapon != null ? weapon.gravity : 0.0F; + } + + public float gravityInWater() { + return weapon != null ? weapon.gravityInWater : 0.0F; + } + + // Tick + + @Override + public void tick() { + if (spent) { + return; + } + if (level().isClientSide() && age == 0) { + int targetId = entityData.get(DATA_TARGET); + if (targetId > 0) { + target = level().getEntity(targetId); + } + } + Vec3 motion = getDeltaMovement(); + if (!motion.equals(prevMotion)) { + faceMotion(); + } + prevMotion = motion; + age++; + xo2 = xo; + yo2 = yo; + zo2 = zo; + super.tick(); + + if (weapon == null) { + if (age >= 2) { + discard(); + return; + } + setWeapon(weaponName()); + if (weapon == null) { + return; + } + } + + if (!level().isClientSide()) { + if (getY() <= level().getMaxY() && !level().isLoaded(blockPosition())) { + if (weapon.delayFuse <= 0) { + discard(); + return; + } + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + if (delayFuse > 0) { + delayFuse--; + if (delayFuse == 0) { + onTimeout(); + discard(); + return; + } + } + if (!isValid()) { + discard(); + return; + } + if (weapon.timeFuse > 0 && age > weapon.timeFuse) { + onTimeout(); + discard(); + return; + } + if (weapon.explosionAltitude > 0 && isGroundWithin(weapon.explosionAltitude)) { + impact(new Impact(position(), null, null, Direction.DOWN), 1.0F); + } + } + + motion = getDeltaMovement(); + setDeltaMovement(motion.x, motion.y + (isInWater() ? gravityInWater() : gravity()), motion.z); + if (!isRemoved()) { + checkCollisions(); + } + motion = getDeltaMovement().scale(accelerationFactor); + double nx = getX() + motion.x; + double ny = getY() + motion.y; + double nz = getZ() + motion.z; + if (level().isClientSide()) { + splash(nx, ny, nz); + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + level().addParticle(ParticleTypes.BUBBLE, nx - m.x * 0.25, ny - m.y * 0.25, nz - m.z * 0.25, m.x, m.y, m.z); + } + setPos(nx, ny, nz); + } + + /** Turns the nose along the flight path, the way every projectile class did after moving. */ + protected void faceMotion() { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + + /** {@code setMotion}: flies straight along {@code (dx, dy, dz)} at full speed. */ + protected void aimAlong(double dx, double dy, double dz) { + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d > 0.0) { + setDeltaMovement(dx * acceleration / d, dy * acceleration / d, dz * acceleration / d); + } + } + + /** {@code guidanceToTarget}: turns a seventh of the way towards the target each tick. */ + protected void guideTo(double tx, double ty, double tz) { + double dx = tx - getX(); + double dy = ty - getY(); + double dz = tz - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d <= 0.0) { + return; + } + Vec3 m = getDeltaMovement(); + setDeltaMovement((m.x * 6.0 + dx * acceleration / d) / 7.0, + (m.y * 6.0 + dy * acceleration / d) / 7.0, + (m.z * 6.0 + dz * acceleration / d) / 7.0); + faceMotion(); + } + + /** {@code checkValid}: whoever fired it still exists and is not too far away. */ + private boolean isValid() { + if (shooter == null && user == null) { + return false; + } + if (user != null && user.isRemoved()) { + return false; + } + Entity origin = user != null ? user : shooter; + double dx = getX() - origin.getX(); + double dz = getZ() - origin.getZ(); + return dx * dx + dz * dz < MAX_RANGE_SQ; + } + + private boolean isGroundWithin(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(Direction.DOWN); + if (!level().getBlockState(pos).isAir()) { + return true; + } + } + return false; + } + + /** Spawns {@code count} trail particles spread over the last two ticks of flight. */ + protected void trail(String name, int count, float size) { + if (!level().isClientSide() || name.isEmpty() || count < 1 || count > 50) { + return; + } + ParticleOptions particle = trailParticle(name); + double x = (getX() - xo) / count; + double y = (getY() - yo) / count; + double z = (getZ() - zo) / count; + double x2 = (xo - xo2) / count; + double y2 = (yo - yo2) / count; + double z2 = (zo - zo2) / count; + for (int i = 0; i < count; i++) { + double px = (xo + x * i + xo2 + x2 * i) / 2.0; + double py = (yo + y * i + yo2 + y2 * i) / 2.0; + double pz = (zo + z * i + zo2 + z2 * i) / 2.0; + level().addAlwaysVisibleParticle(particle, true, px, py, pz, 0.0, 0.0, 0.0); + } + } + + /** + * The original's own smoke particle ({@code explode}) was a large white puff; the cloud is the + * closest vanilla one. Other names are 1.7.10 particle names. + */ + private static ParticleOptions trailParticle(String name) { + return switch (name) { + case "explode", "cloud" -> ParticleTypes.CLOUD; + case "flame" -> ParticleTypes.FLAME; + case "largesmoke" -> ParticleTypes.LARGE_SMOKE; + case "heart" -> ParticleTypes.HEART; + case "fireworksspark" -> ParticleTypes.FIREWORK; + default -> ParticleTypes.SMOKE; + }; + } + + private void splash(double nx, double ny, double nz) { + if (weapon == null || weapon.power <= 0) { + return; + } + boolean wasDry = !level().getFluidState(BlockPos.containing(xo + 0.5, yo + 0.5, zo + 0.5)).is(FluidTags.WATER); + if (wasDry && level().getFluidState(BlockPos.containing(nx + 0.5, ny + 0.5, nz + 0.5)).is(FluidTags.WATER)) { + float pwr = Math.min(weapon.power, 20); + int n = random.nextInt(1 + (int) pwr / 3) + (int) pwr / 2 + 1; + for (int i = 0; i < n; i++) { + level().addParticle(ParticleTypes.SPLASH, nx, ny + 0.5, nz, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3, + pwr * 0.03 * (random.nextDouble() * 0.5 + 0.5) * 1.8, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3); + } + } + } + + /** {@code onUpdateTimeout}: the fuse ran out in mid air. */ + protected void onTimeout() { + if (!(level() instanceof ServerLevel server) || weapon == null) { + return; + } + if (isInWater()) { + if (explosionPowerInWater > 0) { + explode(server, getX(), getY(), getZ(), explosionPowerInWater, explosionPowerInWater, true); + } + } else if (explosionPower > 0) { + explode(server, getX(), getY(), getZ(), explosionPower, weapon.explosionBlock, false); + } else if (explosionPower < 0) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } + + /** {@code onUpdateBomblet}: once the sprinkle timer runs out the carrier splits up. */ + protected void tickBomblets() { + if (!level().isClientSide() && sprinkleTime > 0 && !isRemoved() && weapon != null) { + sprinkleTime--; + if (sprinkleTime == 0) { + for (int i = 0; i < weapon.bomblet; i++) { + sprinkleBomblet(); + } + discard(); + } + } + } + + protected void sprinkleBomblet() { + } + + /** Creates a copy of this projectile's kind for a bomblet, flying from here. */ + protected @Nullable T spawnChild(EntityType type, double mx, double my, double mz, float yaw, float pitch) { + T child = type.create(level(), EntitySpawnReason.TRIGGERED); + if (child == null) { + return null; + } + child.launch(getX(), getY(), getZ(), mx, my, mz, yaw, pitch, acceleration); + child.setWeapon(weaponName()); + child.setParameters(this); + return child; + } + + // Collision + + /** Where something was hit. {@code entity} is set for entity hits, {@code block} for block hits. */ + public record Impact(Vec3 location, @Nullable Entity entity, @Nullable BlockPos block, Direction side) { + } + + /** {@code onUpdateCollided}: the swept ray of this tick against blocks, then entities. */ + protected void checkCollisions() { + if (weapon == null) { + return; + } + Vec3 motion = getDeltaMovement().scale(accelerationFactor); + Vec3 from = position(); + Vec3 to = from.add(motion); + BlockHitResult blockHit = null; + for (int i = 0; i < 5; i++) { + blockHit = level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (blockHit.getType() != HitResult.Type.BLOCK) { + blockHit = null; + break; + } + BlockState state = level().getBlockState(blockHit.getBlockPos()); + if (user == null || level().isClientSide() || !Combat.isBulletBreakable(state)) { + break; + } + level().destroyBlock(blockHit.getBlockPos(), true, user); + } + + if (weapon.delayFuse > 0) { + if (blockHit != null) { + bounce(blockHit.getDirection()); + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + return; + } + + if (blockHit != null) { + to = blockHit.getLocation(); + } + Impact entityHit = findEntity(from, to, motion); + if (entityHit != null) { + // A vehicle applies the factor of the box struck itself, when it takes the damage. + impact(entityHit, 1.0F); + } else if (blockHit != null) { + impact(new Impact(blockHit.getLocation(), null, blockHit.getBlockPos(), blockHit.getDirection()), 1.0F); + } + } + + private @Nullable Impact findEntity(Vec3 from, Vec3 to, Vec3 motion) { + AABB area = getBoundingBox().expandTowards(motion).inflate(21.0); + Entity best = null; + Vec3 bestAt = null; + double bestDistance = 0.0; + List clipped = new ArrayList<>(); + for (Entity candidate : level().getEntities(this, area, this::canHit)) { + Optional at; + if (candidate instanceof AircraftEntity vehicle) { + at = vehicle.clipHitBoxes(from, to, 0.3); + clipped.add(vehicle); + } else { + at = candidate.getBoundingBox().inflate(0.3).clip(from, to); + } + if (at.isPresent()) { + double d = from.distanceTo(at.get()); + if (best == null || d < bestDistance) { + best = candidate; + bestAt = at.get(); + bestDistance = d; + } + } + } + // Only the vehicle actually hit keeps the factor of the box the ray struck. + for (AircraftEntity vehicle : clipped) { + if (vehicle != best) { + vehicle.consumeHitBoxFactor(); + } + } + return best != null ? new Impact(bestAt, best, null, Direction.UP) : null; + } + + /** Clients only learn who fired through synched data; look them up once it has arrived. */ + private void resolveOwners() { + if (shooter == null && entityData.get(DATA_SHOOTER) > 0) { + shooter = level().getEntity(entityData.get(DATA_SHOOTER)); + } + if (user == null && entityData.get(DATA_USER) > 0) { + user = level().getEntity(entityData.get(DATA_USER)); + } + } + + /** {@code canBeCollidedEntity}: never the shooter, its crew, or rounds fired by the same side. */ + public boolean canHit(Entity entity) { + if (level().isClientSide()) { + resolveOwners(); + } + if (!entity.isPickable() || entity == user || entity == shooter) { + return false; + } + if (entity instanceof ProjectileEntity other) { + if (level().isClientSide()) { + return false; + } + if (other.shooter != null && other.shooter == shooter || other.user != null && other.user == user) { + return false; + } + } + return !(shooter instanceof AircraftEntity vehicle) || !vehicle.hasPassenger(entity); + } + + /** {@code boundBullet}: a delayed fuse round bounces off what it hits. */ + private void bounce(Direction side) { + Vec3 m = getDeltaMovement(); + float bound = weapon != null ? weapon.bound : 0.0F; + switch (side) { + case DOWN -> { + if (m.y > 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case UP -> { + if (m.y < 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case NORTH -> { + if (m.z > 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case SOUTH -> { + if (m.z < 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case WEST -> { + if (m.x > 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + case EAST -> { + if (m.x < 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + } + } + + /** {@code onImpact}: damage what was hit, then explode or pierce through. */ + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + if (weapon != null && (weapon.explosion == 0 || weapon.modeNum >= 2) && hit.block() != null) { + debris(hit); + } + if (piercing <= 0) { + // The server decides what the hit does; until its removal arrives the round stays + // where it struck instead of flying on through the hull. + spent = true; + setPos(hit.location()); + setDeltaMovement(Vec3.ZERO); + } + return; + } + if (weapon == null) { + discard(); + return; + } + if (hit.entity() != null) { + hitEntity(hit.entity(), damageFactor); + piercing = 0; + } + float expPower = explosionPower * damageFactor; + float expPowerInWater = explosionPowerInWater * damageFactor; + Vec3 at = hit.location(); + if (piercing > 0) { + piercing--; + if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, 1.0F, 1.0F, false); + } + return; + } + if (expPowerInWater == 0.0F) { + if (weapon.fae) { + explodeFae(server, expPower); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } else if (hit.entity() != null) { + if (isInWater()) { + explode(server, at.x, at.y, at.z, expPowerInWater, expPowerInWater, true); + } else { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } + } else if (isInWater() || hit.block() != null && isWaterAround(hit.block())) { + BlockPos b = hit.block() != null ? hit.block() : blockPosition(); + explode(server, b.getX(), b.getY(), b.getZ(), expPowerInWater, expPowerInWater, true); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + discard(); + } + + /** {@code MCH_Lib.isBlockInWater}: water on any side of the block. */ + private boolean isWaterAround(BlockPos pos) { + for (Direction d : Direction.values()) { + if (level().getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return level().getFluidState(pos).is(FluidTags.WATER); + } + + /** Client side chips flying off the block a round struck. */ + private void debris(Impact hit) { + if (weapon == null || hit.block() == null) { + return; + } + BlockState state = level().getBlockState(hit.block()); + if (state.isAir()) { + return; + } + float p = weapon.power; + Vec3 m = getDeltaMovement(); + var particle = new BlockParticleOption(ParticleTypes.BLOCK, state); + for (int i = 0; i < p / 3.0F; i++) { + level().addParticle(particle, + hit.location().x + (random.nextFloat() - 0.5) * p / 10.0, + hit.location().y + 0.1, + hit.location().z + (random.nextFloat() - 0.5) * p / 10.0, + -m.x * p / 2.0, p / 2.0F, -m.z * p / 2.0); + } + } + + /** {@code onImpactEntity}. */ + protected void hitEntity(Entity entity, float damageFactor) { + if (!entity.isRemoved()) { + DamageSource source = ModDamageTypes.bullet(level(), this, user); + Combat.hurt(entity, source, power * damageFactor, weapon); + } + confirmHit(); + } + + /** {@code notifyHitBullet}: the shooter hears that a round connected. */ + protected void confirmHit() { + if (user instanceof ServerPlayer player) { + ModSounds.playTo(player, "hit", 1.0F, 1.0F); + } + } + + protected void explode(ServerLevel server, double x, double y, double z, float size, float blockSize, boolean inWater) { + if (weapon == null) { + return; + } + boolean destroyBlocks = !isBomblet() || random.nextInt(3) == 0; + Explosions.Params p = new Explosions.Params(this, user, size, blockSize, weapon.flaming, destroyBlocks, 0, inWater, weapon); + if (Explosions.explode(server, x, y, z, p)) { + confirmHit(); + } + } + + /** Fuel air explosives burn whatever they catch. */ + private void explodeFae(ServerLevel server, float size) { + Explosions.Params p = new Explosions.Params(this, user, size, weapon.explosionBlock, weapon.flaming, true, 15, false, weapon); + if (Explosions.explode(server, getX(), getY(), getZ(), p)) { + confirmHit(); + } + } + + // Entity plumbing + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + // Something thrown at a projectile sets it off, as in the original. + if (damage > 0.0F && source.is(DamageTypes.THROWN) && !isRemoved()) { + impact(new Impact(position().add(0.5, 0.5, 0.5), null, blockPosition(), Direction.UP), 1.0F); + return true; + } + return false; + } + + /** Client only: whether the round already hit something and should no longer be drawn. */ + public boolean isSpent() { + return spent; + } + + @Override + public boolean isPickable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putString("WeaponName", weaponName()); + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From bad25644b8df10eab3682262bfd6cd93cd7e107d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:13 +0800 Subject: [PATCH 0751/1461] Add entity/projectile/RocketEntity.java to the 1.21.11 shared sources --- .../entity/projectile/RocketEntity.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java new file mode 100644 index 00000000..c39fa6fe --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** An unguided rocket, {@code MCH_EntityRocket}, which may burst into bomblets. */ +public class RocketEntity extends ProjectileEntity { + public RocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return !isBomblet(); + } + + @Override + public void tick() { + super.tick(); + tickBomblets(); + WeaponDefinition w = weapon(); + if (!isBomblet() && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + RocketEntity child = spawnChild(ModEntities.ROCKET.get(), m.x, m.y, m.z, getYRot(), getXRot()); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(child.getDeltaMovement().add( + (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread)); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From b4bc7b5a417bcf3e46bb12db8fc28124b1082816 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:14 +0800 Subject: [PATCH 0752/1461] Add entity/projectile/ThrowableEntity.java to the 1.21.11 shared sources --- .../entity/projectile/ThrowableEntity.java | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java new file mode 100644 index 00000000..479ead5a --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java @@ -0,0 +1,200 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.Explosions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ARGB; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A thrown grenade, {@code MCH_EntityThrowable}. It flies under its definition's gravity, bounces + * off blocks losing speed, and when its fuse runs out either explodes or, for a smoke grenade, + * pours out coloured smoke until it disappears. + */ +public class ThrowableEntity extends Entity { + private static final EntityDataAccessor DATA_NAME = SynchedEntityData.defineId(ThrowableEntity.class, EntityDataSerializers.STRING); + + private final InterpolationHandler interpolation = new InterpolationHandler(this, 3); + private @Nullable Entity thrower; + private @Nullable ThrowableDefinition definition; + private int definitionGeneration = -1; + + public ThrowableEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Throws {@code def} from {@code thrower}'s eyes the way it looks. + * + * @param strength 0.2 for a flick up to 1.4 for a full draw, the original's acceleration + */ + public static ThrowableEntity thrown(Level level, Entity thrower, ThrowableDefinition def, float strength) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + Vec3 look = thrower.getLookAngle(); + entity.setPos(thrower.getX() + look.x * 0.3, thrower.getEyeY() - 0.1, thrower.getZ() + look.z * 0.3); + entity.setYRot(thrower.getYRot()); + entity.setDeltaMovement(look.scale(1.5 * strength).add(thrower.getDeltaMovement())); + entity.thrower = thrower; + entity.setDefinition(def); + return entity; + } + + /** One fired from a dispenser: no thrower, straight along {@code direction}. */ + public static ThrowableEntity dispensed(Level level, Vec3 at, Vec3 direction, ThrowableDefinition def) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + entity.setPos(at); + entity.setDeltaMovement(direction.normalize().scale(def.dispenseAcceleration)); + entity.setDefinition(def); + return entity; + } + + private void setDefinition(ThrowableDefinition def) { + entityData.set(DATA_NAME, def.name()); + definition = def; + definitionGeneration = MCHeli.content().generation(); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_NAME, ""); + } + + /** The definition by name, looked up again after a content reload. */ + public @Nullable ThrowableDefinition definition() { + if (definitionGeneration != MCHeli.content().generation()) { + definition = MCHeli.content().throwable(entityData.get(DATA_NAME)); + definitionGeneration = MCHeli.content().generation(); + } + return definition; + } + + public String definitionName() { + return entityData.get(DATA_NAME); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + ThrowableDefinition def = definition(); + if (def == null) { + // The original gave a missing definition ten ticks to arrive. + if (tickCount > 10 && !level().isClientSide()) { + discard(); + } + return; + } + if (level().isClientSide()) { + if (def.smokes() && tickCount >= def.timeFuse) { + smoke(def); + } + return; + } + if (tickCount >= def.timeFuse && !def.smokes()) { + Explosions.explode((ServerLevel) level(), getX(), getY(), getZ(), new Explosions.Params(this, thrower, + def.explosion, def.explosion, false, true, 0, isInWater(), null)); + discard(); + return; + } + if (tickCount >= def.aliveTime) { + discard(); + return; + } + fly(def); + } + + /** {@code onUpdate} and {@code boundBullet}: gravity of its own, and a bounce off whatever it hits. */ + private void fly(ThrowableDefinition def) { + boolean water = isInWater(); + Vec3 m = getDeltaMovement().add(0.0, water ? def.gravityInWater : def.gravity, 0.0); + Vec3 from = position(); + move(MoverType.SELF, m); + // Whichever axis came up short hit something. + Vec3 moved = position().subtract(from); + double mx = blocked(moved.x, m.x) ? -m.x * def.bound : m.x; + double my = m.y; + double mz = blocked(moved.z, m.z) ? -m.z * def.bound : m.z; + if (blocked(moved.y, m.y)) { + mx *= 0.9; + mz *= 0.9; + // Off the floor or the ceiling it keeps a share of its speed, unless that would be a crawl. + my = Math.abs(my) > 0.05 ? -my * def.bound : 0.0; + } + double drag = water ? 0.8 : 0.99; + setDeltaMovement(mx * drag, my * drag, mz * drag); + } + + private static boolean blocked(double moved, double wanted) { + return Math.abs(moved) < Math.abs(wanted) - 1.0E-6; + } + + private void smoke(ThrowableDefinition def) { + int color = def.smokeColor; + for (int i = 0; i < def.smokeNum; i++) { + // A little noise per puff, keeping greys grey. + float r = ARGB.redFloat(color) * 0.9F + random.nextFloat() * 0.1F; + float g = ARGB.greenFloat(color) * 0.9F + random.nextFloat() * 0.1F; + float b = ARGB.blueFloat(color) * 0.9F + random.nextFloat() * 0.1F; + if (ARGB.red(color) == ARGB.green(color)) { + g = r; + } + if (ARGB.red(color) == ARGB.blue(color)) { + b = r; + } + if (ARGB.green(color) == ARGB.blue(color)) { + b = g; + } + float size = def.smokeSize + random.nextFloat() * def.smokeSize / 3.0F; + level().addAlwaysVisibleParticle(new SmokeParticleOptions(ARGB.colorFromFloat(1.0F, r, g, b) & 0xFFFFFF, size), true, + getX(), getY() + 0.5, getZ(), + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2, + def.smokeVelocityVertical * random.nextFloat() * 0.2, + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2); + } + } + + /** The server moves it; clients ease towards each position it sends. */ + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From 4f313640af4025f90d59d0c556f7249d96340546 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:14 +0800 Subject: [PATCH 0753/1461] Add entity/projectile/TorpedoEntity.java to the 1.21.11 shared sources --- .../entity/projectile/TorpedoEntity.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java new file mode 100644 index 00000000..a89f7c01 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java @@ -0,0 +1,56 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A torpedo, {@code MCH_EntityTorpedo}. Once in the water it settles on its running speed and + * either holds course or, when guided, runs to the point the gunner aimed at. + */ +public class TorpedoEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + public double accelerationInWater = 2.0; + + public TorpedoEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + boolean guided = w != null && w.guidedTorpedo; + if (!level().isClientSide() && isInWater()) { + if (acceleration < accelerationInWater) { + acceleration += 0.1; + } else if (acceleration > accelerationInWater + 0.2F) { + acceleration -= 0.1; + } + if (guided) { + aimAlong(targetX - getX(), targetY - getY(), targetZ - getZ()); + } else { + Vec3 m = getDeltaMovement(); + aimAlong(m.x, m.y * 0.8F, m.z); + } + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + if (guided) { + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + } + + @Override + public String defaultModel() { + return "torpedo"; + } +} From d215f2e7fc3509cb225db527b20bb0c1e807df39 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:14 +0800 Subject: [PATCH 0754/1461] Add entity/projectile/TvMissileEntity.java to the 1.21.11 shared sources --- .../entity/projectile/TvMissileEntity.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java new file mode 100644 index 00000000..a06fef1a --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A wire guided missile flown by the gunner's view, {@code MCH_EntityTvMissile}. It steers while + * it is the vehicle's active TV missile; once another is fired it carries on straight. + */ +public class TvMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 1440000.0; + + public TvMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + Entity aimer = user(); + if (aimer != null) { + if (distanceToSqr(aimer) > MAX_DISTANCE_SQ) { + discard(); + } + if (!level().isClientSide() && !isRemoved()) { + steer(aimer); + } + } else if (!level().isClientSide()) { + discard(); + } + } + + private void steer(Entity aimer) { + if (aimer.isRemoved() || !(shooter() instanceof AircraftEntity vehicle) || vehicle.tvMissile() != this) { + return; + } + Vec3 dir = Vec3.directionFromRotation(aimer.getXRot(), aimer.getYRot()); + aimAlong(dir.x, dir.y, dir.z); + setYRot(aimer.getYRot()); + setXRot(aimer.getXRot()); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From beb9a4fe9480923edaabadf6f6644ee86b0dfcef Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:15 +0800 Subject: [PATCH 0755/1461] Add item/AircraftItem.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/item/AircraftItem.java | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java new file mode 100644 index 00000000..cb0312c4 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java @@ -0,0 +1,146 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.ChatFormatting; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.component.TooltipDisplay; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Places a vehicle, the same way a boat is placed. + * + *

There is one item per vehicle kind ({@code mcheli:helicopter}, {@code mcheli:plane} and so + * on). Which vehicle a stack places is stored in its {@link ModDataComponents#VEHICLE} component, + * and its look comes from the {@code item_model} component pointing at the model the import wrote + * for that vehicle. Items are registered once at startup, so this is what lets definitions be + * added or reloaded while the game runs. + */ +public class AircraftItem extends Item { + private final String kind; + + public AircraftItem(String kind, Properties properties) { + super(properties); + this.kind = kind; + } + + /** The vehicle kind this item holds, such as {@code helicopter}. */ + public String kind() { + return kind; + } + + /** + * A stack that places {@code def}. Vehicles from the import show the item model the import + * wrote for them; item models only load with the resources, so pack vehicles keep their kind's + * own model. + */ + public static ItemStack stackOf(AircraftDefinition def) { + ItemStack stack = new ItemStack(ModItems.forKind(def.kind())); + stack.set(ModDataComponents.VEHICLE.get(), def.name()); + if (def.pack().equals(ContentSource.ORIGINAL)) { + stack.set(DataComponents.ITEM_MODEL, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, def.name())); + } + return stack; + } + + /** The definition name a stack places, or {@code null} if it is not a vehicle item or has none. */ + public static @Nullable String vehicleOf(ItemStack stack) { + return stack.getItem() instanceof AircraftItem item ? item.definitionName(stack) : null; + } + + protected @Nullable String definitionName(ItemStack stack) { + return stack.get(ModDataComponents.VEHICLE.get()); + } + + @Override + public Component getName(ItemStack stack) { + String name = definitionName(stack); + if (name == null) { + return super.getName(stack); + } + AircraftDefinition def = MCHeli.content().any(name); + // Vehicles added at runtime may bring no lang entry; their DisplayName stands in. + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + /** Who made a vehicle from a content pack. The import's vehicles have their credits in the docs. */ + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display, Consumer out, TooltipFlag flag) { + super.appendHoverText(stack, context, display, out, flag); + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + PackManifest credit = def != null ? def.credit() : null; + if (credit != null) { + out.accept((credit.author().isEmpty() + ? Component.translatable("item.mcheli.vehicle.from_pack", credit.name()) + : Component.translatable("item.mcheli.vehicle.from_pack_by", credit.name(), credit.author())) + .withStyle(ChatFormatting.GRAY)); + } + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + HitResult hit = getPlayerPOVHitResult(level, player, ClipContext.Fluid.ANY); + if (!(hit instanceof BlockHitResult blockHit) || hit.getType() != HitResult.Type.BLOCK) { + return InteractionResult.PASS; + } + + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + if (!level.isClientSide()) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.unknown", String.valueOf(name)), true); + } + return InteractionResult.FAIL; + } + if (!level.isClientSide() && ServerConfig.isDisabled(def.name())) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName()), true); + return InteractionResult.FAIL; + } + + AircraftEntity vehicle = ModEntities.typeFor(def).create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (vehicle == null) { + return InteractionResult.FAIL; + } + Vec3 at = blockHit.getLocation(); + vehicle.setType(def); + vehicle.snapTo(at.x, at.y, at.z, player.getYRot(), 0.0F); + if (!level.noCollision(vehicle, vehicle.getBoundingBox())) { + return InteractionResult.FAIL; + } + + if (!level.isClientSide()) { + level.addFreshEntity(vehicle); + level.gameEvent(player, GameEvent.ENTITY_PLACE, at); + stack.consume(1, player); + } + player.awardStat(Stats.ITEM_USED.get(this)); + return InteractionResult.SUCCESS; + } +} From df2e308cec3a821f24858b22fc0062d122fbd4d3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:15 +0800 Subject: [PATCH 0756/1461] Add item/ChainItem.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/item/ChainItem.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java new file mode 100644 index 00000000..0f89341b --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java @@ -0,0 +1,85 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.ChainEntity; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; + +import java.util.Map; +import java.util.UUID; +import java.util.WeakHashMap; + +/** + * The tow chain, {@code MCH_ItemChain}. Use it on both ends, in either order, two to sixteen + * blocks apart: the vehicle pulls, so one end must be a vehicle. Use it on either end of a chain + * to take the chain off, or on the hooked end again to let go of it. Vehicles and containers + * hand the click over here, and every step is told to the player above the hotbar. + */ +public class ChainItem extends Item { + private static final double MIN_DISTANCE = 2.0; + private static final double MAX_DISTANCE = 16.0; + + /** The first end each player has hooked, until they hook the second. Server only. */ + private static final Map HOOKED = new WeakHashMap<>(); + + public ChainItem(Properties properties) { + super(properties.stacksTo(1)); + } + + /** A click with the chain on {@code target}. */ + public static InteractionResult useOn(Entity target, Player player) { + if (!(player.level() instanceof ServerLevel level)) { + return InteractionResult.SUCCESS; + } + ChainEntity existing = ChainEntity.on(target); + if (existing != null) { + MCHeli.LOGGER.debug("Chain: {} takes the chain off {}", player.getName().getString(), target); + existing.disconnect(); + HOOKED.remove(player); + player.displayClientMessage(Component.translatable("message.mcheli.chain.removed"), true); + return InteractionResult.SUCCESS; + } + UUID firstId = HOOKED.get(player); + Entity first = firstId != null ? level.getEntity(firstId) : null; + // An end hooked far away was most likely forgotten: start over from this one. + if (first == null || first.isRemoved() || target.distanceTo(first) > MAX_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} hooks {}", player.getName().getString(), target); + HOOKED.put(player, target.getUUID()); + ModSounds.playAt(target, "chain", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.hooked", target.getDisplayName()), true); + return InteractionResult.SUCCESS; + } + if (first == target) { + MCHeli.LOGGER.debug("Chain: {} lets go of {}", player.getName().getString(), target); + HOOKED.remove(player); + player.displayClientMessage(Component.translatable("message.mcheli.chain.released"), true); + return InteractionResult.SUCCESS; + } + double distance = target.distanceTo(first); + if (distance < MIN_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} and {} are only {} blocks apart", first, target, distance); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.too_close", String.format("%.1f", distance)), true); + return InteractionResult.FAIL; + } + if (!ChainEntity.canTow(first) && !ChainEntity.canTow(target)) { + MCHeli.LOGGER.debug("Chain: neither {} nor {} can tow", first, target); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.no_vehicle"), true); + return InteractionResult.FAIL; + } + MCHeli.LOGGER.debug("Chain: {} chains {} and {}", player.getName().getString(), first, target); + ChainEntity chain = ChainEntity.connect(level, first, target); + HOOKED.remove(player); + Entity towed = chain.towed(); + Entity tow = chain.tow(); + player.displayClientMessage(Component.translatable("message.mcheli.chain.connected", + towed != null ? towed.getDisplayName() : first.getDisplayName(), tow != null ? tow.getDisplayName() : target.getDisplayName()), true); + return InteractionResult.SUCCESS; + } +} From bfbf35605f3244b44e7696377051731454b9f489 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:16 +0800 Subject: [PATCH 0757/1461] Add item/FuelItem.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/item/FuelItem.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java new file mode 100644 index 00000000..efa8f295 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +/** + * The fuel can, {@code MCH_ItemFuel}. It holds 600 units; the durability bar shows how much is + * left. Using it refills it from the coal and charcoal in the inventory, as in the original. + * + *

A can in a fuel slot of the vehicle screen empties into the tank while the vehicle stands on + * something, as in the original. Not in the original: right-clicking a vehicle with the can pours + * up to 100 units at once, which saves opening the screen. + */ +public class FuelItem extends Item { + /** Units a full can holds, the original's maximum damage. */ + public static final int CAPACITY = 600; + /** Units poured into a vehicle per click: what the original moved per fuel slot per update. */ + private static final int POUR = 100; + private static final int PER_COAL = 100; + private static final int PER_CHARCOAL = 75; + + public FuelItem(Properties properties) { + super(properties.durability(CAPACITY)); + } + + public static int contents(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + /** Fills a vehicle's tank from the can. Server only. */ + public static InteractionResult pour(ItemStack stack, AircraftEntity vehicle, Player player) { + int added = vehicle.refuel(player.getAbilities().instabuild ? POUR : Math.min(contents(stack), POUR)); + if (added <= 0) { + return InteractionResult.FAIL; + } + if (!player.getAbilities().instabuild) { + stack.setDamageValue(stack.getDamageValue() + added); + } + vehicle.level().playSound(null, vehicle.getX(), vehicle.getY(), vehicle.getZ(), SoundEvents.BUCKET_EMPTY, SoundSource.NEUTRAL, 1.0F, 1.0F); + return InteractionResult.SUCCESS; + } + + /** Charcoal first, then coal, each one worth part of the can, until it is full. */ + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + if (!stack.isDamaged() || player.getAbilities().instabuild) { + return InteractionResult.PASS; + } + if (!level.isClientSide()) { + refill(stack, player.getInventory(), Items.CHARCOAL, PER_CHARCOAL); + refill(stack, player.getInventory(), Items.COAL, PER_COAL); + } + return InteractionResult.SUCCESS; + } + + private static void refill(ItemStack can, Inventory inventory, Item coal, int worth) { + for (int slot = 0; slot < inventory.getContainerSize() && can.isDamaged(); slot++) { + ItemStack item = inventory.getItem(slot); + while (item.is(coal) && !item.isEmpty() && can.isDamaged()) { + can.setDamageValue(Math.max(can.getDamageValue() - worth, 0)); + item.shrink(1); + } + } + } +} From 9697391c9aa479624bd2e86185bd4b5db053e2a4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:16 +0800 Subject: [PATCH 0758/1461] Add item/LauncherItem.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/item/LauncherItem.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java new file mode 100644 index 00000000..a83ff5bd --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; + +import java.util.function.Supplier; + +/** + * A shoulder launched missile, {@code MCH_ItemLightWeaponBase}: the FIM-92 Stinger and the + * FGM-148 Javelin. Held up it is a sight that locks onto targets; the attack key fires once + * locked. It holds one missile, and its bar shows whether it is loaded: an empty launcher reloads + * from a missile in the inventory after two seconds held but not raised. + */ +public class LauncherItem extends Item { + /** The original's durability: 0 is loaded, the maximum is empty. */ + private static final int EMPTY = 10; + + private final String weapon; + private final Supplier missile; + + /** + * @param weapon the weapon definition it fires, such as {@code fim92} + * @param missile the item it reloads from + */ + public LauncherItem(String weapon, Supplier missile, Properties properties) { + super(properties.durability(EMPTY)); + this.weapon = weapon; + this.missile = missile; + } + + public String weapon() { + return weapon; + } + + public Item missile() { + return missile.get(); + } + + public static boolean isLoaded(ItemStack stack) { + return stack.getDamageValue() < stack.getMaxDamage(); + } + + public static void setLoaded(ItemStack stack, boolean loaded) { + stack.setDamageValue(loaded ? 0 : stack.getMaxDamage()); + } + + /** Not from a vehicle seat or the GLTD, as in the original. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity) && !(player.getVehicle() instanceof GltdEntity); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResult.PASS; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.BOW; + } +} From 0110b5d165993f3270c3e14ef0de2f4428104c13 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:17 +0800 Subject: [PATCH 0759/1461] Add item/LegacyAircraftItem.java to the 1.21.11 shared sources --- .../mcheli/item/LegacyAircraftItem.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java new file mode 100644 index 00000000..0a286838 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * A vehicle item as 1.0.0 saved it: one item per vehicle, such as {@code mcheli:ah-64}. + * + *

These are still registered for the vehicles imported at startup, so that stacks in 1.0.0 + * worlds load instead of vanishing. They work as before, are not listed anywhere, and turn into + * the generic item of their kind ({@link AircraftItem#stackOf}) as soon as they sit in a player's + * inventory. Stacks in chests convert once a player takes them out. + */ +public final class LegacyAircraftItem extends AircraftItem { + private final String definitionName; + + public LegacyAircraftItem(String kind, String definitionName, Properties properties) { + super(kind, properties); + this.definitionName = definitionName; + } + + @Override + protected @Nullable String definitionName(ItemStack stack) { + return definitionName; + } + + @Override + public void inventoryTick(ItemStack stack, ServerLevel level, Entity owner, @Nullable EquipmentSlot slot) { + if (!(owner instanceof Player player)) { + return; + } + AircraftDefinition def = MCHeli.content().any(definitionName); + if (def == null) { + return; + } + Inventory inventory = player.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + if (inventory.getItem(i) == stack) { + inventory.setItem(i, convert(stack, def)); + return; + } + } + } + + /** The generic stack for {@code def}, keeping count, custom name and anything else set on the old one. */ + static ItemStack convert(ItemStack legacy, AircraftDefinition def) { + ItemStack converted = AircraftItem.stackOf(def); + converted.applyComponents(legacy.getComponentsPatch()); + converted.set(ModDataComponents.VEHICLE.get(), def.name()); + converted.setCount(legacy.getCount()); + return converted; + } +} From 3f162864e14e70fe1f85ed49529dd9927f6db41c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:17 +0800 Subject: [PATCH 0760/1461] Add item/ParachuteItem.java to the 1.21.11 shared sources --- .../mcheli/item/ParachuteItem.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java new file mode 100644 index 00000000..2af7b79b --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; + +/** The parachute, {@code MCH_ItemParachute}: used in mid-air, it opens above the player. */ +public class ParachuteItem extends Item { + public ParachuteItem(Properties properties) { + super(properties.stacksTo(1)); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (player.isPassenger() || player.onGround()) { + return InteractionResult.PASS; + } + if (!level.isClientSide()) { + ParachuteEntity.open(level, player, ParachuteEntity.Kind.ITEM); + player.getItemInHand(hand).consume(1, player); + } + return InteractionResult.SUCCESS; + } +} From 832ce7b55a025744979e4f7e6d1da55e97745326 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:18 +0800 Subject: [PATCH 0761/1461] Add item/PlacedEntityItem.java to the 1.21.11 shared sources --- .../mcheli/item/PlacedEntityItem.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java new file mode 100644 index 00000000..34c524d5 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java @@ -0,0 +1,62 @@ +package com.jnhprojects.mcheli.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; + +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Sets up a piece of equipment that lives in the world as an entity: the GLTD, the container, the + * UAV station. Like the originals' items it goes on top of the block clicked, facing the way the + * player looks, and only where it fits. + */ +public class PlacedEntityItem extends Item { + private final Supplier> type; + + public PlacedEntityItem(Supplier> type, Properties properties) { + super(properties); + this.type = type; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + Player player = context.getPlayer(); + BlockPos above = context.getClickedPos().above(); + Entity entity = type.get().create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (entity == null) { + return InteractionResult.FAIL; + } + entity.snapTo(above.getX() + 0.5, above.getY(), above.getZ() + 0.5, player != null ? player.getYRot() : 0.0F, 0.0F); + prepare(entity, player); + if (!level.noCollision(entity, entity.getBoundingBox().deflate(0.1)) || !level.getEntities(entity, entity.getBoundingBox()).isEmpty()) { + return InteractionResult.FAIL; + } + if (!level.isClientSide()) { + level.addFreshEntity(entity); + level.gameEvent(player, GameEvent.ENTITY_PLACE, entity.position()); + } + ItemStack stack = context.getItemInHand(); + if (player != null) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, player); + return InteractionResult.SUCCESS; + } + + /** Sets up the new entity before it is checked for room and spawned. */ + protected void prepare(Entity entity, @Nullable Player player) { + } +} From 2008c8f8903b5399cf93d2580b191ecb60f45011 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:18 +0800 Subject: [PATCH 0762/1461] Add item/RangefinderItem.java to the 1.21.11 shared sources --- .../mcheli/item/RangefinderItem.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java new file mode 100644 index 00000000..6a4fb996 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; + +/** + * The laser rangefinder, {@code MCH_ItemRangeFinder}. Held up to the eye it is a scope that reads + * the range to whatever it points at; the attack key spots targets for the team, using one of + * its ten charges. Crafting it with redstone charges it again. + */ +public class RangefinderItem extends Item { + public static final int CHARGES = 10; + + public RangefinderItem(Properties properties) { + super(properties.durability(CHARGES)); + } + + /** Not from a vehicle seat: the vehicle's own sight does that job. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity); + } + + /** Charges left, from none to {@link #CHARGES}. */ + public static int charges(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResult.PASS; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.SPYGLASS; + } +} From b13cfd5e66d845851e06bf1c4ec5bc4d6400171d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:19 +0800 Subject: [PATCH 0763/1461] Add item/ThrowableItem.java to the 1.21.11 shared sources --- .../mcheli/item/ThrowableItem.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java new file mode 100644 index 00000000..df8f33f8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java @@ -0,0 +1,123 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.Direction; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.dispenser.BlockSource; +import net.minecraft.core.dispenser.DefaultDispenseItemBehavior; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Hand grenades, {@code MCH_ItemThrowable}. Like the vehicles there is one item, and the + * {@link ModDataComponents#THROWABLE} component says which definition a stack throws, so pack + * throwables need no item of their own. Held like a bow: the longer the draw, the further it goes. + */ +public class ThrowableItem extends Item { + private static final int FULL_DRAW = 35; + + public ThrowableItem(Properties properties) { + super(properties); + } + + /** A stack of {@code def}, showing the item model the import wrote for it. */ + public static ItemStack stackOf(ThrowableDefinition def) { + ItemStack stack = new ItemStack(ModItems.THROWABLE.get()); + stack.set(ModDataComponents.THROWABLE.get(), def.name()); + stack.set(DataComponents.MAX_STACK_SIZE, def.stackSize); + if (def.pack().equals(ContentSource.ORIGINAL)) { + stack.set(DataComponents.ITEM_MODEL, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, def.name())); + } + return stack; + } + + private static @Nullable ThrowableDefinition definition(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + return name != null ? MCHeli.content().throwable(name) : null; + } + + @Override + public Component getName(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + if (name == null) { + return super.getName(stack); + } + ThrowableDefinition def = MCHeli.content().throwable(name); + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (definition(player.getItemInHand(hand)) == null) { + return InteractionResult.FAIL; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.BOW; + } + + @Override + public boolean releaseUsing(ItemStack stack, Level level, LivingEntity entity, int remainingTime) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return false; + } + int drawn = Math.max(getUseDuration(stack, entity) - remainingTime, 5); + float strength = drawn <= FULL_DRAW ? drawn / 25.0F : 1.0F; + level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), SoundEvents.ARROW_SHOOT, SoundSource.PLAYERS, + 0.5F, 0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)); + if (!level.isClientSide()) { + level.addFreshEntity(ThrowableEntity.thrown(level, entity, def, strength)); + } + if (entity instanceof Player player) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, entity); + return true; + } + + /** Dispensers throw them straight out, {@code MCH_ItemThrowableDispenseBehavior}. */ + public static class DispenseBehavior extends DefaultDispenseItemBehavior { + @Override + protected ItemStack execute(BlockSource source, ItemStack stack) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return super.execute(source, stack); + } + Direction facing = source.state().getValue(DispenserBlock.FACING); + Vec3 at = source.center().add(facing.getStepX() * 0.7, facing.getStepY() * 0.7, facing.getStepZ() * 0.7); + Vec3 direction = new Vec3(facing.getStepX(), facing.getStepY() + 0.1, facing.getStepZ()); + source.level().addFreshEntity(ThrowableEntity.dispensed(source.level(), at, direction, def)); + stack.shrink(1); + return stack; + } + } +} From 7bf4f44a8f43c9bc724a19d178a280980bb55032 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:19 +0800 Subject: [PATCH 0764/1461] Add item/UavStationItem.java to the 1.21.11 shared sources --- .../mcheli/item/UavStationItem.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java new file mode 100644 index 00000000..7e3bc1c8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java @@ -0,0 +1,33 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station and the portable UAV controller, {@code MCH_ItemUavStation}. Both place the same + * entity; the kind decides its model and which drones it launches. + */ +public class UavStationItem extends PlacedEntityItem { + private final int kind; + + public UavStationItem(int kind, Properties properties) { + super(ModEntities.UAV_STATION, properties); + this.kind = kind; + } + + /** Squared to the nearest quarter turn, facing the player, as the original placed it. */ + @Override + protected void prepare(Entity entity, @Nullable Player player) { + if (entity instanceof UavStationEntity station) { + station.setKind(kind); + float yaw = player != null ? player.getYRot() : 0.0F; + int quarter = Math.floorMod(Math.round(Mth.wrapDegrees(yaw) / 90.0F), 4); + station.setYRot(quarter * 90.0F - 180.0F); + station.initOffset(); + } + } +} From 3a1a882ee1abc4d9e2e6dc264ab49efcc8006431 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:20 +0800 Subject: [PATCH 0765/1461] Build the slot display from a stack on 1.21.11 --- .../mcheli/item/VehicleIngredient.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java new file mode 100644 index 00000000..f6c65c79 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java @@ -0,0 +1,67 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Matches one vehicle, such as an AH-6 in the recipe that turns it into an AH-6X. Every vehicle of + * a kind shares an item, so a plain item ingredient would accept any of them. In JSON: + *

{"type": "mcheli:vehicle", "vehicle": "ah-6"}
+ * with the type also under {@code neoforge:ingredient_type} and {@code fabric:type}, since each + * loader names custom ingredients its own way. + * + *

This is the loader-independent half; each loader module wraps it in its own ingredient class. + */ +public final class VehicleIngredient { + public static final Identifier ID = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "vehicle"); + + private VehicleIngredient() { + } + + /** The JSON fields, given a way to build the loader's ingredient from a vehicle name. */ + public static MapCodec codec(Function create, Function vehicle) { + return Codec.STRING.fieldOf("vehicle").xmap(create, vehicle); + } + + public static StreamCodec streamCodec(Function create, Function vehicle) { + return ByteBufCodecs.STRING_UTF8.map(create, vehicle).cast(); + } + + /** The items a vehicle can be: the generic item of each kind. */ + @SuppressWarnings("deprecation") + public static HolderSet kindItems() { + return HolderSet.direct(Item::builtInRegistryHolder, + ModItems.HELICOPTER.get(), ModItems.PLANE.get(), ModItems.TANK.get(), ModItems.VEHICLE.get()); + } + + public static Stream> items() { + return kindItems().stream(); + } + + public static boolean matches(String vehicle, @Nullable ItemStack stack) { + return stack != null && vehicle.equals(AircraftItem.vehicleOf(stack)); + } + + /** What recipe books show: the vehicle itself, or nothing while it is not loaded. */ + public static SlotDisplay display(String vehicle) { + AircraftDefinition def = MCHeli.content().any(vehicle); + return def != null ? new SlotDisplay.ItemStackSlotDisplay(AircraftItem.stackOf(def)) + : SlotDisplay.Empty.INSTANCE; + } +} From d0d9479377da0cc2d95f3473cc5d7ddca37286a0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:20 +0800 Subject: [PATCH 0766/1461] Add item/WrenchItem.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/item/WrenchItem.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java new file mode 100644 index 00000000..8ad8f03f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java @@ -0,0 +1,42 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +/** + * The wrench, {@code MCH_ItemWrench}. Right-clicking a damaged vehicle repairs 10 hit points, once + * a second, at one point of the wrench's durability. Sneaking switches the vehicle to its next + * paint scheme. + * + *

The original repaired while the use key was held; a click per repair works the same with + * today's item use, which stops as soon as the cursor leaves the hull. + */ +public class WrenchItem extends Item { + /** An iron tool's durability, the original's tool material. */ + private static final int DURABILITY = 250; + private static final int REPAIR = 10; + private static final int COOLDOWN = 20; + + public WrenchItem(Properties properties) { + super(properties.durability(DURABILITY)); + } + + /** Server only. */ + public static InteractionResult useOn(ItemStack stack, AircraftEntity vehicle, Player player, InteractionHand hand) { + if (player.isSecondaryUseActive()) { + return vehicle.nextTexture() ? InteractionResult.SUCCESS : InteractionResult.FAIL; + } + if (player.getCooldowns().isOnCooldown(stack) || !vehicle.repair(REPAIR)) { + return InteractionResult.FAIL; + } + player.getCooldowns().addCooldown(stack, COOLDOWN); + stack.hurtAndBreak(1, player, hand); + ModSounds.playAt(vehicle, "wrench", 1.0F, 0.9F + vehicle.getRandom().nextFloat() * 0.2F); + return InteractionResult.SUCCESS; + } +} From d7bbdc37b34517bb04a76661c042617b2bd6a311 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:21 +0800 Subject: [PATCH 0767/1461] Pass the registries to recipe.assemble on 1.21.11 --- .../mcheli/menu/DraftingTableMenu.java | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java new file mode 100644 index 00000000..0c5c7885 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java @@ -0,0 +1,148 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.ResultContainer; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; + +import java.util.List; + +/** + * The drafting table, {@code MCH_DraftingTableGuiContainer}: the player's inventory and one output + * slot. The screen browses the MCHeli recipes in the player's recipe book and asks for one by its + * display id; the server finds the recipe, takes the ingredients from the inventory and puts the + * result in the output slot. + */ +public class DraftingTableMenu extends AbstractContainerMenu { + public static final int OUTPUT_X = 178; + public static final int OUTPUT_Y = 90; + + private final ContainerLevelAccess access; + private final ResultContainer output = new ResultContainer(); + private final int outputSlot; + + /** The client side, from the open screen packet. */ + public DraftingTableMenu(int containerId, Inventory inventory) { + this(containerId, inventory, ContainerLevelAccess.NULL); + } + + public DraftingTableMenu(int containerId, Inventory inventory, ContainerLevelAccess access) { + super(ModMenus.DRAFTING_TABLE.get(), containerId); + this.access = access; + addStandardInventorySlots(inventory, 30, 140); + outputSlot = slots.size(); + addSlot(new Slot(output, 0, OUTPUT_X, OUTPUT_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + } + + /** + * {@code createRecipeItem}: makes one of the recipe behind {@code id}, if it is ours, the output + * slot is free and the player carries the ingredients. Creative players need neither. + */ + public void craft(ServerPlayer player, RecipeDisplayId id) { + boolean creative = player.getAbilities().instabuild; + Slot slot = slots.get(outputSlot); + if (slot.hasItem() && !creative) { + return; + } + RecipeManager.ServerDisplayInfo info = player.level().getServer().getRecipeManager().getRecipeFromDisplay(id); + if (info == null) { + return; + } + RecipeHolder holder = info.parent(); + if (!holder.id().identifier().getNamespace().equals(MCHeli.MOD_ID) || !(holder.value() instanceof CraftingRecipe recipe)) { + return; + } + List ingredients = recipe.placementInfo().ingredients(); + if (!creative && !take(player.getInventory(), ingredients, true)) { + return; + } + if (!creative) { + take(player.getInventory(), ingredients, false); + } + slot.set(recipe.assemble(CraftingInput.EMPTY, player.registryAccess())); + broadcastChanges(); + } + + /** + * Finds, and unless {@code dryRun} removes, one item per ingredient. A stack serves several + * ingredients as far as its count goes. + */ + private static boolean take(Inventory inventory, List ingredients, boolean dryRun) { + int[] used = new int[inventory.getContainerSize()]; + for (Ingredient ingredient : ingredients) { + int found = -1; + for (int i = 0; i < inventory.getContainerSize() && found < 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && stack.getCount() > used[i] && ingredient.test(stack)) { + found = i; + } + } + if (found < 0) { + return false; + } + used[found]++; + } + if (!dryRun) { + for (int i = 0; i < used.length; i++) { + if (used[i] > 0) { + inventory.removeItem(i, used[i]); + } + } + } + return true; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(access, player, ModBlocks.DRAFTING_TABLE.get()); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + if (index != outputSlot) { + return ItemStack.EMPTY; + } + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (!moveItemStackTo(stack, 0, outputSlot, true)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** What is left in the output goes back to the player, as in the original. */ + @Override + public void removed(Player player) { + super.removed(player); + if (!player.level().isClientSide()) { + clearContainer(player, output); + } + } +} From 8468ad2abeb5564ad498670cd0273df2d04f7e44 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:21 +0800 Subject: [PATCH 0768/1461] Add menu/UavStationMenu.java to the 1.21.11 shared sources --- .../mcheli/menu/UavStationMenu.java | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java new file mode 100644 index 00000000..4c5f7bd8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java @@ -0,0 +1,154 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_ContainerUavStation}: one slot for the drone to put up, the + * launch offset with its -10, -1, +1 and +10 buttons per axis, and Continue to take the last drone + * again. The player sits at the station while it is open, so the client finds it as their vehicle. + */ +public class UavStationMenu extends AbstractContainerMenu { + /** Button ids: axis times four plus the step, then Continue. */ + public static final int[] STEPS = {-10, -1, 1, 10}; + public static final int BUTTON_CONTINUE = 12; + + private static final int DATA_SIZE = 4; + private static final int DATA_CAN_CONTINUE = 3; + + private final @Nullable UavStationEntity station; + private final ContainerData data; + + /** The client side, from the open screen packet. */ + public UavStationMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof UavStationEntity s ? s : null, + new SimpleContainer(1), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public UavStationMenu(int containerId, Inventory inventory, UavStationEntity station) { + this(containerId, inventory, station, station.slot(), new ServerData(station)); + } + + private UavStationMenu(int containerId, Inventory inventory, @Nullable UavStationEntity station, Container slot, ContainerData data) { + super(ModMenus.UAV_STATION.get(), containerId); + this.station = station; + this.data = data; + checkContainerSize(slot, 1); + addSlot(new Slot(slot, 0, 20, 20) { + @Override + public boolean mayPlace(ItemStack stack) { + return AircraftItem.vehicleOf(stack) != null; + } + + @Override + public int getMaxStackSize() { + return 1; + } + }); + addStandardInventorySlots(inventory, 8, 84); + addDataSlots(data); + } + + public @Nullable UavStationEntity station() { + return station; + } + + /** The launch offset along {@code axis} (0 x, 1 y, 2 z), as the server last reported it. */ + public int offset(int axis) { + return (short) data.get(axis); + } + + public boolean canContinue() { + return data.get(DATA_CAN_CONTINUE) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (station == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CONTINUE) { + return station.continueControl(server); + } + if (id < 0 || id >= 12) { + return false; + } + int[] pos = {station.offset().getX(), station.offset().getY(), station.offset().getZ()}; + pos[id / 4] += STEPS[id % 4]; + station.setOffset(new BlockPos(pos[0], pos[1], pos[2])); + return true; + } + + @Override + public boolean stillValid(Player player) { + return station != null && station.isAlive() && player.getVehicle() == station; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (index == 0) { + if (!moveItemStackTo(stack, 1, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (AircraftItem.vehicleOf(stack) == null || !moveItemStackTo(stack, 0, 1, false)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** Whatever is left in the slot goes back to the player, as with a crafting table. */ + @Override + public void removed(Player player) { + super.removed(player); + if (station != null && !player.level().isClientSide()) { + clearContainer(player, station.slot()); + } + } + + private record ServerData(UavStationEntity station) implements ContainerData { + @Override + public int get(int index) { + BlockPos o = station.offset(); + return switch (index) { + case 0 -> o.getX(); + case 1 -> o.getY(); + case 2 -> o.getZ(); + default -> station.lastUav() != null ? 1 : 0; + }; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } +} From d6dd06a080caca9c3659f0274272041d5e54174f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:21 +0800 Subject: [PATCH 0769/1461] Add menu/VehicleMenu.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/menu/VehicleMenu.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java new file mode 100644 index 00000000..b2dba1e4 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java @@ -0,0 +1,217 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.weapon.Resupply; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGuiContainer}: fuel slots, a parachute slot per ejection + * seat, reloading the weapons from the crew member's own items and the way into the cargo chest. + * + *

Only crew open it, so the client finds the vehicle as the one its player rides. The weapon + * rounds and which weapons can be reloaded right now come from the server through data slots, + * since a client only hears about the ammunition of the seat it sits in. + */ +public class VehicleMenu extends AbstractContainerMenu { + /** Weapons the screen can show; more than any vehicle in the original has. */ + public static final int MAX_WEAPONS = 32; + /** Button ids from zero up reload that weapon; this one opens the cargo chest. */ + public static final int BUTTON_CARGO = 1000; + + public static final int FUEL_X = 8; + public static final int FUEL_Y = 30; + public static final int PARACHUTE_X = 8; + public static final int PARACHUTE_Y = 66; + public static final int INVENTORY_Y = 102; + + private static final int DATA_RELOADABLE = MAX_WEAPONS; + private static final int DATA_SIZE = MAX_WEAPONS + 2; + + private final @Nullable AircraftEntity vehicle; + private final Container equipment; + private final ContainerData data; + private final int parachuteSlots; + private boolean supplied; + + /** The client side, from the open screen packet. */ + public VehicleMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof AircraftEntity v ? v : null, + new SimpleContainer(VehicleInventory.EQUIPMENT_SLOTS), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public VehicleMenu(int containerId, Inventory inventory, AircraftEntity vehicle) { + this(containerId, inventory, vehicle, vehicle.inventory().equipment(), new ServerData(vehicle, inventory.player)); + } + + private VehicleMenu(int containerId, Inventory inventory, @Nullable AircraftEntity vehicle, Container equipment, ContainerData data) { + super(ModMenus.VEHICLE.get(), containerId); + this.vehicle = vehicle; + this.equipment = equipment; + this.data = data; + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + parachuteSlots = def != null ? VehicleInventory.parachuteSlots(def) : 0; + checkContainerSize(equipment, VehicleInventory.EQUIPMENT_SLOTS); + + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + addSlot(new Slot(equipment, i, FUEL_X + i * 18, FUEL_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() instanceof FuelItem; + } + }); + } + for (int i = 0; i < VehicleInventory.PARACHUTE_SLOTS; i++) { + boolean active = i < parachuteSlots; + addSlot(new Slot(equipment, VehicleInventory.FUEL_SLOTS + i, PARACHUTE_X + i * 18, PARACHUTE_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return active && stack.getItem() instanceof ParachuteItem; + } + + @Override + public boolean isActive() { + return active; + } + }); + } + addStandardInventorySlots(inventory, 8, INVENTORY_Y); + addDataSlots(data); + } + + public @Nullable AircraftEntity vehicle() { + return vehicle; + } + + /** Parachute slots in use: one per ejection seat. */ + public int parachuteSlots() { + return parachuteSlots; + } + + /** Magazine and reserve of a weapon together, as the server last reported them. */ + public int rounds(int weapon) { + return weapon >= 0 && weapon < MAX_WEAPONS ? data.get(weapon) : 0; + } + + /** Whether the server would reload this weapon now: the vehicle is down and the items are there. */ + public boolean canReload(int weapon) { + if (weapon < 0 || weapon >= MAX_WEAPONS) { + return false; + } + int bits = (data.get(DATA_RELOADABLE) & 0xFFFF) | (data.get(DATA_RELOADABLE + 1) & 0xFFFF) << 16; + return (bits & 1 << weapon) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (vehicle == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CARGO) { + return vehicle.openCargo(server); + } + if (id >= 0 && id < MAX_WEAPONS && Resupply.supply(server, vehicle, id)) { + supplied = true; + return true; + } + return false; + } + + @Override + public boolean stillValid(Player player) { + return vehicle != null && equipment.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + int equipmentEnd = VehicleInventory.EQUIPMENT_SLOTS; + if (index < equipmentEnd) { + if (!moveItemStackTo(stack, equipmentEnd, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof FuelItem) { + if (!moveItemStackTo(stack, 0, VehicleInventory.FUEL_SLOTS, false)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof ParachuteItem && parachuteSlots > 0) { + int first = VehicleInventory.FUEL_SLOTS; + if (!moveItemStackTo(stack, first, first + parachuteSlots, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** As in the original, closing the screen after a resupply loads every magazine from its reserve. */ + @Override + public void removed(Player player) { + super.removed(player); + if (supplied && vehicle != null && vehicle.weapons() != null && !player.level().isClientSide()) { + vehicle.weapons().reloadAll(); + } + } + + /** Rounds per weapon and the reload bits, worked out on the server whenever the menu syncs. */ + private record ServerData(AircraftEntity vehicle, Player player) implements ContainerData { + @Override + public int get(int index) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons == null) { + return 0; + } + if (index < MAX_WEAPONS) { + WeaponSet set = weapons.set(index); + // Data slots travel as shorts. + return set != null ? Math.min(set.ammo() + set.restAmmo(), Short.MAX_VALUE) : 0; + } + int bits = 0; + if (player instanceof ServerPlayer server) { + for (int i = 0; i < Math.min(weapons.sets().size(), MAX_WEAPONS); i++) { + if (Resupply.canSupply(server, vehicle, i)) { + bits |= 1 << i; + } + } + } + return index == DATA_RELOADABLE ? bits & 0xFFFF : bits >>> 16; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } +} From 4806686321cd3dd9e1014b1ad2604d58d016d70a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:22 +0800 Subject: [PATCH 0770/1461] Board through interactAt on 1.21.11 --- .../mcheli/network/BoardVehiclePacket.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java new file mode 100644 index 00000000..0f7ccf72 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.Vec3; + +/** + * Client to server: the player right-clicked a vehicle's hull outside its main hit box. The + * server repeats the reach check against the hull boxes, then treats it as an ordinary click. + * + * @param location where on the hull the click landed, in world coordinates + */ +public record BoardVehiclePacket(int entityId, Vec3 location) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("board_vehicle"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(BoardVehiclePacket::encode, BoardVehiclePacket::decode); + + /** The same slack vanilla allows on top of the player's reach for a click on an entity. */ + private static final double REACH_SLACK = 3.0; + + public static void encode(BoardVehiclePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeDouble(msg.location.x); + buf.writeDouble(msg.location.y); + buf.writeDouble(msg.location.z); + } + + public static BoardVehiclePacket decode(FriendlyByteBuf buf) { + return new BoardVehiclePacket(buf.readVarInt(), new Vec3(buf.readDouble(), buf.readDouble(), buf.readDouble())); + } + + public static void handle(BoardVehiclePacket msg, ServerPlayer sender) { + if (sender == null || !(sender.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) || vehicle.isRemoved()) { + return; + } + Vec3 eye = sender.getEyePosition(); + double reach = sender.entityInteractionRange() + REACH_SLACK; + // The click must land on the hull, and the hull must be within reach. + if (eye.distanceToSqr(msg.location) > reach * reach) { + return; + } + Vec3 through = eye.add(msg.location.subtract(eye).scale(1.05)); + boolean onHull = vehicle.clipHitBoxes(eye, through, 0.5).isPresent(); + vehicle.consumeHitBoxFactor(); + if (onHull) { + vehicle.interactAt(sender, msg.location.subtract(vehicle.position()), InteractionHand.MAIN_HAND); + } + } + + @Override + public Type type() { + return TYPE; + } +} From f442f863d134d398ab098b0184e18a60ba3a9784 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:22 +0800 Subject: [PATCH 0771/1461] Add network/CrewActionPacket.java to the 1.21.11 shared sources --- .../mcheli/network/CrewActionPacket.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java new file mode 100644 index 00000000..1b565e21 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java @@ -0,0 +1,164 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A one-off request from a crew member: lower the gear, fold the wings, change seats and so on. + * + *

The client only asks; the server checks that the sender sits in that vehicle, in a seat + * allowed to do it, and then applies the same conditions the original checked before switching. + * The result reaches every client through synched data. + */ +public record CrewActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("crew_action"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(CrewActionPacket::encode, CrewActionPacket::decode); + + public enum Action { + /** Raise or lower the landing gear. Pilot only. */ + LANDING_GEAR, + /** Fold or unfold hatches, wings or swept wings. Pilot only. */ + FOLD, + /** Tilt the nozzles for vertical flight. Pilot only. */ + VTOL, + /** Eject from seat zero or one. */ + EJECT, + /** Move to the next free crew seat. */ + SWITCH_SEAT, + /** Load nearby cargo onto the racks, or drive onto a carrier's rack. Pilot only. */ + RACK_LOAD, + /** Drop the last cargo, or leave the rack this vehicle rides on. Pilot only. */ + RACK_UNLOAD, + /** Select the next weapon this seat may use. */ + NEXT_WEAPON, + /** Select the previous weapon this seat may use. */ + PREVIOUS_WEAPON, + /** Cycle the fire mode of the selected weapon. */ + WEAPON_MODE, + /** Release flares. Pilot and first crew seat. */ + FLARE, + /** Open the vehicle screen: fuel and parachute slots, reloading, the way to the cargo chest. */ + VEHICLE_SCREEN, + /** Hand the controls to the autopilot and man the weapons, or take them back. Pilot only. */ + GUNNER_MODE, + /** Turn night vision on for the sender, in vehicles with {@code EnableNightVision}. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF, + /** Let the troops down: ropes from a hover, a parachute drop, or out on the ground. Pilot only. */ + DROP_CREW, + /** Slide down a rope or jump with a parachute. Crew other than the pilot. */ + JUMP_OUT, + /** Turn the search light on or off. Pilot and first crew seat. */ + SEARCH_LIGHT + } + + public static void encode(CrewActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static CrewActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown crew action " + ordinal); + } + return new CrewActionPacket(entityId, actions[ordinal]); + } + + public static void handle(CrewActionPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + boolean pilot = vehicle.getControllingPassenger() == sender; + switch (msg.action) { + case LANDING_GEAR -> { + if (pilot) { + vehicle.toggleLandingGear(); + } + } + case FOLD -> { + if (pilot) { + vehicle.toggleFold(); + } + } + case VTOL -> { + if (pilot && vehicle instanceof PlaneEntity plane) { + plane.toggleVtol(); + } + } + case GUNNER_MODE -> { + if (pilot) { + vehicle.setGunnerMode(!vehicle.isGunnerMode()); + } + } + case EJECT -> vehicle.eject(sender); + case SWITCH_SEAT -> vehicle.switchSeat(sender); + case RACK_LOAD -> { + if (pilot) { + if (!vehicle.boardNearbyRack()) { + vehicle.loadNearbyCargo(); + } + } + } + case RACK_UNLOAD -> { + if (pilot) { + if (vehicle.isPassenger()) { + vehicle.leaveRack(); + } else { + vehicle.unloadCargo(); + } + } + } + case NEXT_WEAPON, PREVIOUS_WEAPON -> { + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons != null && !vehicle.isDestroyed()) { + int next = weapons.next(seat, msg.action == Action.NEXT_WEAPON ? 1 : -1); + if (next >= 0) { + weapons.switchTo(sender, seat, next); + } + } + } + case WEAPON_MODE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.switchMode(vehicle.seatOf(sender)); + } + } + case FLARE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null && vehicle.seatOf(sender) <= 1) { + weapons.flares.use(); + } + } + case VEHICLE_SCREEN -> vehicle.openScreen(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> vehicle.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + case DROP_CREW -> { + if (pilot && !vehicle.dropCrew()) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + case SEARCH_LIGHT -> vehicle.toggleSearchLight(sender); + case JUMP_OUT -> { + if (!pilot && !vehicle.jumpOut(sender)) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 3ffa974e1761f610f729aa5d37552ac00d86c2b3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:23 +0800 Subject: [PATCH 0772/1461] Add network/DraftingTablePacket.java to the 1.21.11 shared sources --- .../mcheli/network/DraftingTablePacket.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java new file mode 100644 index 00000000..46294aa3 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; + +/** + * The Create button of the drafting table, {@code MCH_DraftingTableCreatePacket}: make one of the + * recipe the player's recipe book knows by this display id. + */ +public record DraftingTablePacket(RecipeDisplayId recipe) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("drafting_table"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(DraftingTablePacket::encode, DraftingTablePacket::decode); + + public static void encode(DraftingTablePacket msg, FriendlyByteBuf buf) { + RecipeDisplayId.STREAM_CODEC.encode(buf, msg.recipe); + } + + public static DraftingTablePacket decode(FriendlyByteBuf buf) { + return new DraftingTablePacket(RecipeDisplayId.STREAM_CODEC.decode(buf)); + } + + public static void handle(DraftingTablePacket msg, ServerPlayer sender) { + if (sender != null && sender.containerMenu instanceof DraftingTableMenu menu && menu.stillValid(sender)) { + menu.craft(sender, msg.recipe); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 612728f142f1fe7a7cd41092dd9d83e665f21d4b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:23 +0800 Subject: [PATCH 0773/1461] Add network/EquipmentActionPacket.java to the 1.21.11 shared sources --- .../mcheli/network/EquipmentActionPacket.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java new file mode 100644 index 00000000..76090f2e --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A request from a player using a piece of equipment they sit at, such as the GLTD. Like + * {@link CrewActionPacket} the client only asks; the equipment checks that the sender is the one + * using it. + */ +public record EquipmentActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("equipment_action"); + public static final StreamCodec STREAM_CODEC = + StreamCodec.ofMember(EquipmentActionPacket::encode, EquipmentActionPacket::decode); + + public enum Action { + /** Mark the spot under the sight and call in the A-10. */ + CALL_STRIKE, + /** Turn night vision on for the sender. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF + } + + public static void encode(EquipmentActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static EquipmentActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown equipment action " + ordinal); + } + return new EquipmentActionPacket(entityId, actions[ordinal]); + } + + public static void handle(EquipmentActionPacket msg, ServerPlayer sender) { + if (sender == null || sender.getVehicle() == null || sender.getVehicle().getId() != msg.entityId) { + return; + } + if (sender.getVehicle() instanceof GltdEntity gltd) { + switch (msg.action) { + case CALL_STRIKE -> gltd.callStrike(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> gltd.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 9bb9e90732bee6cea47a6b2a591158ccf1b80632 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:24 +0800 Subject: [PATCH 0774/1461] Add network/ExplosionEffectPacket.java to the 1.21.11 shared sources --- .../mcheli/network/ExplosionEffectPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java new file mode 100644 index 00000000..279b1511 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.ExplosionEffect; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to nearby players: draw an MCHeli explosion here. The original's + * {@code MCH_PacketEffectExplosion}, plus a flag for the bigger fireball of a crashing vehicle. + */ +public record ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater, boolean crash) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("explosion_effect"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(ExplosionEffectPacket::encode, ExplosionEffectPacket::decode); + + public ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater) { + this(x, y, z, size, inWater, false); + } + + public static void encode(ExplosionEffectPacket msg, FriendlyByteBuf buf) { + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.size); + buf.writeBoolean(msg.inWater); + buf.writeBoolean(msg.crash); + } + + public static ExplosionEffectPacket decode(FriendlyByteBuf buf) { + return new ExplosionEffectPacket(buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readBoolean(), + buf.readBoolean()); + } + + public static void handle(ExplosionEffectPacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player == null || !Float.isFinite(msg.size)) { + return; + } + float size = Math.min(msg.size, 50.0F); + if (msg.crash) { + ExplosionEffect.crash(player.level(), msg.x, msg.y, msg.z, Math.min(size, ExplosionEffect.MAX_CRASH_SIZE)); + } else { + ExplosionEffect.play(player.level(), msg.x, msg.y, msg.z, size, msg.inWater); + } + } + + @Override + public Type type() { + return TYPE; + } +} From b113185d31ef84dd335309b24c2dc0b244cc62e0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:24 +0800 Subject: [PATCH 0775/1461] Add network/FireWeaponPacket.java to the 1.21.11 shared sources --- .../mcheli/network/FireWeaponPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java new file mode 100644 index 00000000..0056dcc0 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.ServerFiring; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Client to server: the crew member's selected weapon fired on their side, so fire it for real. + * The client already checked readiness, ammunition and lock; the server checks again with its own + * timers and only trusts the two option values, a locked entity and a fire mode. + * + * @param weapon the weapon the client fired, ignored unless it is still the seat's selection + */ +public record FireWeaponPacket(int entityId, int weapon, int option1, int option2) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("fire_weapon"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(FireWeaponPacket::encode, FireWeaponPacket::decode); + + public static void encode(FireWeaponPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.option1); + buf.writeVarInt(msg.option2); + } + + public static FireWeaponPacket decode(FriendlyByteBuf buf) { + return new FireWeaponPacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(FireWeaponPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons == null || weapons.selected(seat) != msg.weapon || weapons.operatorSeat(msg.weapon) != seat) { + return; + } + Shot shot = new Shot(vehicle, sender); + shot.option1 = msg.option1; + shot.option2 = msg.option2; + weapons.use(sender, seat, shot, ServerFiring.INSTANCE); + } + + @Override + public Type type() { + return TYPE; + } +} From 6c760f54f6fcba0fb65d93a4c7c93bead940a003 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:25 +0800 Subject: [PATCH 0776/1461] Add network/LauncherPacket.java to the 1.21.11 shared sources --- .../mcheli/network/LauncherPacket.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java new file mode 100644 index 00000000..d3f204c7 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java @@ -0,0 +1,119 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +/** + * The player with a launcher in hand, {@code MCH_PacketLightWeaponPlayerControl}: fire at the + * locked target, or finish a reload. The client locks and times the reload; the server checks + * the launcher, the target and the missile in the inventory. + * + * @param target the locked entity, for {@link Action#FIRE} + * @param mode the Javelin's attack mode: 0 direct, 1 top attack + */ +public record LauncherPacket(Action action, int target, int mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("launcher"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LauncherPacket::encode, LauncherPacket::decode); + + /** Locks further than this are refused, with some slack over the lock range for lag. */ + private static final double MAX_TARGET_DISTANCE = 260.0; + + public enum Action { + FIRE, + RELOAD + } + + public static void encode(LauncherPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeVarInt(msg.target); + buf.writeVarInt(msg.mode); + } + + public static LauncherPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown launcher action " + ordinal); + } + return new LauncherPacket(actions[ordinal], buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(LauncherPacket msg, ServerPlayer sender) { + ItemStack stack = sender == null ? ItemStack.EMPTY : sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof LauncherItem launcher) || !LauncherItem.canUse(sender)) { + return; + } + switch (msg.action) { + case FIRE -> fire(sender, stack, launcher, msg.target, msg.mode); + case RELOAD -> reload(sender, stack, launcher); + } + } + + private static void fire(ServerPlayer player, ItemStack stack, LauncherItem launcher, int targetId, int mode) { + WeaponDefinition def = MCHeli.content().weapon(launcher.weapon()); + ServerLevel level = player.level(); + Entity target = level.getEntity(targetId); + if (def == null || !LauncherItem.isLoaded(stack) || target == null || target.isRemoved() + || target.distanceToSqr(player) > MAX_TARGET_DISTANCE * MAX_TARGET_DISTANCE) { + return; + } + ProjectileEntity missile = def.type == WeaponType.AT_MISSILE + ? ModEntities.AT_MISSILE.get().create(level, EntitySpawnReason.TRIGGERED) + : ModEntities.AA_MISSILE.get().create(level, EntitySpawnReason.TRIGGERED); + if (missile == null) { + return; + } + float yaw = player.getYRot(); + float pitch = player.getXRot(); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + Vec3 at = player.getEyePosition().add(dir); + missile.launch(at.x, at.y, at.z, dir.x, dir.y, dir.z, yaw, pitch, def.acceleration); + missile.setWeapon(launcher.weapon()); + missile.setParameters(def, player, player); + missile.setTarget(target); + if (missile instanceof ATMissileEntity atMissile) { + atMissile.guidanceType = Math.clamp(mode, 0, Math.max(def.modeNum - 1, 0)); + } + level.addFreshEntity(missile); + if (def.sound != null) { + ModSounds.playAt(player, def.sound, def.soundVolume, def.soundPitch); + } + LauncherItem.setLoaded(stack, false); + } + + private static void reload(ServerPlayer player, ItemStack stack, LauncherItem launcher) { + if (LauncherItem.isLoaded(stack)) { + return; + } + if (!player.getAbilities().instabuild) { + int slot = player.getInventory().findSlotMatchingItem(new ItemStack(launcher.missile())); + if (slot < 0) { + return; + } + player.getInventory().removeItem(slot, 1); + } + LauncherItem.setLoaded(stack, true); + } + + @Override + public Type type() { + return TYPE; + } +} From a7c5fc31af483d293aa095c838acb6a9484bd3d1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:25 +0800 Subject: [PATCH 0777/1461] Add network/LockWarningPacket.java to the 1.21.11 shared sources --- .../mcheli/network/LockWarningPacket.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java new file mode 100644 index 00000000..97d2cbf4 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java @@ -0,0 +1,44 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +/** + * Client to server: this player is locking on to an entity. If it is a vehicle with flares, or a + * crew member of one, its crew hears the warning. The original's {@code MCH_PacketNotifyLock}. + */ +public record LockWarningPacket(int targetId) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("lock_warning"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LockWarningPacket::encode, LockWarningPacket::decode); + + public static void encode(LockWarningPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.targetId); + } + + public static LockWarningPacket decode(FriendlyByteBuf buf) { + return new LockWarningPacket(buf.readVarInt()); + } + + public static void handle(LockWarningPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity)) { + return; + } + Entity target = sender.level().getEntity(msg.targetId); + if (target != null && target.distanceToSqr(sender) < 400.0 * 400.0) { + AircraftEntity vehicle = target instanceof AircraftEntity a ? a : target.getVehicle() instanceof AircraftEntity a ? a : null; + if (vehicle != null) { + vehicle.warnLocked(); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 060d5a8575bef6036c2a61e4b4c3e39d934f2bd3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:26 +0800 Subject: [PATCH 0778/1461] Add network/MarkerPacket.java to the 1.21.11 shared sources --- .../mcheli/network/MarkerPacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java new file mode 100644 index 00000000..1de0bfc3 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to client: the team spotted these entities for {@code ticks}, or marked {@code point}, + * {@code MCH_PacketNotifySpotedEntity} and {@code MCH_PacketNotifyMarkPoint}. Only the spotter and + * their teammates get it. + */ +public record MarkerPacket(int[] entities, int ticks, BlockPos point) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("marker"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(MarkerPacket::encode, MarkerPacket::decode); + /** Spotting sends no point; this stands in for none. */ + private static final BlockPos NO_POINT = new BlockPos(0, Integer.MIN_VALUE, 0); + private static final int MAX_ENTITIES = 256; + + public static MarkerPacket spotted(int[] entities, int ticks) { + return new MarkerPacket(entities, ticks, NO_POINT); + } + + public static MarkerPacket point(BlockPos point) { + return new MarkerPacket(new int[0], 0, point); + } + + public static void encode(MarkerPacket msg, FriendlyByteBuf buf) { + buf.writeVarIntArray(msg.entities); + buf.writeVarInt(msg.ticks); + buf.writeBlockPos(msg.point); + } + + public static MarkerPacket decode(FriendlyByteBuf buf) { + return new MarkerPacket(buf.readVarIntArray(MAX_ENTITIES), buf.readVarInt(), buf.readBlockPos()); + } + + public static void handle(MarkerPacket msg) { + if (msg.entities.length > 0) { + Markers.spotted(msg.entities, msg.ticks); + } + if (!msg.point.equals(NO_POINT)) { + Markers.point(msg.point); + } + } + + @Override + public Type type() { + return TYPE; + } +} From c2ced844adb832f3adb2735e8ab044f6fc9741fc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:26 +0800 Subject: [PATCH 0779/1461] Add network/ModNetwork.java to the 1.21.11 shared sources --- .../mcheli/network/ModNetwork.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java new file mode 100644 index 00000000..26065705 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java @@ -0,0 +1,90 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Our packets. Each one is a custom payload with its own id and the same encoding on every loader, + * so a client on one loader can talk to a server on another. The loader module registers the + * lists below with its own networking API; handlers always run on the main thread. + */ +public final class ModNetwork { + /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ + public static final int PROTOCOL_VERSION = 11; + + /** Client to server. The handler gets the sending player. */ + public static final List> SERVERBOUND = List.of( + new Serverbound<>(PilotStatePacket.TYPE, PilotStatePacket.STREAM_CODEC, PilotStatePacket::handle), + new Serverbound<>(CrewActionPacket.TYPE, CrewActionPacket.STREAM_CODEC, CrewActionPacket::handle), + new Serverbound<>(EquipmentActionPacket.TYPE, EquipmentActionPacket.STREAM_CODEC, EquipmentActionPacket::handle), + new Serverbound<>(SpotPacket.TYPE, SpotPacket.STREAM_CODEC, SpotPacket::handle), + new Serverbound<>(LauncherPacket.TYPE, LauncherPacket.STREAM_CODEC, LauncherPacket::handle), + new Serverbound<>(DraftingTablePacket.TYPE, DraftingTablePacket.STREAM_CODEC, DraftingTablePacket::handle), + new Serverbound<>(TeamPacket.TYPE, TeamPacket.STREAM_CODEC, TeamPacket::handle), + new Serverbound<>(TestFlightPacket.TYPE, TestFlightPacket.STREAM_CODEC, TestFlightPacket::handle), + new Serverbound<>(FireWeaponPacket.TYPE, FireWeaponPacket.STREAM_CODEC, FireWeaponPacket::handle), + new Serverbound<>(BoardVehiclePacket.TYPE, BoardVehiclePacket.STREAM_CODEC, BoardVehiclePacket::handle), + new Serverbound<>(LockWarningPacket.TYPE, LockWarningPacket.STREAM_CODEC, LockWarningPacket::handle), + new Serverbound<>(PackRequestPacket.TYPE, PackRequestPacket.STREAM_CODEC, PackRequestPacket::handle), + new Serverbound<>(PackSubmitPacket.TYPE, PackSubmitPacket.STREAM_CODEC, PackSubmitPacket::handle)); + + /** Server to client. Handlers touch client classes, so they only ever run on a client. */ + public static final List> CLIENTBOUND = List.of( + new Clientbound<>(WeaponStatePacket.TYPE, WeaponStatePacket.STREAM_CODEC, WeaponStatePacket::handle), + new Clientbound<>(ExplosionEffectPacket.TYPE, ExplosionEffectPacket.STREAM_CODEC, ExplosionEffectPacket::handle), + new Clientbound<>(PackListPacket.TYPE, PackListPacket.STREAM_CODEC, PackListPacket::handle), + new Clientbound<>(PackDataPacket.TYPE, PackDataPacket.STREAM_CODEC, PackDataPacket::handle), + new Clientbound<>(PackSoundPacket.TYPE, PackSoundPacket.STREAM_CODEC, PackSoundPacket::handle), + new Clientbound<>(MarkerPacket.TYPE, MarkerPacket.STREAM_CODEC, MarkerPacket::handle)); + + private ModNetwork() { + } + + static CustomPacketPayload.Type type(String name) { + return new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, name)); + } + + public static void sendToServer(CustomPacketPayload message) { + Platform.get().sendToServer(message); + } + + public static void sendToPlayer(ServerPlayer player, CustomPacketPayload message) { + Platform.get().sendToPlayer(player, message); + } + + public static void sendToAll(MinecraftServer server, CustomPacketPayload message) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + sendToPlayer(player, message); + } + } + + /** Sends to every player in {@code level} within {@code range} blocks of the point. */ + public static void sendNear(ServerLevel level, double x, double y, double z, double range, CustomPacketPayload message) { + for (ServerPlayer player : level.players()) { + if (player.distanceToSqr(x, y, z) < range * range) { + sendToPlayer(player, message); + } + } + } + + public record Serverbound(CustomPacketPayload.Type type, + StreamCodec codec, + BiConsumer handler) { + } + + public record Clientbound(CustomPacketPayload.Type type, + StreamCodec codec, + Consumer handler) { + } +} From 8f0d206c61800e5ce06a8b8ee81c406c3c545e91 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:26 +0800 Subject: [PATCH 0780/1461] Add network/PackDataPacket.java to the 1.21.11 shared sources --- .../mcheli/network/PackDataPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java new file mode 100644 index 00000000..89c1644d --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: one chunk of a pack's zip. Chunks of a pack arrive in order, so + * {@code offset} only lets the client check that nothing went missing. + * + * @param total length of the whole zip, as in the {@link PackListPacket} + */ +public record PackDataPacket(String hash, int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_data"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackDataPacket::encode, PackDataPacket::decode); + + /** Well under the 1 MiB limit of a clientbound custom payload. */ + public static final int CHUNK_SIZE = 128 * 1024; + + public static void encode(PackDataPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.hash, 64); + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackDataPacket decode(FriendlyByteBuf buf) { + return new PackDataPacket(buf.readUtf(64), buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackDataPacket msg) { + ClientAccess.packs().onPackData(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From ccab4107a9e8d9e0278b5aa99d05fe6adc264c90 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:27 +0800 Subject: [PATCH 0781/1461] Add network/PackListPacket.java to the 1.21.11 shared sources --- .../mcheli/network/PackListPacket.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java new file mode 100644 index 00000000..ad7f0c85 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +import java.util.ArrayList; +import java.util.List; + +/** + * Server to player: the content packs the server uses, in load order. Sent on join and after + * {@code /mcheli reload}. The client requests the packs it has not cached ({@link PackRequestPacket}) + * and then rebuilds its content from exactly this list, so both sides fly the same definitions. + * The imported 1.0.3 content is never listed: every player imports their own copy. + */ +public record PackListPacket(List packs) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_list"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackListPacket::encode, PackListPacket::decode); + + public static final int MAX_PACKS = 256; + + /** + * @param size length of the pack's zip in bytes, which is what {@link PackDataPacket} sends + */ + public record Entry(String id, String version, String hash, int size) { + } + + public static void encode(PackListPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.packs.size()); + for (Entry entry : msg.packs) { + buf.writeUtf(entry.id, 64); + buf.writeUtf(entry.version, 32); + buf.writeUtf(entry.hash, 64); + buf.writeVarInt(entry.size); + } + } + + public static PackListPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > MAX_PACKS) { + throw new DecoderException("Too many content packs: " + count); + } + List packs = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + packs.add(new Entry(buf.readUtf(64), buf.readUtf(32), buf.readUtf(64), buf.readVarInt())); + } + return new PackListPacket(List.copyOf(packs)); + } + + public static void handle(PackListPacket msg) { + ClientAccess.packs().onPackList(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 57c7f438218bbd349297e14bafa40b3a00c87fd4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:27 +0800 Subject: [PATCH 0782/1461] Add network/PackRequestPacket.java to the 1.21.11 shared sources --- .../mcheli/network/PackRequestPacket.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java new file mode 100644 index 00000000..50f10f74 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.network; + +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Player to server: send me these packs, by content hash. The server only answers for hashes in + * the list it sent, so a client cannot ask for anything else. + */ +public record PackRequestPacket(List hashes) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_request"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackRequestPacket::encode, PackRequestPacket::decode); + + public static void encode(PackRequestPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.hashes.size()); + for (String hash : msg.hashes) { + buf.writeUtf(hash, 64); + } + } + + public static PackRequestPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > PackListPacket.MAX_PACKS) { + throw new DecoderException("Too many content packs requested: " + count); + } + List hashes = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + hashes.add(buf.readUtf(64)); + } + return new PackRequestPacket(List.copyOf(hashes)); + } + + public static void handle(PackRequestPacket msg, ServerPlayer sender) { + if (sender != null) { + PackSync.request(sender, msg.hashes); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 09624713864dd5a22731a6034ba79c0e05b9a0a4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:28 +0800 Subject: [PATCH 0783/1461] Add network/PackSoundPacket.java to the 1.21.11 shared sources --- .../mcheli/network/PackSoundPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java new file mode 100644 index 00000000..8bac5034 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: play a sound a content pack brought, once, at a point. The game's own sound + * packet ends at the sound manager, which only knows the sounds in {@code sounds.json}, and a pack + * has none: it would be silent. See {@code ModSounds}. + */ +public record PackSoundPacket(String sound, double x, double y, double z, float volume, float pitch) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_sound"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSoundPacket::encode, PackSoundPacket::decode); + + private static final int MAX_NAME = 256; + + public static void encode(PackSoundPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.sound, MAX_NAME); + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.volume); + buf.writeFloat(msg.pitch); + } + + public static PackSoundPacket decode(FriendlyByteBuf buf) { + return new PackSoundPacket(buf.readUtf(MAX_NAME), buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readFloat()); + } + + public static void handle(PackSoundPacket msg) { + ClientAccess.packs().onPackSound(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 29d228f3c3c7fad5a5680ce6c1f2995d1b0dc1eb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:28 +0800 Subject: [PATCH 0784/1461] Add network/PackSubmissions.java to the 1.21.11 shared sources --- .../mcheli/network/PackSubmissions.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java new file mode 100644 index 00000000..b25eba0a --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java @@ -0,0 +1,185 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Util; +import org.jspecify.annotations.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Players offering a server the vehicles they built, for its operators to approve. + *

+ *  Submit in the workshop   ── PackSubmitPacket(chunk) ──▶  until the whole zip is here
+ *  checked off the main thread, kept in mcheli/pending, operators told
+ *  /mcheli approve <pack>    moves it to mcheli/packs and reloads, so every player gets it
+ * 
+ * Nothing a client sends is trusted. Submissions are off until the server's owner turns them on, one + * zip can be no larger than the server allows for a pack, a player sends one at a time with a pause + * in between, chunks out of order end the transfer, and a few transfers run at once at most. The + * bytes stay in memory only while they arrive; {@link PendingPacks} checks them as it would any pack. + * + *

Everything here runs on the server thread except the check, which reads the whole pack. + */ +public final class PackSubmissions { + private static final int MAX_UPLOADS = 4; + private static final long COOLDOWN_MILLIS = 30_000L; + + private static final Map UPLOADS = new HashMap<>(); + private static final Map LAST_SUBMISSION = new HashMap<>(); + + private PackSubmissions() { + } + + static void receive(ServerPlayer player, PackSubmitPacket packet) { + UUID id = player.getUUID(); + if (packet.offset() == 0) { + UPLOADS.remove(id); + Component refusal = refusal(player, packet.total()); + if (refusal != null) { + player.sendSystemMessage(refusal); + return; + } + UPLOADS.put(id, new ByteArrayOutputStream()); + } + ByteArrayOutputStream upload = UPLOADS.get(id); + // No upload means it was refused above and the rest of its chunks are still on their way. + if (upload == null) { + return; + } + long end = (long) upload.size() + packet.data().length; + if (packet.offset() != upload.size() || packet.total() > ServerConfig.maxPackBytes() || end > packet.total()) { + UPLOADS.remove(id); + player.sendSystemMessage(Component.translatable("message.mcheli.submit.failed", "chunks arrived out of order")); + return; + } + upload.write(packet.data(), 0, packet.data().length); + if (end == packet.total()) { + UPLOADS.remove(id); + LAST_SUBMISSION.put(id, System.currentTimeMillis()); + check(player, upload.toByteArray()); + } + } + + private static @Nullable Component refusal(ServerPlayer player, int total) { + if (!ServerConfig.allowSubmissions()) { + return Component.translatable("message.mcheli.submit.off"); + } + if (total <= 0 || total > ServerConfig.maxPackBytes()) { + return Component.translatable("message.mcheli.submit.too_large", ServerConfig.maxPackBytes() >> 20); + } + if (UPLOADS.size() >= MAX_UPLOADS) { + return Component.translatable("message.mcheli.submit.busy"); + } + long now = System.currentTimeMillis(); + LAST_SUBMISSION.values().removeIf(time -> now - time >= COOLDOWN_MILLIS); + return LAST_SUBMISSION.containsKey(player.getUUID()) ? Component.translatable("message.mcheli.submit.busy") : null; + } + + private static void check(ServerPlayer player, byte[] zip) { + MinecraftServer server = player.level().getServer(); + UUID id = player.getUUID(); + String name = player.getScoreboardName(); + CompletableFuture.supplyAsync(() -> { + try { + return MCHeli.packs().pending().store(zip, name, id); + } catch (IOException | PackException | RuntimeException e) { + throw new java.util.concurrent.CompletionException(e); + } + }, Util.nonCriticalIoPool()).whenComplete((submission, failure) -> server.execute(() -> { + if (failure != null) { + Throwable cause = failure.getCause() != null ? failure.getCause() : failure; + MCHeli.LOGGER.info("{} sent a pack that was refused: {}", name, cause.toString()); + tell(server, id, Component.translatable("message.mcheli.submit.failed", cause.getMessage() != null ? cause.getMessage() : cause.toString())); + } else { + arrived(server, submission); + } + })); + } + + private static void arrived(MinecraftServer server, PendingPacks.Submission submission) { + MCHeli.LOGGER.info("{} submitted content pack {} {} ({})", submission.submitter(), submission.id(), + submission.manifest().version(), ContentPack.shortHash(submission.hash())); + // Even with approval off, a pack taking over one that is in use waits for a person. + boolean replaces = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (!ServerConfig.requireApproval() && !replaces) { + try { + install(server, submission); + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not install submitted content pack {}", submission.id(), e); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.failed", e.getMessage())); + } + return; + } + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.waiting", submission.id())); + Component note = Component.translatable("message.mcheli.submit.to_operators", submission.submitter(), submission.id(), submission.manifest().version()); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (PackSync.seesProblems(player)) { + player.sendSystemMessage(note); + } + } + } + + /** + * Puts a waiting pack in use: into {@code mcheli/packs}, then a reload that sends every player + * the new list. + * + * @return whether the pack is in use now; the server's allow list or size limits may leave it out + */ + public static boolean install(MinecraftServer server, PendingPacks.Submission submission) throws IOException, PackException { + MCHeli.packs().pending().install(submission, MCHeli.packs().folder()); + ContentReload.reload(server); + MCHeli.LOGGER.info("Content pack {} from {} approved", submission.id(), submission.submitter()); + boolean live = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (live) { + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.approved", submission.id())); + } + return live; + } + + /** Throws a waiting pack away and tells the player who sent it, if they are here. */ + public static void reject(MinecraftServer server, PendingPacks.Submission submission) throws IOException { + MCHeli.packs().pending().remove(submission); + MCHeli.LOGGER.info("Content pack {} from {} rejected", submission.id(), submission.submitter()); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.rejected", submission.id())); + } + + private static void tell(MinecraftServer server, UUID player, Component message) { + ServerPlayer online = server.getPlayerList().getPlayer(player); + if (online != null) { + online.sendSystemMessage(message); + } + } + + /** An operator joining hears what is waiting for them. */ + static void onLogin(ServerPlayer player) { + if (!PackSync.seesProblems(player)) { + return; + } + try { + List waiting = MCHeli.packs().pending().list(); + if (!waiting.isEmpty()) { + player.sendSystemMessage(Component.translatable("message.mcheli.submit.waiting_for_you", waiting.size())); + } + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not list the submitted content packs", e); + } + } + + static void onLogout(ServerPlayer player) { + UPLOADS.remove(player.getUUID()); + } +} From ea822ea3b856c56bb5127936663a2dd7b997058b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:29 +0800 Subject: [PATCH 0785/1461] Add network/PackSubmitPacket.java to the 1.21.11 shared sources --- .../mcheli/network/PackSubmitPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java new file mode 100644 index 00000000..d142e700 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Player to server: one chunk of a pack the player made, offered to the server's operators. See + * {@link PackSubmissions}, which treats all of this as untrusted. + * + * @param offset where this chunk goes; 0 starts a new submission + * @param total length of the whole zip + */ +public record PackSubmitPacket(int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_submit"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSubmitPacket::encode, PackSubmitPacket::decode); + + /** Under the 32 KiB limit of a serverbound custom payload, which is far lower than the other way. */ + public static final int CHUNK_SIZE = 30 * 1024; + + public static void encode(PackSubmitPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackSubmitPacket decode(FriendlyByteBuf buf) { + return new PackSubmitPacket(buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackSubmitPacket msg, ServerPlayer sender) { + PackSubmissions.receive(sender, msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 4d123ccb570f248f8ad67108a0454511d0c1f992 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:29 +0800 Subject: [PATCH 0786/1461] Add network/PackSync.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/network/PackSync.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSync.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSync.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSync.java new file mode 100644 index 00000000..b3e53269 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PackSync.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * The server half of the content pack sync. + *

+ *  player joins          ── PackListPacket(id, version, hash, size per pack) ──▶ client
+ *                        ◀── PackRequestPacket(hashes it has not cached) ────────
+ *  every tick            ── PackDataPacket(chunk) ──▶  until each pack is sent
+ * 
+ * Each player gets {@link #CHUNKS_PER_TICK} chunks a tick (about 5 MB a second), one pack after + * the other, so a download never crowds out the game's own packets. A pack's bytes are read when + * its turn comes and dropped once sent. Everything here runs on the server thread. + */ +public final class PackSync { + private static final int CHUNKS_PER_TICK = 2; + + private static final Map> TRANSFERS = new HashMap<>(); + + private PackSync() { + } + + /** Sends a joining player the pack list, and operators any pack problems. */ + public static void onLogin(ServerPlayer player) { + ModNetwork.sendToPlayer(player, list()); + if (seesProblems(player)) { + MCHeli.packs().reportProblems(player::sendSystemMessage); + } + PackSubmissions.onLogin(player); + } + + public static void onLogout(ServerPlayer player) { + TRANSFERS.remove(player.getUUID()); + PackSubmissions.onLogout(player); + } + + /** The packs the server uses now, as the client should load them. */ + public static PackListPacket list() { + List entries = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + try { + entries.add(new PackListPacket.Entry(pack.id(), pack.manifest().version(), pack.hash(), (int) pack.archiveSize())); + } catch (IOException e) { + MCHeli.LOGGER.error("Content pack {} cannot be sent to players", pack, e); + } + } + return new PackListPacket(List.copyOf(entries)); + } + + /** After a reload: drops transfers in progress and sends every player the new list. */ + public static void sendListToAll(MinecraftServer server) { + TRANSFERS.clear(); + ModNetwork.sendToAll(server, list()); + } + + /** Operators, and the owner of a single player or LAN world, are told what is wrong with the packs. */ + public static boolean seesProblems(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + return server.isSingleplayerOwner(player.nameAndId()) + || Commands.hasPermission(Commands.LEVEL_GAMEMASTERS).test(player.createCommandSourceStack()); + } + + static void request(ServerPlayer player, List hashes) { + Deque queue = TRANSFERS.computeIfAbsent(player.getUUID(), id -> new ArrayDeque<>()); + for (String hash : hashes) { + ContentPack pack = find(hash); + if (pack == null) { + MCHeli.LOGGER.debug("{} asked for content pack {}, which the server does not use", player.getScoreboardName(), hash); + continue; + } + if (queue.stream().noneMatch(t -> t.pack == pack)) { + queue.add(new Transfer(pack)); + } + } + } + + private static @Nullable ContentPack find(String hash) { + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.hash().equals(hash)) { + return pack; + } + } + return null; + } + + public static void pump(MinecraftServer server) { + Iterator>> it = TRANSFERS.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey()); + Deque queue = entry.getValue(); + if (player == null || queue.isEmpty()) { + it.remove(); + continue; + } + for (int sent = 0; sent < CHUNKS_PER_TICK && !queue.isEmpty(); sent++) { + Transfer transfer = queue.peek(); + PackDataPacket chunk = transfer.next(); + if (chunk == null) { + queue.poll(); + continue; + } + ModNetwork.sendToPlayer(player, chunk); + if (transfer.done()) { + queue.poll(); + } + } + } + } + + /** One pack on its way to one player. */ + private static final class Transfer { + private final ContentPack pack; + private byte @Nullable [] data; + private int offset; + + Transfer(ContentPack pack) { + this.pack = pack; + } + + /** The next chunk, or {@code null} when the pack could not be read. */ + @Nullable PackDataPacket next() { + if (data == null) { + try { + data = pack.archive(); + } catch (IOException e) { + MCHeli.LOGGER.error("Could not read content pack {} to send it", pack, e); + return null; + } + } + int end = Math.min(offset + PackDataPacket.CHUNK_SIZE, data.length); + PackDataPacket chunk = new PackDataPacket(pack.hash(), offset, data.length, Arrays.copyOfRange(data, offset, end)); + offset = end; + return chunk; + } + + boolean done() { + return data != null && offset >= data.length; + } + } +} From 25450ee38db9aae3f408422a5acddce66596e0eb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:30 +0800 Subject: [PATCH 0787/1461] Add network/PilotStatePacket.java to the 1.21.11 shared sources --- .../mcheli/network/PilotStatePacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java new file mode 100644 index 00000000..6426f060 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Sent by the pilot every tick. Position, yaw and pitch already travel through the vanilla + * vehicle move packet; this carries the parts vanilla does not know about. + * + * @param keys the pilot's key state as packed by {@link AircraftEntity.ControlInput#write()}, so + * other players see wheels steer and tracks turn + */ +public record PilotStatePacket(int entityId, float throttle, float reverse, float roll, boolean hovering, byte keys) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pilot_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PilotStatePacket::encode, PilotStatePacket::decode); + + public static void encode(PilotStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeFloat(msg.throttle); + buf.writeFloat(msg.reverse); + buf.writeFloat(msg.roll); + buf.writeBoolean(msg.hovering); + buf.writeByte(msg.keys); + } + + public static PilotStatePacket decode(FriendlyByteBuf buf) { + return new PilotStatePacket(buf.readVarInt(), buf.readFloat(), buf.readFloat(), buf.readFloat(), buf.readBoolean(), buf.readByte()); + } + + public static void handle(PilotStatePacket msg, ServerPlayer sender) { + if (sender == null) { + return; + } + // Only the player actually flying this aircraft may drive it. + if (sender.getVehicle() instanceof AircraftEntity aircraft + && aircraft.getId() == msg.entityId + && aircraft.getControllingPassenger() == sender + && Float.isFinite(msg.throttle) + && Float.isFinite(msg.reverse) + && Float.isFinite(msg.roll)) { + aircraft.acceptPilotState(msg.throttle, msg.reverse, msg.roll, msg.hovering, msg.keys); + } + } + + @Override + public Type type() { + return TYPE; + } +} From f3767bf0fa334de5489fbbe9fa6a23abcadc227e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:30 +0800 Subject: [PATCH 0788/1461] Add network/SpotPacket.java to the 1.21.11 shared sources --- .../mcheli/network/SpotPacket.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java new file mode 100644 index 00000000..1ee72286 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.weapon.Spotting; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; + +/** + * The attack key through a rangefinder, {@code MCH_PacketIndSpotEntity}: spot players and + * vehicles, spot mobs, or mark a point for the team. Spotting uses a charge; marking is free. + */ +public record SpotPacket(Mode mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("spot"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(SpotPacket::encode, SpotPacket::decode); + + /** The rangefinder's modes, in the order its mode key steps through them. */ + public enum Mode { + PLAYERS_AND_VEHICLES(Spotting.PLAYERS | Spotting.GROUND_VEHICLES | Spotting.HELICOPTERS | Spotting.PLANES), + MOBS(Spotting.OTHERS | Spotting.MONSTERS), + MARK_POINT(0); + + final int filter; + + Mode(int filter) { + this.filter = filter; + } + } + + public static void encode(SpotPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.mode.ordinal()); + } + + public static SpotPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Mode[] modes = Mode.values(); + if (ordinal < 0 || ordinal >= modes.length) { + throw new IllegalArgumentException("Unknown spot mode " + ordinal); + } + return new SpotPacket(modes[ordinal]); + } + + public static void handle(SpotPacket msg, ServerPlayer sender) { + if (sender == null || !RangefinderItem.canUse(sender)) { + return; + } + ItemStack stack = sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + stack = sender.getItemInHand(InteractionHand.OFF_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + return; + } + } + boolean done; + if (msg.mode == Mode.MARK_POINT) { + done = Spotting.markPoint(sender); + } else if (RangefinderItem.charges(stack) > 0) { + if (!sender.getAbilities().instabuild) { + // An empty rangefinder stays: it only needs redstone, unlike a worn out tool. + stack.setDamageValue(stack.getDamageValue() + 1); + } + done = Spotting.spot(sender, msg.mode.filter); + } else { + done = false; + } + ModSounds.playAt(sender, done ? "pi" : "ng", 1.0F, 1.0F); + } + + @Override + public Type type() { + return TYPE; + } +} From e7fc7e1821b14e1dfbccf46b7bae5c3e94121a96 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:31 +0800 Subject: [PATCH 0789/1461] Add network/TeamPacket.java to the 1.21.11 shared sources --- .../mcheli/network/TeamPacket.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java new file mode 100644 index 00000000..14b2db42 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A button of the team manager, {@code MCH_PacketIndMultiplayCommand}. Operators only; the + * server checks again. + * + * @param name the new team's name, for {@link Action#CREATE_TEAM} + * @param color the new team's colour, as a vanilla team colour id + */ +public record TeamPacket(Action action, String name, int color, boolean friendlyFire) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("team"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TeamPacket::encode, TeamPacket::decode); + + public enum Action { + PVP, + SHUFFLE, + CREATE_TEAM, + JUMP_TO_SPAWN, + DESTROY_ALL + } + + public TeamPacket(Action action) { + this(action, "", 0, false); + } + + public static void encode(TeamPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeUtf(msg.name, 16); + buf.writeVarInt(msg.color); + buf.writeBoolean(msg.friendlyFire); + } + + public static TeamPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown team action " + ordinal); + } + return new TeamPacket(actions[ordinal], buf.readUtf(16), buf.readVarInt(), buf.readBoolean()); + } + + public static void handle(TeamPacket msg, ServerPlayer sender) { + if (sender == null || !TeamManager.mayManage(sender)) { + return; + } + switch (msg.action) { + case PVP -> TeamManager.togglePvp(sender); + case SHUFFLE -> TeamManager.shuffle(sender); + case CREATE_TEAM -> TeamManager.create(sender, msg.name, msg.color, msg.friendlyFire); + case JUMP_TO_SPAWN -> TeamManager.jumpToSpawn(sender); + case DESTROY_ALL -> TeamManager.destroyAll(sender); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 187c5f6100b0dfc3147e44ffdf8fe80f6212a2d6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:31 +0800 Subject: [PATCH 0790/1461] Add network/TestFlightPacket.java to the 1.21.11 shared sources --- .../mcheli/network/TestFlightPacket.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java new file mode 100644 index 00000000..db12a390 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.workshop.TestFlight; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** The workshop's Test flight button: fly the vehicle just saved. The server checks who is asking. */ +public record TestFlightPacket(String vehicle) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("test_flight"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TestFlightPacket::encode, TestFlightPacket::decode); + private static final int MAX_NAME = 64; + + public static void encode(TestFlightPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.vehicle, MAX_NAME); + } + + public static TestFlightPacket decode(FriendlyByteBuf buf) { + return new TestFlightPacket(buf.readUtf(MAX_NAME)); + } + + public static void handle(TestFlightPacket msg, ServerPlayer sender) { + if (sender != null) { + TestFlight.start(sender, msg.vehicle); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 7c87a24bbf758f4067814d15942701c2388a6026 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:32 +0800 Subject: [PATCH 0791/1461] Add network/WeaponStatePacket.java to the 1.21.11 shared sources --- .../mcheli/network/WeaponStatePacket.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java new file mode 100644 index 00000000..292f643e --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to crew: the magazine and reserve of one weapon after it fired, reloaded or was + * resupplied. The original's {@code MCH_PacketNotifyAmmoNum}. + */ +public record WeaponStatePacket(int entityId, int weapon, int ammo, int rest) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("weapon_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(WeaponStatePacket::encode, WeaponStatePacket::decode); + + public static void encode(WeaponStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.ammo); + buf.writeVarInt(msg.rest); + } + + public static WeaponStatePacket decode(FriendlyByteBuf buf) { + return new WeaponStatePacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(WeaponStatePacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player != null && player.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.applyAmmo(msg.weapon, msg.ammo, msg.rest); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From a7d73d71436bff9fb0deae276dc20eccea6346f8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:32 +0800 Subject: [PATCH 0792/1461] Add particle/GlowParticleOptions.java to the 1.21.11 shared sources --- .../mcheli/particle/GlowParticleOptions.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java new file mode 100644 index 00000000..63a3577f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A soft ball of light blended on top of the scene: the flash of an explosion, a burning wreck, an + * ember. Spawned on the client only, but the codecs let {@code /particle mcheli:glow} show it too. + * + * @param color RGB of the light + * @param size radius in blocks at its largest + * @param lifetime ticks until it has faded out + * @param flicker whether it wavers like a fire rather than fading smoothly like a flash + */ +public record GlowParticleOptions(int color, float size, int lifetime, boolean flicker) implements ParticleOptions { + /** White hot at the centre of a blast. */ + public static final int FLASH = 0xFFE2B0; + /** Burning fuel and the fire inside the smoke. */ + public static final int FIRE = 0xFF8A2E; + /** Embers and the deeper red at the edge of a fireball. */ + public static final int EMBER = 0xFF5A1E; + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.RGB_COLOR_CODEC.fieldOf("color").forGetter(GlowParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(GlowParticleOptions::size), + ExtraCodecs.POSITIVE_INT.fieldOf("lifetime").forGetter(GlowParticleOptions::lifetime), + Codec.BOOL.optionalFieldOf("flicker", false).forGetter(GlowParticleOptions::flicker)) + .apply(i, GlowParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, GlowParticleOptions::color, + ByteBufCodecs.FLOAT, GlowParticleOptions::size, + ByteBufCodecs.VAR_INT, GlowParticleOptions::lifetime, + ByteBufCodecs.BOOL, GlowParticleOptions::flicker, + GlowParticleOptions::new); + + public static GlowParticleOptions flash(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, false); + } + + public static GlowParticleOptions fire(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, true); + } + + @Override + public ParticleType getType() { + return ModParticles.GLOW.get(); + } +} From ff684a0b6a182869f2b84ac95eedce296303d250 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:32 +0800 Subject: [PATCH 0793/1461] Add particle/SmokeParticleOptions.java to the 1.21.11 shared sources --- .../mcheli/particle/SmokeParticleOptions.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java new file mode 100644 index 00000000..29a0b0ef --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java @@ -0,0 +1,42 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A puff of coloured smoke that swells and pales as it drifts, {@code MCH_EntityParticleSmoke} + * with {@code toWhite} set. The smoke grenades use it to mark a spot. + * + * @param color RGB the puff starts with + * @param size the original's particle size; a puff ends up about {@code size / 5} blocks across + * @param lifetime ticks the puff lasts, or 0 for the grenade smoke's three to five seconds + */ +public record SmokeParticleOptions(int color, float size, int lifetime) implements ParticleOptions { + public SmokeParticleOptions(int color, float size) { + this(color, size, 0); + } + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.RGB_COLOR_CODEC.fieldOf("color").forGetter(SmokeParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(SmokeParticleOptions::size), + ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("lifetime", 0).forGetter(SmokeParticleOptions::lifetime)) + .apply(i, SmokeParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, SmokeParticleOptions::color, + ByteBufCodecs.FLOAT, SmokeParticleOptions::size, + ByteBufCodecs.VAR_INT, SmokeParticleOptions::lifetime, + SmokeParticleOptions::new); + + @Override + public ParticleType getType() { + return ModParticles.SMOKE.get(); + } +} From d9aa0a4a907080655cfdf0dc2add7ed3f27d76d4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:33 +0800 Subject: [PATCH 0794/1461] Ask the loader for block entity types on 1.21.11 --- .../jnhprojects/mcheli/platform/Platform.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Platform.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Platform.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Platform.java new file mode 100644 index 00000000..bbff8489 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Platform.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.function.BiFunction; + +/** + * What the shared code needs from the mod loader. Each loader module installs its implementation + * first thing during mod construction, before anything else of ours runs. + * + *

Kept small on purpose: registration and events go the other way, with the loader module + * calling into the shared code (see {@code Registration} and the {@code *Hooks} classes). + */ +public interface Platform { + + static Platform get() { + Platform platform = Holder.instance; + if (platform == null) { + throw new IllegalStateException("No mod loader platform installed yet"); + } + return platform; + } + + static void install(Platform platform) { + if (Holder.instance != null) { + throw new IllegalStateException("Platform already installed: " + Holder.instance.loaderName()); + } + Holder.instance = Objects.requireNonNull(platform); + } + + /** {@code forge}, {@code neoforge} or {@code fabric}, for logs and {@code /mcheli status}. */ + String loaderName(); + + /** The game directory, which holds the {@code mcheli} folder. */ + Path gameDir(); + + /** The config directory, which holds the client config. */ + Path configDir(); + + /** Our version as the loader read it from the mod metadata. */ + String modVersion(); + + /** Whether this is a physical client, as opposed to a dedicated server. */ + boolean isClient(); + + void sendToServer(CustomPacketPayload payload); + + void sendToPlayer(ServerPlayer player, CustomPacketPayload payload); + + /** + * A builder for one of our creative tabs, placed after {@code after}. Vanilla's builder is not + * public on every loader and only Forge and NeoForge can order tabs. + */ + CreativeModeTab.Builder creativeTab(ResourceKey after); + + /** A block entity type; vanilla's constructor is private on 1.21.11. */ + BlockEntityType blockEntityType(BiFunction factory, Block block); + + final class Holder { + private static volatile Platform instance; + + private Holder() { + } + } +} From ff4536435418147443e159285f55a791dfc252b0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:33 +0800 Subject: [PATCH 0795/1461] Add platform/Registered.java to the 1.21.11 shared sources --- .../mcheli/platform/Registered.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Registered.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Registered.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Registered.java new file mode 100644 index 00000000..3d1b9085 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Registered.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.resources.Identifier; + +import java.util.function.Supplier; + +/** + * A registry entry the shared code declares and the loader module registers, like Forge's + * {@code RegistryObject}. {@link #get()} works once the loader has registered it. + */ +public final class Registered implements Supplier { + private final Identifier id; + private volatile T value; + + Registered(Identifier id) { + this.id = id; + } + + public Identifier id() { + return id; + } + + @Override + public T get() { + T v = value; + if (v == null) { + throw new IllegalStateException("Registry entry " + id + " is not registered yet"); + } + return v; + } + + void bind(T value) { + this.value = value; + } +} From 4a87c1ce8ca30ca4e3331a4a99b34e8ae8e75785 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:34 +0800 Subject: [PATCH 0796/1461] Add platform/Registration.java to the 1.21.11 shared sources --- .../mcheli/platform/Registration.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Registration.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Registration.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Registration.java new file mode 100644 index 00000000..030a07dd --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/platform/Registration.java @@ -0,0 +1,80 @@ +package com.jnhprojects.mcheli.platform; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.Registry; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Every vanilla registry entry the mod adds. The shared registry classes declare their entries + * here during mod construction; the loader module then registers them its own way: Forge and + * NeoForge from their register event, Fabric straight away. + */ +public final class Registration { + private static final List> PENDING = new ArrayList<>(); + private static final Set>> REGISTRIES = new LinkedHashSet<>(); + + private Registration() { + } + + /** + * Declares an entry. + * + * @param factory makes the value; it gets the entry's key for {@code Item.Properties#setId} + * and the like + */ + public static synchronized Registered register(ResourceKey> registry, String name, + Function, T> factory) { + Identifier id = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, name); + Registered entry = new Registered<>(id); + PENDING.add(new Pending<>(registry, entry, factory)); + REGISTRIES.add(registry); + return entry; + } + + /** The registries with entries of ours, in the order they were first used. */ + public static synchronized List>> registries() { + return List.copyOf(REGISTRIES); + } + + /** + * A key from {@link #registries()} with its element type filled in, for the loader's generic + * register calls. The type is whatever the caller needs; entries are only ever handed to the + * registry they were declared for. + */ + @SuppressWarnings("unchecked") + public static ResourceKey> typed(ResourceKey> registry) { + return (ResourceKey>) registry; + } + + /** + * Creates our entries for one registry, in declaration order, and hands each to the loader. + * Called once per registry. + */ + public static synchronized void registerAll(ResourceKey> registry, BiConsumer sink) { + for (Pending pending : PENDING) { + if (pending.registry.equals(registry)) { + @SuppressWarnings("unchecked") + Pending ours = (Pending) pending; + ours.create(sink); + } + } + } + + private record Pending(ResourceKey> registry, Registered entry, + Function, T> factory) { + @SuppressWarnings("unchecked") + void create(BiConsumer sink) { + T value = factory.apply(ResourceKey.create((ResourceKey>) registry, entry.id())); + entry.bind(value); + sink.accept(entry.id(), value); + } + } +} From 017b01c27c0833b763d5159a6b4c929bf88188f4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:34 +0800 Subject: [PATCH 0797/1461] Get the drafting table's block entity type from the platform on 1.21.11 --- .../mcheli/registry/ModBlockEntities.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java new file mode 100644 index 00000000..00d40aed --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java @@ -0,0 +1,21 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; + +/** Our block entities: the drafting table's, which draws its model. */ +public final class ModBlockEntities { + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.BLOCK_ENTITY_TYPE, + "drafting_table", key -> Platform.get().blockEntityType(DraftingTableBlockEntity::new, ModBlocks.DRAFTING_TABLE.get())); + + private ModBlockEntities() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From f04449f58496692eb15e820d4e78c21e95b8154d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:35 +0800 Subject: [PATCH 0798/1461] Add registry/ModBlocks.java to the 1.21.11 shared sources --- .../mcheli/registry/ModBlocks.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java new file mode 100644 index 00000000..19995a92 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockBehaviour; + +/** Our blocks: the drafting table, lit or not. */ +public final class ModBlocks { + public static final Registered DRAFTING_TABLE = Registration.register(Registries.BLOCK, "drafting_table", + key -> new DraftingTableBlock(BlockBehaviour.Properties.of().setId(key) + .strength(0.2F) + .sound(SoundType.METAL) + .noOcclusion() + .lightLevel(state -> state.getValue(DraftingTableBlock.LIT) ? 15 : 0))); + + private ModBlocks() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From cc5499cfb7bd87944e5173d32700dc715fbad905 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:35 +0800 Subject: [PATCH 0799/1461] Add registry/ModCreativeTabs.java to the 1.21.11 shared sources --- .../mcheli/registry/ModCreativeTabs.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java new file mode 100644 index 00000000..870aa8b8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java @@ -0,0 +1,72 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** + * One tab per vehicle kind, listing a stack for each loaded definition of that kind, and one for + * the equipment and throwables. Those lists are read from the content registry whenever a tab is + * built, so reloaded content shows up. + */ +public final class ModCreativeTabs { + public static final Registered HELICOPTERS = register("helicopters", "helicopter", "ah-64", + CreativeModeTabs.SPAWN_EGGS); + public static final Registered PLANES = register("planes", "plane", "f-35b", key(HELICOPTERS)); + public static final Registered TANKS = register("tanks", "tank", "m1a2", key(PLANES)); + public static final Registered VEHICLES = register("vehicles", "vehicle", "mk15", key(TANKS)); + public static final Registered EQUIPMENT = Registration.register(Registries.CREATIVE_MODE_TAB, "equipment", + key -> Platform.get().creativeTab(key(VEHICLES)) + .title(Component.translatable("itemGroup.mcheli.equipment")) + .icon(() -> new ItemStack(ModItems.WRENCH.get())) + .displayItems((params, output) -> { + ModItems.EQUIPMENT.forEach(item -> output.accept(item.get())); + MCHeli.content().throwables().forEach(def -> output.accept(ThrowableItem.stackOf(def))); + }) + .build()); + + private ModCreativeTabs() { + } + + private static ResourceKey key(Registered tab) { + return ResourceKey.create(Registries.CREATIVE_MODE_TAB, tab.id()); + } + + private static Registered register(String name, String kind, String icon, ResourceKey after) { + return Registration.register(Registries.CREATIVE_MODE_TAB, name, key -> Platform.get().creativeTab(after) + .title(Component.translatable("itemGroup.mcheli." + name)) + .icon(() -> { + AircraftDefinition def = MCHeli.content().any(icon); + return def != null ? AircraftItem.stackOf(def) : new ItemStack(ModItems.forKind(kind)); + }) + .displayItems((params, output) -> { + for (AircraftDefinition def : MCHeli.content().all()) { + if (def.kind().equals(kind)) { + output.accept(AircraftItem.stackOf(def)); + } + } + }) + .build()); + } + + /** Our tabs, for rebuilding their contents after a content reload. */ + public static List all() { + return List.of(HELICOPTERS.get(), PLANES.get(), TANKS.get(), VEHICLES.get(), EQUIPMENT.get()); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 1fc67ca3ec6d4784f90019014f1d4c4d8624166d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:36 +0800 Subject: [PATCH 0800/1461] Add registry/ModDataComponents.java to the 1.21.11 shared sources --- .../mcheli/registry/ModDataComponents.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java new file mode 100644 index 00000000..377a5783 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.Codec; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.codec.ByteBufCodecs; + +public final class ModDataComponents { + /** + * The definition a vehicle item places, such as {@code ah-64}. Saved with the stack and sent + * to clients; the item itself only says which kind of vehicle it is. + */ + public static final Registered> VEHICLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "vehicle", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + /** The definition a throwable item throws, such as {@code m18_red}. */ + public static final Registered> THROWABLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "throwable", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + private ModDataComponents() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From a126912a33c887ea229d4b8b2de0f0d2447d7a2d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:36 +0800 Subject: [PATCH 0801/1461] Add registry/ModEntities.java to the 1.21.11 shared sources --- .../mcheli/registry/ModEntities.java | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java new file mode 100644 index 00000000..b6686ccd --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java @@ -0,0 +1,170 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.entity.ContainerEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.RappelEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; + +/** + * One entity type per vehicle kind. The concrete vehicle is stored on the entity, the same approach + * the original took, so adding content never changes the entity registry. + */ +public final class ModEntities { + public static final Registered> HELICOPTER = register("helicopter", HelicopterEntity::new); + public static final Registered> PLANE = register("plane", PlaneEntity::new); + public static final Registered> TANK = register("tank", TankEntity::new); + public static final Registered> VEHICLE = register("vehicle", GroundVehicleEntity::new); + + public static final Registered> BULLET = projectile("bullet", BulletEntity::new); + public static final Registered> ROCKET = projectile("rocket", RocketEntity::new); + public static final Registered> BOMB = projectile("bomb", BombEntity::new); + public static final Registered> AA_MISSILE = projectile("aa_missile", AAMissileEntity::new); + public static final Registered> AS_MISSILE = projectile("as_missile", ASMissileEntity::new); + public static final Registered> AT_MISSILE = projectile("at_missile", ATMissileEntity::new); + public static final Registered> TV_MISSILE = projectile("tv_missile", TvMissileEntity::new); + public static final Registered> TORPEDO = projectile("torpedo", TorpedoEntity::new); + public static final Registered> MARKER_ROCKET = projectile("marker_rocket", MarkerRocketEntity::new); + public static final Registered> DISPENSED_ITEM = projectile("dispensed_item", DispensedItemEntity::new); + public static final Registered> FLARE = Registration.register(Registries.ENTITY_TYPE, "flare", key -> EntityType.Builder + .of(FlareEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(2) + .fireImmune() + .noSave() + .build(key)); + public static final Registered> A10 = Registration.register(Registries.ENTITY_TYPE, "a10", key -> EntityType.Builder + .of(A10Entity::new, MobCategory.MISC) + .sized(5.0F, 3.0F) + .clientTrackingRange(32) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + + public static final Registered> PARACHUTE = Registration.register(Registries.ENTITY_TYPE, "parachute", key -> EntityType.Builder + .of(ParachuteEntity::new, MobCategory.MISC) + .sized(1.5F, 0.6F) + .clientTrackingRange(10) + .updateInterval(1) + .build(key)); + + public static final Registered> THROWABLE = Registration.register(Registries.ENTITY_TYPE, "throwable", key -> EntityType.Builder + .of(ThrowableEntity::new, MobCategory.MISC) + .sized(0.25F, 0.25F) + .clientTrackingRange(8) + .updateInterval(2) + .noSave() + .build(key)); + + public static final Registered> GLTD = Registration.register(Registries.ENTITY_TYPE, "gltd", key -> EntityType.Builder + .of(GltdEntity::new, MobCategory.MISC) + .sized(0.5F, 0.5F) + .clientTrackingRange(10) + .updateInterval(3) + .build(key)); + + public static final Registered> CONTAINER = Registration.register(Registries.ENTITY_TYPE, "container", key -> EntityType.Builder + .of(ContainerEntity::new, MobCategory.MISC) + .sized(2.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key)); + + public static final Registered> CHAIN = Registration.register(Registries.ENTITY_TYPE, "chain", key -> EntityType.Builder + .of(ChainEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(2) + .fireImmune() + .build(key)); + + public static final Registered> RAPPEL = Registration.register(Registries.ENTITY_TYPE, "rappel", key -> EntityType.Builder + .of(RappelEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + + public static final Registered> UAV_STATION = Registration.register(Registries.ENTITY_TYPE, "uav_station", key -> EntityType.Builder + .of(UavStationEntity::new, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key)); + + private ModEntities() { + } + + /** Projectiles are a metre cube like the original's, and never saved. */ + private static Registered> projectile(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + } + + private static Registered> register(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .build(key)); + } + + /** The entity type that hosts a definition of this kind. */ + public static EntityType typeFor(AircraftDefinition def) { + if (def instanceof HelicopterDefinition) { + return HELICOPTER.get(); + } + if (def instanceof PlaneDefinition) { + return PLANE.get(); + } + if (def instanceof TankDefinition) { + return TANK.get(); + } + return VEHICLE.get(); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 3889129f4bb4b633a417aa86f23c0ec8143e62bc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:37 +0800 Subject: [PATCH 0802/1461] Add registry/ModItems.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/registry/ModItems.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java new file mode 100644 index 00000000..e7415dfd --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java @@ -0,0 +1,102 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.item.LegacyAircraftItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.item.PlacedEntityItem; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.item.UavStationItem; +import com.jnhprojects.mcheli.item.WrenchItem; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; + +import java.util.List; +import java.util.Map; + +/** + * One item per vehicle kind; the stack says which vehicle (see {@link AircraftItem}). The 1.0.0 + * items, one per vehicle, are kept for the content present at startup so old worlds keep them. The + * equipment (fuel can, wrench, parachute and so on) has one item each, and the throwables share one. + */ +public final class ModItems { + public static final Registered HELICOPTER = kind("helicopter"); + public static final Registered PLANE = kind("plane"); + public static final Registered TANK = kind("tank"); + public static final Registered VEHICLE = kind("vehicle"); + + public static final Registered FUEL = Registration.register(Registries.ITEM, "fuel", key -> new FuelItem(new Item.Properties().setId(key))); + public static final Registered WRENCH = Registration.register(Registries.ITEM, "wrench", key -> new WrenchItem(new Item.Properties().setId(key))); + public static final Registered PARACHUTE = Registration.register(Registries.ITEM, "parachute", key -> new ParachuteItem(new Item.Properties().setId(key))); + public static final Registered GLTD = Registration.register(Registries.ITEM, "gltd", + key -> new PlacedEntityItem(ModEntities.GLTD, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered RANGEFINDER = Registration.register(Registries.ITEM, "rangefinder", key -> new RangefinderItem(new Item.Properties().setId(key))); + public static final Registered CONTAINER = Registration.register(Registries.ITEM, "container", + key -> new PlacedEntityItem(ModEntities.CONTAINER, new Item.Properties().setId(key))); + public static final Registered CHAIN = Registration.register(Registries.ITEM, "chain", key -> new ChainItem(new Item.Properties().setId(key))); + public static final Registered STINGER_MISSILE = Registration.register(Registries.ITEM, "fim92_bullet", key -> new Item(new Item.Properties().setId(key).stacksTo(16))); + public static final Registered STINGER = Registration.register(Registries.ITEM, "fim92", + key -> new LauncherItem("fim92", () -> ModItems.STINGER_MISSILE.get(), new Item.Properties().setId(key))); + public static final Registered JAVELIN_MISSILE = Registration.register(Registries.ITEM, "fgm148_bullet", key -> new Item(new Item.Properties().setId(key).stacksTo(16))); + public static final Registered JAVELIN = Registration.register(Registries.ITEM, "fgm148", + key -> new LauncherItem("fgm148", () -> ModItems.JAVELIN_MISSILE.get(), new Item.Properties().setId(key))); + public static final Registered UAV_STATION = Registration.register(Registries.ITEM, "uav_station", + key -> new UavStationItem(UavStationEntity.STATION, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered UAV_CONTROLLER = Registration.register(Registries.ITEM, "uav_station2", + key -> new UavStationItem(UavStationEntity.PORTABLE, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered DRAFTING_TABLE = Registration.register(Registries.ITEM, "drafting_table", + key -> new BlockItem(ModBlocks.DRAFTING_TABLE.get(), new Item.Properties().setId(key).useBlockDescriptionPrefix())); + /** Every throwable; the stack says which (see {@link ThrowableItem}). */ + public static final Registered THROWABLE = Registration.register(Registries.ITEM, "throwable", key -> new ThrowableItem(new Item.Properties().setId(key))); + + /** The equipment tab, in the order it lists them. */ + public static final List> EQUIPMENT = List.of(FUEL, WRENCH, PARACHUTE, GLTD, RANGEFINDER, CONTAINER, CHAIN, + STINGER, STINGER_MISSILE, JAVELIN, JAVELIN_MISSILE, UAV_STATION, UAV_CONTROLLER, DRAFTING_TABLE); + + private static final Map> BY_KIND = + Map.of("helicopter", HELICOPTER, "plane", PLANE, "tank", TANK, "vehicle", VEHICLE); + + private ModItems() { + } + + private static Registered kind(String kind) { + return Registration.register(Registries.ITEM, kind, key -> new AircraftItem(kind, new Item.Properties().setId(key).stacksTo(1))); + } + + /** + * Declares the entries, with a 1.0.0 item for each original vehicle in {@code content}; the + * loader module registers them. + */ + public static void init(ContentRegistry content) { + for (AircraftDefinition def : content.all()) { + String name = def.name(); + // Only the imported content existed in 1.0.0; pack vehicles never had their own item. + if (!def.pack().equals(ContentSource.ORIGINAL) || BY_KIND.containsKey(name) || EQUIPMENT.stream().anyMatch(e -> e.id().getPath().equals(name))) { + // 1.0.0 could not have had such an item either: it would have clashed the same way. + continue; + } + String kind = def.kind(); + Registration.register(Registries.ITEM, name, key -> new LegacyAircraftItem(kind, name, + new Item.Properties().setId(key).stacksTo(1))); + } + } + + /** The generic item of a vehicle kind ({@code helicopter}, {@code plane}, {@code tank} or {@code vehicle}). */ + public static Item forKind(String kind) { + Registered item = BY_KIND.get(kind); + if (item == null) { + throw new IllegalArgumentException("Unknown vehicle kind " + kind); + } + return item.get(); + } +} From 58866a1651d15540d46e9e604f25f88037193de6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:37 +0800 Subject: [PATCH 0803/1461] Add registry/ModMenus.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/registry/ModMenus.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java new file mode 100644 index 00000000..0dac0052 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; + +/** Our screens with slots. The cargo chest uses vanilla's chest menus and needs no entry here. */ +public final class ModMenus { + public static final Registered> VEHICLE = Registration.register(Registries.MENU, "vehicle", + key -> new MenuType<>(VehicleMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.MENU, "drafting_table", + key -> new MenuType<>(DraftingTableMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> UAV_STATION = Registration.register(Registries.MENU, "uav_station", + key -> new MenuType<>(UavStationMenu::new, FeatureFlags.VANILLA_SET)); + + private ModMenus() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 5ba2e897103e7337f71b0862110d8504fdb2cd80 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:38 +0800 Subject: [PATCH 0804/1461] Add registry/ModParticles.java to the 1.21.11 shared sources --- .../mcheli/registry/ModParticles.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java new file mode 100644 index 00000000..4ed53acd --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java @@ -0,0 +1,48 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; + +public final class ModParticles { + /** Ignores the particle limit: an explosion that loses its flash to the cap looks broken. */ + public static final Registered> GLOW = Registration.register(Registries.PARTICLE_TYPE, "glow", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return GlowParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return GlowParticleOptions.STREAM_CODEC; + } + }); + + /** Smoke grenades: a marker that vanishes under the particle limit is no marker. */ + public static final Registered> SMOKE = Registration.register(Registries.PARTICLE_TYPE, "smoke", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return SmokeParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return SmokeParticleOptions.STREAM_CODEC; + } + }); + + private ModParticles() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 298b71d4ffde247f6d0f011f122ca5a12d0f8635 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:38 +0800 Subject: [PATCH 0805/1461] Add team/Markers.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/team/Markers.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/team/Markers.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/team/Markers.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/team/Markers.java new file mode 100644 index 00000000..4d869373 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/team/Markers.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.team; + +import net.minecraft.core.BlockPos; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * What this client's team has spotted, {@code MCH_GuiTargetMarker}: the spotted entities, each for + * as long as it was spotted, and the last marked point. Filled by {@code MarkerPacket} on the + * client's main thread and drawn by the HUD; a dedicated server never touches it. + */ +public final class Markers { + /** A marked point stays up this long, unless another replaces it. */ + public static final int POINT_TICKS = 60 * 20; + + private static final Map SPOTTED = new HashMap<>(); + private static @Nullable BlockPos point; + private static int pointTicks; + + private Markers() { + } + + public static void spotted(int[] entities, int ticks) { + for (int id : entities) { + SPOTTED.merge(id, ticks, Math::max); + } + } + + public static void point(BlockPos pos) { + point = pos; + pointTicks = POINT_TICKS; + } + + /** Entity ids and ticks left. */ + public static Map spotted() { + return SPOTTED; + } + + public static @Nullable BlockPos point() { + return point; + } + + public static void tick() { + SPOTTED.replaceAll((id, ticks) -> ticks - 1); + SPOTTED.values().removeIf(ticks -> ticks <= 0); + if (pointTicks > 0 && --pointTicks == 0) { + point = null; + } + } + + public static void clear() { + SPOTTED.clear(); + point = null; + pointTicks = 0; + } +} From 47c140610b334601a5cdea604babbc1568d2205f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:39 +0800 Subject: [PATCH 0806/1461] Offer the ChatFormatting colours for teams on 1.21.11 --- .../jnhprojects/mcheli/team/TeamManager.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java new file mode 100644 index 00000000..88123ade --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java @@ -0,0 +1,116 @@ +package com.jnhprojects.mcheli.team; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.Permissions; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.gamerules.GameRules; +import net.minecraft.world.level.portal.TeleportTransition; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * The team manager's actions, {@code MCH_Multiplay} and {@code MCH_MultiplayPacketHandler}: for + * operators running a match. Teams are the vanilla scoreboard teams, so {@code /team} and the + * player list see the same ones. + */ +public final class TeamManager { + /** The colours a team can have. */ + public static final List COLORS = Arrays.stream(ChatFormatting.values()).filter(ChatFormatting::isColor).toList(); + + private TeamManager() { + } + + /** Operators only, as the original checked the scoreboard command's permission. */ + public static boolean mayManage(Player player) { + return player.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER); + } + + /** Switches the {@code pvp} game rule in every dimension and says which way it went. */ + public static void togglePvp(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + boolean pvp = !player.level().getGameRules().get(GameRules.PVP); + for (ServerLevel level : server.getAllLevels()) { + level.getGameRules().set(GameRules.PVP, pvp, server); + } + broadcast(server, Component.translatable(pvp ? "message.mcheli.team.pvp_on" : "message.mcheli.team.pvp_off")); + } + + /** {@code shuffleTeam}: every player online goes into one of the teams in turn, in random order. */ + public static void shuffle(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + Scoreboard scoreboard = server.getScoreboard(); + List teams = new ArrayList<>(scoreboard.getPlayerTeams()); + if (teams.isEmpty()) { + player.displayClientMessage(Component.translatable("message.mcheli.team.no_teams"), true); + return; + } + List players = new ArrayList<>(server.getPlayerList().getPlayers()); + Collections.shuffle(players); + Collections.shuffle(teams); + for (int i = 0; i < players.size(); i++) { + scoreboard.addPlayerToTeam(players.get(i).getScoreboardName(), teams.get(i % teams.size())); + } + broadcast(server, Component.translatable("message.mcheli.team.shuffled", teams.size())); + } + + /** The original's create team screen: a name, a colour and friendly fire. */ + public static void create(ServerPlayer player, String name, int color, boolean friendlyFire) { + Scoreboard scoreboard = player.level().getServer().getScoreboard(); + String trimmed = name.trim(); + if (trimmed.isEmpty() || trimmed.length() > 16 || trimmed.contains(" ") || scoreboard.getPlayerTeam(trimmed) != null) { + player.displayClientMessage(Component.translatable("message.mcheli.team.bad_name", trimmed), true); + return; + } + PlayerTeam team = scoreboard.addPlayerTeam(trimmed); + team.setColor(COLORS.get(Math.floorMod(color, COLORS.size()))); + team.setAllowFriendlyFire(friendlyFire); + player.displayClientMessage(Component.translatable("message.mcheli.team.created", trimmed), true); + } + + /** {@code jumpSpawnPoint}: every player back to where they would respawn. */ + public static void jumpToSpawn(ServerPlayer player) { + for (ServerPlayer target : List.copyOf(player.level().getServer().getPlayerList().getPlayers())) { + target.stopRiding(); + target.teleport(target.findRespawnPositionAndUseSpawnBlock(false, TeleportTransition.DO_NOTHING)); + } + } + + /** {@code destoryAllAircraft}: every loaded vehicle is gone, its crew left standing. */ + public static void destroyAll(ServerPlayer player) { + int count = 0; + for (ServerLevel level : player.level().getServer().getAllLevels()) { + for (AircraftEntity vehicle : level.getEntities(EntityTypeTest.forClass(AircraftEntity.class), e -> true)) { + vehicle.ejectPassengers(); + vehicle.discard(); + count++; + } + } + player.displayClientMessage(Component.translatable("message.mcheli.team.destroyed", count), true); + } + + /** {@code entity} and everyone on its team. */ + public static List teammates(ServerPlayer entity) { + List out = new ArrayList<>(); + for (ServerPlayer player : entity.level().getServer().getPlayerList().getPlayers()) { + if (player == entity || player.isAlliedTo(entity)) { + out.add(player); + } + } + return out; + } + + private static void broadcast(MinecraftServer server, Component message) { + server.getPlayerList().broadcastSystemMessage(message, false); + } +} From 491449c171ac98f94089f42f702a9e608d129bb6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:39 +0800 Subject: [PATCH 0807/1461] Add weapon/CasStrike.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/weapon/CasStrike.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java new file mode 100644 index 00000000..e78278a8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java @@ -0,0 +1,125 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A close air support run, {@code MCH_WeaponCAS}: three A-10 passes over a spot, 45 ticks apart, + * the second and third offset to either side. Vehicles with a CAS weapon drive theirs from their + * {@link WeaponSet}; the GLTD keeps one of these. + */ +public final class CasStrike { + /** Ticks from the call to the first pass: the time it takes the A-10 to arrive. */ + public static final int FIRST_PASS = 50; + private static final int BETWEEN_PASSES = 45; + private static final int PASSES = 3; + private static final RandomSource RANDOM = RandomSource.create(); + + private final String weapon; + private double x; + private double y; + private double z; + private int direction; + private @Nullable Entity shooter; + private @Nullable Entity user; + private int passes = PASSES; + private int ticksToNext; + + /** @param weapon the CAS weapon definition the A-10s fire, such as {@code a10gau8} */ + public CasStrike(String weapon) { + this.weapon = weapon; + } + + /** Whether passes are still to come. */ + public boolean isRunning() { + return passes < PASSES; + } + + /** + * Calls the strike on {@code at}. The A-10 comes in from one side of the caller's view, + * picked at random, never straight from behind or towards them. + */ + public void start(Vec3 at, float callerYaw, @Nullable Entity shooter, @Nullable Entity user) { + x = at.x; + y = at.y; + z = at.z; + direction = Math.floorMod(approach(callerYaw), 4); + this.shooter = shooter; + this.user = user; + passes = 0; + ticksToNext = FIRST_PASS; + } + + /** The original's approach: the quarter the caller faces, turned one way or the other. */ + static int approach(float callerYaw) { + int facing = (int) Mth.positiveModulo(callerYaw + 45.0F, 360.0F) / 90; + return facing + (RANDOM.nextBoolean() ? -1 : 1); + } + + public void tick(ServerLevel level, WeaponDefinition def) { + if (passes >= PASSES || --ticksToNext > 0) { + return; + } + Vec3 offset = passOffset(passes, direction); + spawnA10(level, weapon, def, direction, x + offset.x, y + 20.0, z + offset.z, shooter, user); + passes++; + ticksToNext = BETWEEN_PASSES; + } + + /** The first pass is dead on; the second strays to one side of the track, the third to the other. */ + static Vec3 passOffset(int pass, int direction) { + if (pass == 0) { + return Vec3.ZERO; + } + double sign = pass == 1 ? 1.0 : -1.0; + if (direction % 2 == 0) { + return new Vec3(RANDOM.nextDouble() * 10.0 * sign, 0.0, (RANDOM.nextDouble() - 0.5) * 10.0); + } + return new Vec3((RANDOM.nextDouble() - 0.5) * 10.0, 0.0, RANDOM.nextDouble() * 10.0 * sign); + } + + /** One A-10, starting 90 blocks short of the spot and flying over it at three blocks a tick. */ + static void spawnA10(ServerLevel level, String weapon, WeaponDefinition def, int direction, double x, double y, double z, + @Nullable Entity shooter, @Nullable Entity user) { + double mx = 0.0; + double mz = 0.0; + switch (direction) { + case 0 -> { + mz = 3.0; + z -= 90.0; + } + case 1 -> { + mx = -3.0; + x += 90.0; + } + case 2 -> { + mz = -3.0; + z += 90.0; + } + default -> { + mx = 3.0; + x -= 90.0; + } + } + A10Entity a10 = ModEntities.A10.get().create(level, EntitySpawnReason.TRIGGERED); + if (a10 == null) { + return; + } + a10.setPos(x, y, z); + a10.setup(weapon, direction, mx, mz, shooter, user); + a10.power = def.power; + a10.explosionPower = def.explosion; + a10.acceleration = def.acceleration; + level.addFreshEntity(a10); + ModSounds.playAt(level, x, y, z, "a-10_snd", 150.0F, 1.0F); + } +} From 59165dad8853087bc1b2904f3a6331a036406f19 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:39 +0800 Subject: [PATCH 0808/1461] Add weapon/Combat.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/weapon/Combat.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java new file mode 100644 index 00000000..204fe93f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +/** Damage rules shared by bullets and explosions. */ +public final class Combat { + private Combat() { + } + + /** The {@code DamageFactor} kind a target belongs to, or {@code null} for anything else. */ + public static @Nullable String kind(Entity entity) { + return switch (entity) { + case Player p -> "player"; + case HelicopterEntity h -> "helicopter"; + case PlaneEntity p -> "plane"; + case TankEntity t -> "tank"; + case GroundVehicleEntity v -> "vehicle"; + default -> null; + }; + } + + public static float factor(@Nullable WeaponDefinition def, Entity target) { + String kind = kind(target); + return def != null && kind != null ? def.damageFactor(kind) : 1.0F; + } + + /** + * Deals weapon damage. The original scaled every living target's invulnerability window by + * {@code HurtResistantTime}, zero by default, so each round of a burst lands. + */ + public static boolean hurt(Entity target, DamageSource source, float damage, @Nullable WeaponDefinition def) { + if (!(target.level() instanceof ServerLevel level)) { + return false; + } + if (target instanceof LivingEntity living) { + living.invulnerableTime = 0; + } + return target.hurtServer(level, source, damage * factor(def, target)); + } + + /** The convention tag every loader fills, so modded panes count too. */ + private static final TagKey GLASS_PANES = TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("c", "glass_panes")); + + /** + * {@code BulletBreakableBlocks} with its default list: panes, grass, flowers, vines, wheat, + * sugar cane and lily pads, which a round shoots through. + */ + public static boolean isBulletBreakable(BlockState state) { + return state.is(GLASS_PANES) + || state.is(BlockTags.FLOWERS) + || state.is(Blocks.SHORT_GRASS) || state.is(Blocks.TALL_GRASS) + || state.is(Blocks.FERN) || state.is(Blocks.LARGE_FERN) + || state.is(Blocks.VINE) || state.is(Blocks.WHEAT) + || state.is(Blocks.SUGAR_CANE) || state.is(Blocks.LILY_PAD); + } +} From ea8fa842f964ff9c032d02a2bc5a7b80f71fa476 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:40 +0800 Subject: [PATCH 0809/1461] Add weapon/ExplosionEffect.java to the 1.21.11 shared sources --- .../mcheli/weapon/ExplosionEffect.java | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java new file mode 100644 index 00000000..4f4b0dc8 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java @@ -0,0 +1,232 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.HashSet; +import java.util.Set; + +/** + * The look of an MCHeli explosion, ported from {@code MCH_Explosion.effectExplosion} and + * {@code effectExplosionInWater}: a fireball, ground dust thrown up from the crater, and columns + * and rings of smoke that hang around for seconds. It runs on each client that receives the + * explosion; the burning debris arcs are real flares spawned by the server. + */ +public final class ExplosionEffect { + /** The largest crash fireball drawn; beyond this the particle count gets out of hand. */ + public static final float MAX_CRASH_SIZE = 12.0F; + /** The widest explosion flash, in blocks; past this it washes out the whole screen. */ + private static final float MAX_FLASH_RADIUS = 30.0F; + + private ExplosionEffect() { + } + + /** + * Not in the original: a crashing vehicle goes up in a far bigger show than its fuel explosion + * alone makes. The same shell of smoke as a missile hit in mid-air, a cluster of fireballs + * around it, a plume of smoke that keeps climbing for seconds and burning fuel thrown about. + * The fire glows for several seconds afterwards, which at night lights up the smoke above it. + */ + public static void crash(Level level, double x, double y, double z, float size) { + play(level, x, y, z, size, false); + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, Math.min(size * 3.0F, MAX_FLASH_RADIUS), 20), + true, x, y + size * 0.3, z, 0.0, 0.0, 0.0); + int fireballs = Math.max(3, (int) (size / 2.0F)); + for (int i = 0; i < fireballs; i++) { + double r = size * 0.5 * random.nextDouble(); + double a = random.nextDouble() * Math.PI * 2.0; + double fx = x + Math.cos(a) * r; + double fy = y + random.nextDouble() * size * 0.4; + double fz = z + Math.sin(a) * r; + level.addAlwaysVisibleParticle(ParticleTypes.EXPLOSION_EMITTER, true, fx, fy, fz, 1.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size * (0.4F + random.nextFloat() * 0.3F), + 80 + random.nextInt(60)), true, fx, fy, fz, 0.0, 0.01, 0.0); + } + for (int i = 0; i < size * 4; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + x + (random.nextDouble() - 0.5) * size * 0.6, y + random.nextDouble() * 2.0, z + (random.nextDouble() - 0.5) * size * 0.6, + (random.nextDouble() - 0.5) * 0.02, 0.05 + random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.02); + } + for (int i = 0; i < size * 10; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = (0.2 + random.nextDouble() * 0.4) * size / 6.0; + double up = random.nextDouble() * speed; + level.addAlwaysVisibleParticle(random.nextInt(4) == 0 ? ParticleTypes.LAVA : ParticleTypes.FLAME, true, x, y + 0.5, z, + Math.cos(a) * speed, up, Math.sin(a) * speed); + if (i % 4 == 0) { + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.5F + random.nextFloat() * 0.4F, + 20 + random.nextInt(20)), true, x, y + 0.5, z, Math.cos(a) * speed, up, Math.sin(a) * speed); + } + } + } + + /** + * Not in the original: the light of the blast itself, a white hot flash inside a wider red + * glow, and a few embers thrown out. Faint by day; at night it lights up the smoke. + */ + private static void flash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + float radius = Math.min(2.0F + size * 1.5F, MAX_FLASH_RADIUS); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, radius, 8 + (int) size), true, x, y, z, 0.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.EMBER, Math.min(radius * 1.6F, MAX_FLASH_RADIUS), + 14 + (int) (size * 2.0F)), true, x, y, z, 0.0, 0.0, 0.0); + int embers = Math.min((int) (size * 2.0F), 24); + for (int i = 0; i < embers; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = 0.2 + random.nextDouble() * 0.3 * Math.min(size, 4.0F); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 15 + random.nextInt(15)), true, x, y, z, Math.cos(a) * speed, 0.1 + random.nextDouble() * speed, Math.sin(a) * speed); + } + } + + public static void play(Level level, double x, double y, double z, float size, boolean inWater) { + if (size <= 0.0F) { + return; + } + if (inWater) { + splash(level, x, y, z, size); + return; + } + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(size >= 2.0F ? ParticleTypes.EXPLOSION_EMITTER : ParticleTypes.EXPLOSION, true, x, y, z, 1.0, 0.0, 0.0); + flash(level, x, y, z, size); + + int count = 0; + int puffEvery = (int) Math.max(size, 4.0F); + for (BlockPos pos : affectedBlocks(level, x, y, z, size)) { + count++; + double bx = pos.getX() + random.nextFloat(); + double by = pos.getY() + random.nextFloat(); + double bz = pos.getZ() + random.nextFloat(); + double mx = bx - x; + double my = by - y; + double mz = bz - z; + double d = Math.sqrt(mx * mx + my * my + mz * mz); + if (d == 0.0) { + continue; + } + double push = 0.5 / (d / size + 0.1) * (random.nextFloat() * random.nextFloat() + 0.3F) * 0.5; + mx = mx / d * push; + my = my / d * push; + mz = mz / d * push; + double px = (bx + x) / 2.0; + double py = (by + y) / 2.0; + double pz = (bz + z) / 2.0; + double angle = Math.toRadians(random.nextInt(360)); + + if (count % 4 == 0) { + // Dirt and stone thrown up from the ground under the blast. + BlockState ground = level.getBlockState(BlockPos.containing(px + 0.5, py - 0.5, pz + 0.5)); + if (!ground.isAir()) { + float force = Math.min(size / 3.0F, 2.0F) * (0.5F + random.nextFloat() * 0.5F); + BlockParticleOption dust = new BlockParticleOption(ParticleTypes.BLOCK, ground); + for (int i = 0; i < 3; i++) { + level.addAlwaysVisibleParticle(dust, true, px, py + 1.0, pz, + Math.sin(angle) * force * 0.5, 0.5 + my / 5.0 * force, Math.cos(angle) * force * 0.5); + } + } + } + if (size <= 1.0F || count % puffEvery == 0) { + // Smoke either shoots up as a column or rolls out along the ground. + if (random.nextBoolean()) { + my *= 3.0; + mx *= 0.1; + mz *= 0.1; + } else { + my *= 0.2; + mx *= 3.0; + mz *= 3.0; + } + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, px, py, pz, mx * 0.3, my * 0.3, mz * 0.3); + for (int i = 0; i < 2; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, px, py, pz, mx, my, mz); + } + if (random.nextInt(3) == 0) { + level.addAlwaysVisibleParticle(ParticleTypes.FLAME, true, px, py, pz, mx * 0.5, my * 0.5, mz * 0.5); + } + } + } + } + + /** + * The blocks the original's effect ray marched through: sixteen by sixteen rays from the + * centre, each running until block resistance used up its strength. + */ + @SuppressWarnings("deprecation") // The plain block resistance is what the original used here. + private static Set affectedBlocks(Level level, double x, double y, double z, float size) { + Set out = new HashSet<>(); + RandomSource random = level.getRandom(); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + for (int k = 0; k < 16; k++) { + if (i != 0 && i != 15 && j != 0 && j != 15 && k != 0 && k != 15) { + continue; + } + double dx = i / 15.0F * 2.0F - 1.0F; + double dy = j / 15.0F * 2.0F - 1.0F; + double dz = k / 15.0F * 2.0F - 1.0F; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + dx /= len; + dy /= len; + dz /= len; + float strength = size * (0.7F + random.nextFloat() * 0.6F); + double cx = x; + double cy = y; + double cz = z; + for (float step = 0.3F; strength > 0.0F; strength -= step * 0.75F) { + cursor.set(Mth.floor(cx), Mth.floor(cy), Mth.floor(cz)); + BlockState state = level.getBlockState(cursor); + if (!state.isAir()) { + strength -= (state.getBlock().getExplosionResistance() + 0.3F) * step; + } + if (strength > 0.0F) { + out.add(cursor.immutable()); + } + cx += dx * step; + cy += dy * step; + cz += dz * step; + } + } + } + } + return out; + } + + /** {@code effectExplosionInWater}: a dome of spray from the water around the charge. */ + private static void splash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + int range = Math.max((int) (size + 0.5), 1); + int ex = (int) (x + 0.5); + int ey = (int) (y + 0.5); + int ez = (int) (z + 0.5); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int dy = -range; dy <= range; dy++) { + for (int dx = -range; dx <= range; dx++) { + for (int dz = -range; dz <= range; dz++) { + if (dx * dx + dy * dy + dz * dz >= range * range) { + continue; + } + cursor.set(ex + dx, ey + dy, ez + dz); + if (!level.getFluidState(cursor).is(FluidTags.WATER)) { + continue; + } + for (int n = random.nextInt(2); n > 0; n--) { + double up = 1.0 - Math.sqrt(dx * dx + dz * dz) / range + random.nextFloat() * 0.4 * range * 0.4; + level.addAlwaysVisibleParticle(ParticleTypes.SPLASH, true, cursor.getX(), cursor.getY() + 1.0, cursor.getZ(), + (double) dx / range * (random.nextFloat() - 0.2), up, (double) dz / range * (random.nextFloat() - 0.2)); + level.addAlwaysVisibleParticle(ParticleTypes.BUBBLE, true, cursor.getX(), cursor.getY(), cursor.getZ(), 0.0, up * 0.5, 0.0); + } + } + } + } + } +} From 8d942006d6c5d56eaf741aee3693717bf017e12f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:40 +0800 Subject: [PATCH 0810/1461] Add weapon/Explosions.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/weapon/Explosions.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java new file mode 100644 index 00000000..f7fe3e89 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java @@ -0,0 +1,217 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.network.ExplosionEffectPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerExplosion; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Port of {@code MCH_Explosion}. + * + *

The original explosion had two sizes: one for blocks and one, doubled, for entities, with its + * own damage curve and the weapon's per-target {@code DamageFactor}. Entity damage is computed + * here as it was; the blocks, particles and sound go through a vanilla explosion of the block size, + * so mob griefing rules, protection mods and the client effects all behave as players expect. + */ +public final class Explosions { + /** Players this far away still see the smoke of an explosion. */ + private static final double EFFECT_RANGE = 256.0; + /** Share of the blast knockback an empty vehicle takes; they are far heavier than a mob. */ + private static final double VEHICLE_PUSH = 0.25; + + private Explosions() { + } + + /** What an explosion request needs besides its position and size. */ + public record Params(@Nullable Entity exploder, @Nullable Entity cause, float size, float blockSize, + boolean flaming, boolean destroyBlocks, int fireSeconds, boolean inWater, + @Nullable WeaponDefinition weapon) { + } + + /** + * @return whether any entity took damage, which the shooter hears as a hit confirmation + */ + public static boolean explode(ServerLevel level, double x, double y, double z, Params p) { + Vec3 center = new Vec3(x, y, z); + boolean hit = damageEntities(level, center, p); + + boolean breakBlocks = p.destroyBlocks && p.blockSize > 0.0F && ServerConfig.explosionsBreakBlocks(); + float radius = breakBlocks ? p.blockSize : Math.max(p.size, 0.5F); + level.explode(p.exploder, explosionSource(level, p), new Calculator(p.inWater, level), + x, y, z, radius, p.flaming && breakBlocks && ServerConfig.explosionsStartFires(), + breakBlocks ? Level.ExplosionInteraction.MOB : Level.ExplosionInteraction.NONE); + ModNetwork.sendNear(level, x, y, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y, z, p.size, p.inWater)); + if (!p.inWater) { + throwDebris(level, x, y, z, p.size); + } + return hit; + } + + /** + * The look of a vehicle going up when it crashes: no damage or blocks of its own (the crash + * explosion does those), but the {@link ExplosionEffect#crash big fireball} on every nearby + * client and twice the burning debris. Drawn a little above the impact so its smoke spreads + * into the air the way a missile hit's does, rather than straight into the ground. + */ + public static void crashFireball(ServerLevel level, double x, double y, double z, float size) { + float visual = Math.clamp(size * 2.0F, 6.0F, ExplosionEffect.MAX_CRASH_SIZE); + double lift = visual * 0.25; + ModNetwork.sendNear(level, x, y + lift, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y + lift, z, visual, false, true)); + throwDebris(level, x, y, z, visual, 24); + } + + private static void throwDebris(ServerLevel level, double x, double y, double z, float size) { + throwDebris(level, x, y, z, size, 12); + } + + /** + * Big blasts throw burning debris that arcs away trailing smoke, which the original made of + * flares. One piece per point of size, up to {@code maxPieces}. + */ + private static void throwDebris(ServerLevel level, double x, double y, double z, float size, int maxPieces) { + if (size < 4.0F) { + return; + } + int pieces = Math.min((int) size * maxPieces / 12, maxPieces); + for (int i = 0; i < pieces; i++) { + double angle = Math.toRadians(level.getRandom().nextInt(360)); + double speed = Math.min(size / 12.0F, 0.6) * (0.5F + level.getRandom().nextFloat() * 0.5F); + FlareEntity debris = ModEntities.FLARE.get().create(level, EntitySpawnReason.TRIGGERED); + if (debris != null) { + double spread = size / 2.0; + debris.launch(x + (level.getRandom().nextDouble() - 0.5) * spread, y + 2.0, z + (level.getRandom().nextDouble() - 0.5) * spread, + Math.sin(angle) * speed, (1.0 + level.getRandom().nextFloat() * 0.2) * speed, Math.cos(angle) * speed, 2.0F, 0, false); + level.addFreshEntity(debris); + } + } + } + + private static DamageSource explosionSource(ServerLevel level, Params p) { + return level.damageSources().explosion(p.exploder, p.cause); + } + + /** {@code doExplosionA} for entities: twice the block size, the 1.7.10 damage curve and knockback. */ + private static boolean damageEntities(ServerLevel level, Vec3 center, Params p) { + float size = p.size * 2.0F; + if (size <= 0.0F) { + return false; + } + AABB area = new AABB(center, center).inflate(size + 1.0); + DamageSource source = explosionSource(level, p); + boolean hit = false; + for (Entity entity : level.getEntities(p.exploder, area)) { + double d7 = Math.sqrt(entity.distanceToSqr(center)) / size; + if (d7 > 1.0) { + continue; + } + double dx = entity.getX() - center.x; + double dy = entity.getEyeY() - center.y; + double dz = entity.getZ() - center.z; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist == 0.0) { + continue; + } + dx /= dist; + dy /= dist; + dz /= dist; + double exposure = ServerExplosion.getSeenPercent(center, entity); + double impact = (1.0 - d7) * exposure; + float damage = (int) ((impact * impact + impact) / 2.0 * 8.0 * size + 1.0); + if (damage > 0.0F && !(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) + && !(entity instanceof FallingBlockEntity)) { + // Our own rounds caught in the blast do not count as hitting something. + if (!(entity instanceof ProjectileEntity bullet) || bullet.user() != p.cause) { + hit = true; + } + } + Combat.hurt(entity, source, damage, p.weapon); + if (entity instanceof AircraftEntity vehicle) { + // Not in the original: a vehicle someone is driving is simulated on their client, + // so a push here only snaps it back a step. An empty one still rocks a little. + if (vehicle.getControllingPassenger() == null) { + entity.push(dx * impact * VEHICLE_PUSH * 0.4, dy * impact * VEHICLE_PUSH * 0.1, dz * impact * VEHICLE_PUSH * 0.4); + } + } else if (!(entity instanceof ProjectileEntity)) { + entity.push(dx * impact * 0.4, dy * impact * 0.1, dz * impact * 0.4); + } + if (damage > 0.0F && p.fireSeconds > 0) { + double fire = 1.0 - dist / size; + if (fire > 0.0) { + entity.igniteForSeconds((float) (fire * p.fireSeconds)); + } + } + } + return hit; + } + + /** + * Vanilla explosion settings for the block pass: no entity damage or knockback (done above), + * and under water the blocks crumble the way the original's water charges made them. + */ + private static final class Calculator extends ExplosionDamageCalculator { + private final boolean inWater; + private final Level level; + + Calculator(boolean inWater, Level level) { + this.inWater = inWater; + this.level = level; + } + + // Vanilla's own calculator reads the block's resistance the same deprecated way. + @SuppressWarnings("deprecation") + @Override + public Optional getBlockExplosionResistance(Explosion explosion, BlockGetter getter, BlockPos pos, + BlockState block, FluidState fluid) { + if (!inWater) { + return super.getBlockExplosionResistance(explosion, getter, pos, block, fluid); + } + if (block.isAir()) { + return Optional.empty(); + } + float resistance = block.getBlock().getExplosionResistance(); + return Optional.of(resistance * (level.getRandom().nextFloat() * 0.2F + 0.2F)); + } + + @Override + public boolean shouldDamageEntity(Explosion explosion, Entity entity) { + return false; + } + + @Override + public float getKnockbackMultiplier(Entity entity) { + return 0.0F; + } + } + + /** {@code playExplosionSound}: the bang of a round that has no explosive charge. */ + public static void playSound(Level level, double x, double y, double z) { + float pitch = (1.0F + (level.getRandom().nextFloat() - level.getRandom().nextFloat()) * 0.2F) * 0.7F; + level.playSound(null, x, y, z, SoundEvents.GENERIC_EXPLODE.value(), SoundSource.BLOCKS, 4.0F, pitch); + } +} From 0b6779468c3558dbd9d0bc9ac6f4161f177ee4f2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:41 +0800 Subject: [PATCH 0811/1461] Add weapon/FlareDispenser.java to the 1.21.11 shared sources --- .../mcheli/weapon/FlareDispenser.java | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java new file mode 100644 index 00000000..12ddc658 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java @@ -0,0 +1,195 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.level.block.LevelEvent; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and smoke dischargers, ported from {@code MCH_Flare} and the flare half of + * {@code MCH_MissileDetector}. While a salvo burns, every missile homing on the vehicle within 60 + * blocks is decoyed. + * + *

The ten {@code FlareType} values pick a pattern: 1 random scatter, 2 and 3 to the sides and + * rear, 4 and 5 trailing, 10 a ring of smoke grenades. Unknown types fall back to type 1. + */ +public final class FlareDispenser { + private static final int DECOY_RANGE = 60; + + /** {@code num, interval, tickWait, tickEnable, numFlareMax} per type. */ + private static final int[][] TYPES = new int[11][]; + + static { + TYPES[1] = new int[]{1, 3, 200, 100, 16}; + TYPES[2] = new int[]{3, 5, 300, 200, 16}; + TYPES[3] = new int[]{2, 3, 200, 100, 16}; + TYPES[4] = new int[]{1, 3, 200, 100, 16}; + TYPES[5] = new int[]{2, 3, 200, 100, 16}; + TYPES[10] = new int[]{8, 1, 250, 60, 1}; + for (int i : new int[]{0, 6, 7, 8, 9}) { + TYPES[i] = TYPES[1]; + } + } + + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final RandomSource random = RandomSource.create(); + private int tick; + private int fired; + private int type; + private int typeIndex; + + FlareDispenser(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + } + + public boolean isFitted() { + return !def.flareTypes.isEmpty(); + } + + /** Waiting out the reload of the last salvo. */ + public boolean isPreparing() { + return tick != 0; + } + + /** The salvo is still burning, so incoming missiles are being decoyed. */ + public boolean isActive() { + int[] p = params(type); + return tick != 0 && tick > p[2] - p[3]; + } + + public int ticksLeft() { + return tick; + } + + public int cooldown() { + return params(type)[2]; + } + + public int currentType() { + return def.flareTypes.isEmpty() ? 0 : def.flareTypes.get(typeIndex % def.flareTypes.size()); + } + + public void nextType() { + if (!def.flareTypes.isEmpty()) { + typeIndex = (typeIndex + 1) % def.flareTypes.size(); + } + } + + /** {@code useFlare}: starts a salvo of the current type if the last one has finished. */ + public boolean use() { + if (!isFitted() || tick != 0 || vehicle.isDestroyed()) { + return false; + } + type = currentType(); + fired = 0; + // The client waited 50 ticks longer before showing the dispenser as ready again. + tick = params(type)[2] + (vehicle.level().isClientSide() ? 50 : 0); + return true; + } + + private static int[] params(int type) { + return type >= 0 && type < TYPES.length && TYPES[type] != null ? TYPES[type] : TYPES[1]; + } + + void tick() { + if (type <= 0 || vehicle.isRemoved()) { + return; + } + if (tick > 0) { + tick--; + } + int[] p = params(type); + if (!vehicle.level().isClientSide()) { + if (tick > 0 && tick % p[1] == 0 && fired < p[4]) { + Vec3 at = vehicle.toWorld(def.flarePosition); + spawnSalvo(new Vec3(at.x + vehicle.xo - vehicle.getX(), at.y + vehicle.yo - vehicle.getY(), at.z + vehicle.zo - vehicle.getZ())); + } + if (isActive()) { + decoyMissiles(); + } + } + } + + private void spawnSalvo(Vec3 at) { + fired++; + int[] p = params(type); + Vec3 m = vehicle.getDeltaMovement(); + double x = at.x - m.x * 2.0; + double y = at.y - m.y * 2.0 - 1.0; + double z = at.z - m.z * 2.0; + vehicle.level().levelEvent(LevelEvent.SOUND_DISPENSER_PROJECTILE_LAUNCH, net.minecraft.core.BlockPos.containing(x, y, z), 0); + for (int i = 0; i < p[0]; i++) { + double tx = 0.0; + double ty = m.y; + double tz = 0.0; + int fuse = 0; + double r = vehicle.getYRot(); + double fy = y; + switch (type) { + case 1 -> { + tx = Mth.sin(random.nextFloat() * 360.0F); + tz = Mth.cos(random.nextFloat() * 360.0F); + } + case 2, 3 -> { + if (i == 0) { + r += 90.0; + } else if (i == 1) { + r -= 90.0; + } else if (i == 2) { + r += 180.0; + } + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 0.6; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 0.6; + } + case 4 -> { + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 1.3; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 1.3; + } + case 5 -> { + r = Math.toRadians(r); + tx = (-Math.sin(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + tz = (Math.cos(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + } + default -> { + } + } + tx += m.x; + ty += m.y / 2.0; + tz += m.z; + if (type == 10) { + r += 360.0 / p[0] / 2.0 + i * (360.0 / p[0]); + r = Math.toRadians(r); + tx = -Math.sin(r) * 2.0; + tz = Math.cos(r) * 2.0; + ty = 0.7; + fy += 2.0; + fuse = 10; + } + FlareEntity flare = ModEntities.FLARE.get().create(vehicle.level(), EntitySpawnReason.TRIGGERED); + if (flare != null) { + flare.launch(x, fy, z, tx * 0.5, ty * 0.5, tz * 0.5, 6.0F, fuse, type == 4); + vehicle.level().addFreshEntity(flare); + } + } + } + + /** {@code destroyMissile}: missiles homing on this vehicle or its crew lose their target. */ + private void decoyMissiles() { + for (ProjectileEntity missile : vehicle.level().getEntitiesOfClass(ProjectileEntity.class, + vehicle.getBoundingBox().inflate(DECOY_RANGE), p -> p.target() != null)) { + if (missile.target() == vehicle || vehicle.hasPassenger(missile.target())) { + missile.decoy(); + } + } + } +} From 62c47da0ce8d78c7690093a4b9cf1fa78a767e19 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:41 +0800 Subject: [PATCH 0812/1461] Add weapon/Guidance.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/weapon/Guidance.java | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java new file mode 100644 index 00000000..b8142e92 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java @@ -0,0 +1,220 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Predicate; + +/** + * Lock-on for seeking missiles, ported from {@code MCH_WeaponGuidanceSystem}. It runs on the + * client of the crew member aiming; the locked entity travels to the server with the fire request. + * + *

A target is acquired inside a cone around the aim, then the lock count climbs while it stays + * in the cone. Stealth shrinks both the range and the cone and stretches the time to lock. + */ +public final class Guidance { + public final boolean canLockInAir; + public final boolean canLockOnGround; + public final boolean canLockInWater = false; + public final boolean ridableOnly; + public final double lockRange = 200.0; + public final int lockAngle = 5; + private final int lockCountMax; + private final @Nullable Predicate filter; + + private @Nullable Entity target; + private @Nullable Entity lastLocked; + private int lockCount; + private int prevLockCount; + private int continueLockCount; + private int soundCount; + + public Guidance(boolean air, boolean ground, boolean ridableOnly, int lockTime, @Nullable Predicate filter) { + this.canLockInAir = air; + this.canLockOnGround = ground; + this.ridableOnly = ridableOnly; + this.lockCountMax = Math.max(lockTime, 1); + this.filter = filter; + } + + public int lockCount() { + return lockCount; + } + + public int lockCountMax() { + float stealth = stealth(target); + return (int) (lockCountMax + lockCountMax * stealth); + } + + public @Nullable Entity target() { + return lockCount > 0 && target != null && !target.isRemoved() ? target : null; + } + + public @Nullable Entity lastLocked() { + return lastLocked; + } + + public boolean isLocked() { + return lockCount == lockCountMax() && lastLocked != null; + } + + /** Called every client tick: a lock decays unless {@link #lock} kept it alive this tick. */ + public void tick() { + if (lockCount != prevLockCount) { + prevLockCount = lockCount; + } else { + lockCount = 0; + prevLockCount = 0; + } + } + + /** + * Advances the lock for one tick of aiming from {@code user}'s view. + * + * @param eye where the view is seen from. The original used the player's position, which on a + * 1.7.10 client was eye height; gunner sights and launcher cameras sit higher still, + * and a 5 degree cone measured from the wrong point misses targets close by. + * @return true once the lock is complete; {@link #lastLocked()} is the target to send + */ + public boolean lock(Entity user, Vec3 eye) { + if (lockCount == 0) { + acquire(user, eye); + return false; + } + if (target == null || target.isRemoved() || !inLockableState(target)) { + clear(); + return false; + } + double range = lockRange * (1.0F - stealth(target)); + if (user.distanceToSqr(target) >= range * range) { + clear(); + return false; + } + soundCount = (soundCount + 1) % 15; + if (inCone(eye, user.getYRot(), user.getXRot(), target, lockAngle)) { + if (lockCount < lockCountMax()) { + lockCount++; + } + } else if (continueLockCount > 0) { + continueLockCount--; + if (continueLockCount <= 0 && lockCount > 0) { + lockCount--; + } + } else { + continueLockCount = 0; + lockCount--; + } + if (lockCount >= lockCountMax()) { + if (continueLockCount <= 0) { + continueLockCount = Math.min(lockCountMax() / 3, 20); + } + lastLocked = target; + prevLockCount = lockCount - 1; + return true; + } + return false; + } + + /** True on the tick the target should hear that it is being tracked. */ + public boolean shouldNotifyTarget() { + return lockCount > 0 && soundCount == 1; + } + + private void acquire(Entity user, Vec3 eye) { + Entity best = null; + double bestDistance = lockRange * lockRange * 2.0; + for (Entity entity : user.level().getEntities(user, user.getBoundingBox().inflate(lockRange), this::canLock)) { + double d = entity.distanceToSqr(user); + float stealth = 1.0F - stealth(entity); + double range = lockRange * stealth; + float angle = lockAngle * (stealth / 2.0F + 0.5F); + if (d < range * range && d < bestDistance && inCone(eye, user.getYRot(), user.getXRot(), entity, angle)) { + Vec3 to = new Vec3(entity.getX(), entity.getY() + entity.getBbHeight() / 2.0F, entity.getZ()); + HitResult hit = user.level().clip(new ClipContext(eye, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, user)); + if (hit.getType() == HitResult.Type.MISS) { + best = entity; + bestDistance = d; + } + } + } + target = best; + if (best != null) { + lockCount++; + } + } + + public void clear() { + target = null; + lockCount = 0; + continueLockCount = 0; + soundCount = 0; + } + + private boolean canLock(Entity entity) { + if (ridableOnly && entity instanceof Player && entity.getVehicle() == null) { + return false; + } + if (!(entity instanceof LivingEntity) && !(entity instanceof AircraftEntity)) { + return false; + } + if (filter != null && !filter.test(entity)) { + return false; + } + return inLockableState(entity); + } + + private boolean inLockableState(Entity entity) { + if (!canLockInWater && entity.isInWater()) { + return false; + } + boolean onGround = isOnGround(entity); + return onGround ? canLockOnGround : canLockInAir; + } + + /** On the ground, or less than twelve blocks above something solid. */ + public static boolean isOnGround(Entity entity) { + if (entity.onGround()) { + return true; + } + BlockPos.MutableBlockPos pos = BlockPos.containing(entity.getX() + 0.5, entity.getY() + 0.5, entity.getZ() + 0.5).mutable(); + for (int i = 0; i < 12; i++) { + if (!entity.level().getBlockState(pos).isAir() && !entity.level().getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + pos.move(0, -1, 0); + } + return false; + } + + public static float stealth(@Nullable Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + return vehicle.stealth(); + } + return entity != null && entity.getVehicle() instanceof AircraftEntity vehicle ? vehicle.stealth() : 0.0F; + } + + /** {@code inLockRange}: whether {@code target} is within {@code angle} degrees of the aim. */ + public static boolean inCone(Entity from, float yaw, float pitch, Entity target, float angle) { + return inCone(from.position(), yaw, pitch, target, angle); + } + + public static boolean inCone(Vec3 from, float yaw, float pitch, Entity target, float angle) { + double dx = target.getX() - from.x; + double dy = target.getY() + target.getBbHeight() / 2.0F - from.y; + double dz = target.getZ() - from.z; + float targetYaw = (float) Math.toDegrees(Math.atan2(dz, dx)); + float diffYaw = Mth.positiveModulo(targetYaw - yaw - 90.0F, 360.0F); + float targetPitch = -(float) Math.toDegrees(Math.atan2(dy, Math.sqrt(dx * dx + dz * dz))); + float diffPitch = targetPitch - pitch; + return (diffYaw < angle || diffYaw > 360.0F - angle) && Math.abs(diffPitch) < angle; + } +} From 05e811bf369d31c830bcf2d71da6ce4b7694920a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:42 +0800 Subject: [PATCH 0813/1461] Add weapon/LegacyItems.java to the 1.21.11 shared sources --- .../mcheli/weapon/LegacyItems.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java new file mode 100644 index 00000000..f763f73f --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import org.jspecify.annotations.Nullable; + +/** + * Maps the 1.7.10 item names and damage values in the content files to today's items. Names + * that still exist resolve directly; the few that were renamed or flattened are listed here. + */ +public final class LegacyItems { + private static final String[] DYES = { + "ink_sac", "red_dye", "green_dye", "cocoa_beans", "lapis_lazuli", "purple_dye", "cyan_dye", "light_gray_dye", + "gray_dye", "pink_dye", "lime_dye", "yellow_dye", "light_blue_dye", "magenta_dye", "orange_dye", "bone_meal"}; + + private LegacyItems() { + } + + public static @Nullable Item item(String name, int damage) { + if (name.isEmpty()) { + return null; + } + String id = switch (name) { + case "dye" -> DYES[Math.floorMod(damage, DYES.length)]; + case "boat" -> "oak_boat"; + case "reeds" -> "sugar_cane"; + case "wool" -> "white_wool"; + case "log" -> "oak_log"; + case "planks" -> "oak_planks"; + default -> name.startsWith("minecraft:") ? name.substring("minecraft:".length()) : name; + }; + Identifier key = Identifier.tryParse(id.contains(":") ? id : "minecraft:" + id); + if (key == null) { + return null; + } + Item item = BuiltInRegistries.ITEM.getValue(key); + return item == Items.AIR ? null : item; + } +} From 16154bb06988d843ae42891eca26a89bbdf261ec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:42 +0800 Subject: [PATCH 0814/1461] Add weapon/ModDamageTypes.java to the 1.21.11 shared sources --- .../mcheli/weapon/ModDamageTypes.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java new file mode 100644 index 00000000..b4007926 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * Damage types defined in {@code data/mcheli/damage_type}. Bullets and missiles use their own type + * so death messages name the gun instead of calling it a thrown snowball, which is what the + * original's {@code causeThrownDamage} amounted to. + */ +public final class ModDamageTypes { + public static final ResourceKey BULLET = + ResourceKey.create(Registries.DAMAGE_TYPE, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "bullet")); + + private ModDamageTypes() { + } + + public static DamageSource bullet(Level level, Entity projectile, @Nullable Entity owner) { + return new DamageSource(level.registryAccess().lookupOrThrow(Registries.DAMAGE_TYPE).getOrThrow(BULLET), projectile, owner); + } +} From edb9969c0870f3dfbbe6bdd51fbde9c41e123328 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:43 +0800 Subject: [PATCH 0815/1461] Add weapon/Resupply.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/weapon/Resupply.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java new file mode 100644 index 00000000..533f5fba --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** + * Resupplying a weapon from a crew member's own items, the {@code supplyAmmo} path of the + * original's vehicle screen. One supply costs the weapon's {@code Item} lines and adds + * {@code SuppliedNum} rounds; it only works while the vehicle stands on something. + */ +public final class Resupply { + private Resupply() { + } + + /** {@code canPlayerSupplyAmmo}: whether {@link #supply} would do anything right now. */ + public static boolean canSupply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.set(weapon) : null; + if (set == null || vehicle.isDestroyed() || player.getVehicle() != vehicle || !vehicle.canSupply()) { + return false; + } + if (set.ammo() + set.restAmmo() >= set.maxAmmo()) { + return false; + } + return player.getAbilities().instabuild || canPay(player.getInventory(), set.definition()); + } + + /** Pays for and adds one supply of {@code weapon}. Server only. */ + public static boolean supply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + if (!canSupply(player, vehicle, weapon)) { + return false; + } + WeaponSet set = vehicle.weapons().set(weapon); + if (!player.getAbilities().instabuild) { + pay(player.getInventory(), set.definition()); + } + set.supply(); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + return true; + } + + private static boolean canPay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null || inventory.countItem(item) < cost.count()) { + return false; + } + } + return true; + } + + private static void pay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + int left = cost.count(); + for (int i = 0; i < inventory.getContainerSize() && left > 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.is(item)) { + continue; + } + if (item == Items.WATER_BUCKET || item == Items.LAVA_BUCKET) { + // Buckets give their bucket back, one at a time. + inventory.setItem(i, new ItemStack(Items.BUCKET)); + left--; + } else { + int take = Math.min(left, stack.getCount()); + stack.shrink(take); + left -= take; + } + } + } + inventory.setChanged(); + } +} From 79ae9d6c586ec6e53a707098007579c4aa56e4bb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:43 +0800 Subject: [PATCH 0816/1461] Add weapon/ServerFiring.java to the 1.21.11 shared sources --- .../mcheli/weapon/ServerFiring.java | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java new file mode 100644 index 00000000..3f867744 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java @@ -0,0 +1,418 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * What each weapon type does when the server fires it: the {@code shot} methods of the + * {@code MCH_Weapon*} classes. Every method returns whether a round was used. + */ +public final class ServerFiring implements WeaponSet.Trigger { + public static final ServerFiring INSTANCE = new ServerFiring(); + private static final RandomSource RANDOM = RandomSource.create(); + + private ServerFiring() { + } + + @Override + public boolean fire(WeaponSet set, int mount, Shot shot) { + if (!(shot.shooter.level() instanceof ServerLevel level)) { + return false; + } + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_1, MACHINE_GUN_2 -> gun(level, set, shot); + case ROCKET -> rocket(level, set, shot); + case BOMB -> bomb(level, set, shot); + case AA_MISSILE -> aaMissile(level, set, mount, shot); + case AS_MISSILE -> asMissile(level, set, shot); + case AT_MISSILE -> atMissile(level, set, mount, shot); + case TV_MISSILE -> tvMissile(level, set, mount, shot); + case TORPEDO -> torpedo(level, set, shot); + case CAS -> cas(level, set, shot); + case MARKER_ROCKET -> markerRocket(level, set, shot); + case DISPENSER -> dispenser(level, set, shot); + case TARGETING_POD -> targetingPod(level, set, shot); + case DUMMY, SMOKE -> false; + case null -> false; + }; + } + + private static @Nullable T spawn(ServerLevel level, EntityType type, WeaponSet set, Shot shot, + Vec3 dir, float yaw, float pitch, double acceleration) { + T e = type.create(level, EntitySpawnReason.TRIGGERED); + if (e == null) { + return null; + } + e.launch(shot.x, shot.y, shot.z, dir.x, dir.y, dir.z, yaw, pitch, acceleration); + e.setWeapon(set.name()); + e.setParameters(set.definition(), shot.shooter, shot.user); + return e; + } + + private static Vec3 forward(float yaw, float pitch) { + return Vec3.directionFromRotation(pitch, yaw); + } + + private static void sound(WeaponSet set, Entity at) { + sound(set, at, soundName(set)); + } + + private static String soundName(WeaponSet set) { + return set.ownSound != null ? set.ownSound.sound() : set.definition().sound; + } + + private static void sound(WeaponSet set, Entity at, String name) { + if (set.soundAllowed) { + WeaponDefinition w = set.definition(); + float rnd = w.soundPitchRandom; + float pitch = set.ownSound != null && set.ownSound.pitch() != null ? set.ownSound.pitch() : w.soundPitch; + ModSounds.playAt(at, name, w.soundVolume, pitch * (1.0F - rnd) + RANDOM.nextFloat() * rnd); + } + } + + // Guns, rockets, bombs + + private static boolean gun(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + BulletEntity e = spawn(level, ModEntities.BULLET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, w.acceleration); + if (e == null) { + return false; + } + if (w.type == WeaponType.MACHINE_GUN_2 && w.modeNum >= 2) { + // Mode 0 fires ball ammunition, which only bangs; mode 1 is high explosive. + e.explosionPower = shot.option1 == 0 ? -w.explosion : w.explosion; + } + Vec3 m = e.getDeltaMovement(); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean rocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + RocketEntity e = spawn(level, ModEntities.ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + if (shot.option1 == 0 && set.definition().modeNum > 1) { + e.piercing = 0; + } + level.addFreshEntity(e); + return true; + } + + private static boolean bomb(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + if (w.destruct) { + // A drone's self destruct charge. + if (shot.shooter instanceof HelicopterEntity drone && drone.definition() != null && drone.definition().uav) { + Explosions.explode(level, drone.getX(), drone.getY(), drone.getZ(), + new Explosions.Params(null, shot.user, w.explosion, w.explosionBlock, w.flaming, true, 0, false, null)); + sound(set, shot.shooter); + drone.ejectPassengers(); + drone.discard(); + } + return true; + } + sound(set, shot.shooter); + Vec3 motion = shot.shooter.getDeltaMovement(); + BombEntity e = spawn(level, ModEntities.BOMB.get(), set, shot, motion, shot.shooter.getYRot(), 0.0F, w.acceleration); + if (e == null) { + return false; + } + e.setDeltaMovement(motion); + level.addFreshEntity(e); + return true; + } + + // Missiles + + private static float fixedYaw(WeaponSet set, int mount) { + return set.mounts().get(mount).yaw(); + } + + private static float fixedPitch(WeaponSet set, int mount) { + return set.mounts().get(mount).pitch(); + } + + private static boolean aaMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + sound(set, shot.shooter); + float yaw = shot.shooter.getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + AAMissileEntity e = spawn(level, ModEntities.AA_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + level.addFreshEntity(e); + return true; + } + + /** Ray from the shooter along the aimer's view, as the original's AS missile and CAS sights. */ + private static @Nullable BlockHitResult sight(ServerLevel level, Shot shot, double eyeHeight, double range) { + Entity aimer = shot.aimer(); + Vec3 from = shot.shooter.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(forward(aimer.getYRot(), aimer.getXRot()).scale(range)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + return hit.getType() == HitResult.Type.BLOCK ? hit : null; + } + + private static boolean nextToWater(ServerLevel level, BlockPos pos) { + if (level.getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + for (Direction d : Direction.values()) { + if (level.getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + private static boolean asMissile(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 1.62, 250.0); + if (hit == null || nextToWater(level, hit.getBlockPos())) { + return false; + } + Entity aimer = shot.aimer(); + ASMissileEntity e = spawn(level, ModEntities.AS_MISSILE.get(), set, shot, forward(aimer.getYRot(), aimer.getXRot()), + aimer.getYRot(), aimer.getXRot(), set.definition().acceleration); + if (e == null) { + return false; + } + e.targetX = hit.getLocation().x; + e.targetY = hit.getLocation().y; + e.targetZ = hit.getLocation().z; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean atMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + ATMissileEntity e = spawn(level, ModEntities.AT_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + e.guidanceType = shot.option2; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean tvMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.aimer().getXRot() + fixedPitch(set, mount); + boolean tvGuided = shot.option1 == 0; + float acceleration = set.definition().acceleration * (tvGuided ? 1.0F : 1.5F); + TvMissileEntity e = spawn(level, ModEntities.TV_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, acceleration); + if (e == null) { + return false; + } + level.addFreshEntity(e); + if (shot.shooter instanceof AircraftEntity vehicle) { + vehicle.setTvMissile(e); + } + sound(set, shot.shooter); + return true; + } + + private static boolean torpedo(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Vec3 vehicleMotion = shot.shooter.getDeltaMovement(); + float yaw = shot.yaw; + float pitch = shot.pitch; + Vec3 target = null; + if (w.guidedTorpedo) { + Entity aimer = shot.aimer(); + yaw = aimer.getYRot(); + pitch = aimer.getXRot(); + Vec3 from = aimer.position(); + Vec3 to = from.add(forward(yaw, pitch).scale(150.0)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + if (hit.getType() != HitResult.Type.BLOCK || !nextToWater(level, hit.getBlockPos())) { + return false; + } + target = hit.getLocation(); + } + Vec3 m = forward(yaw, pitch).scale(w.acceleration).add(vehicleMotion); + double speed = m.length(); + TorpedoEntity e = spawn(level, ModEntities.TORPEDO.get(), set, shot, m, yaw, 0.0F, speed); + if (e == null) { + return false; + } + e.setDeltaMovement(m); + e.accelerationInWater = w.accelerationInWater; + if (target != null) { + e.targetX = target.x; + e.targetY = target.y; + e.targetZ = target.z; + } + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + // Support weapons + + private static boolean markerRocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + MarkerRocketEntity e = spawn(level, ModEntities.MARKER_ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + e.setStatus(1); + level.addFreshEntity(e); + return true; + } + + private static boolean dispenser(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + DispensedItemEntity e = spawn(level, ModEntities.DISPENSED_ITEM.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + Vec3 m = shot.shooter.getDeltaMovement().add(e.getDeltaMovement().scale(0.5)); + e.setDeltaMovement(m); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + return true; + } + + /** + * Spots everything of the weapon's {@code Target} kinds inside its cone, which then glows for + * {@code MarkTime}. Marking a block for the team needs the team manager of a later phase. + */ + private static boolean targetingPod(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Entity aimer = shot.aimer(); + if ((w.target & WeaponDefinition.TARGET_BLOCK) != 0) { + // {@code MCH_Multiplay.markPoint}: the block under the crew's view, 300 blocks out, goes to the team. + boolean marked = aimer instanceof ServerPlayer player && Spotting.markPoint(player, new Vec3(shot.x, shot.y, shot.z), 300.0); + sound(set, aimer, marked ? soundName(set) : "ng"); + return true; + } + List spotted = new ArrayList<>(); + double range = Math.max(w.length, 1.0F); + for (LivingEntity entity : level.getEntitiesOfClass(LivingEntity.class, aimer.getBoundingBox().inflate(range))) { + if (entity == aimer || shot.shooter.hasPassenger(entity) || aimer.distanceToSqr(entity) > range * range + || !matchesTarget(entity, w.target) || !Guidance.inCone(aimer, aimer.getYRot(), aimer.getXRot(), entity, w.angle)) { + continue; + } + spotted.add(entity.getId()); + } + if (aimer instanceof ServerPlayer player) { + Spotting.tellTeam(player, spotted, Math.max(w.markTime, 20)); + } + sound(set, shot.shooter, !spotted.isEmpty() ? soundName(set) : "ng"); + return true; + } + + private static boolean matchesTarget(LivingEntity entity, int bits) { + Entity vehicle = entity.getVehicle(); + if (vehicle instanceof PlaneEntity) { + return (bits & WeaponDefinition.TARGET_PLANES) != 0; + } + if (vehicle instanceof HelicopterEntity) { + return (bits & WeaponDefinition.TARGET_HELICOPTERS) != 0; + } + if (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity) { + return (bits & WeaponDefinition.TARGET_GROUND_VEHICLES) != 0; + } + if (entity instanceof Player) { + return (bits & WeaponDefinition.TARGET_PLAYERS) != 0; + } + if (entity instanceof Enemy) { + return (bits & WeaponDefinition.TARGET_MONSTERS) != 0; + } + return (bits & WeaponDefinition.TARGET_OTHERS) != 0; + } + + // Close air support + + private static boolean cas(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 2.0, 150.0); + if (hit == null) { + return false; + } + Vec3 at = hit.getLocation(); + set.casX = at.x; + set.casY = at.y; + set.casZ = at.z; + set.casDirection = Math.floorMod(CasStrike.approach(shot.aimer().getYRot()), 4); + set.casUser = shot.user; + set.casShooter = shot.shooter; + set.casStartTick = CasStrike.FIRST_PASS; + set.casCount = 0; + if (shot.user instanceof ServerPlayer player) { + ModSounds.playTo(player, set.definition().sound, 1.0F, 1.0F); + } + return true; + } + + /** {@code MCH_WeaponCAS.update}: three passes, 45 ticks apart, the later ones offset to the sides. */ + static void tick(AircraftEntity vehicle, WeaponSet set) { + if (set.casCount >= 3 || set.countWait == 0 || set.ticksSinceShot(0) != set.casStartTick + || !(vehicle.level() instanceof ServerLevel level)) { + return; + } + Vec3 offset = CasStrike.passOffset(set.casCount, set.casDirection); + CasStrike.spawnA10(level, set.name(), set.definition(), set.casDirection, set.casX + offset.x, set.casY + 20.0, + set.casZ + offset.z, set.casShooter, set.casUser); + set.casStartTick = set.ticksSinceShot(0) + 45; + set.casCount++; + } +} From a3abd3d40799d6a3546e56ae0064a40cca5911e9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:44 +0800 Subject: [PATCH 0817/1461] Add weapon/Shot.java to the 1.21.11 shared sources --- .../com/jnhprojects/mcheli/weapon/Shot.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java new file mode 100644 index 00000000..6a9f53cf --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java @@ -0,0 +1,40 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +/** + * One trigger pull, the port of {@code MCH_WeaponParam}. The weapon set fills in the aim and the + * muzzle position; the client fills in the two option values (a locked target, a fire mode) and + * the server receives them with the fire request. + */ +public final class Shot { + /** The vehicle firing, or the player for hand held weapons. */ + public final Entity shooter; + /** Who pulled the trigger. Damage and kills are credited to this entity. */ + public final @Nullable Entity user; + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + public float roll; + public int option1; + public int option2; + public boolean infiniteAmmo; + /** Set by the weapon set when the magazine ran dry with this shot. */ + public boolean reload; + + public Shot(Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + this.x = shooter.getX(); + this.y = shooter.getY(); + this.z = shooter.getZ(); + } + + /** The entity whose view steers guided weapons: the user if there is one, else the shooter. */ + public Entity aimer() { + return user != null ? user : shooter; + } +} From 140ae2d5c008e586e4e6003682eab93783aa1c2a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:44 +0800 Subject: [PATCH 0818/1461] Add weapon/Spotting.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/weapon/Spotting.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java new file mode 100644 index 00000000..b9331154 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.network.MarkerPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +/** + * Spotting with the rangefinder, {@code MCH_Multiplay.spotEntity} and {@code markPoint}. + * + *

Spotted targets and marked points go to the spotter and their team only, as screen markers + * the HUD draws ({@code MCH_GuiTargetMarker}); nobody else sees them. + */ +public final class Spotting { + // The original's target filter bits. + public static final int OTHERS = 1; + public static final int MONSTERS = 2; + public static final int PLAYERS = 4; + public static final int GROUND_VEHICLES = 8; + public static final int HELICOPTERS = 16; + public static final int PLANES = 32; + + /** {@code RangeFinderSpotDist}. */ + public static final double RANGE = 400.0; + /** {@code RangeFinderSpotTime} in ticks, for players and vehicles. */ + private static final int PLAYER_TIME = 15 * 20; + /** Mobs stay marked for a minute. */ + private static final int MOB_TIME = 60 * 20; + /** Half the width of the cone a spot covers, in degrees: the original's angle times two. */ + private static final double CONE = 40.0; + + private Spotting() { + } + + static boolean matches(int filter, Entity entity) { + int bit; + if (entity instanceof PlaneEntity) { + bit = PLANES; + } else if (entity instanceof HelicopterEntity) { + bit = HELICOPTERS; + } else if (entity instanceof AircraftEntity) { + bit = GROUND_VEHICLES; + } else if (entity instanceof Player) { + bit = PLAYERS; + } else if (entity instanceof Enemy) { + bit = MONSTERS; + } else if (entity instanceof LivingEntity) { + bit = OTHERS; + } else { + return false; + } + return (filter & bit) != 0; + } + + /** + * Marks what {@code spotter} sees within {@link #RANGE} and the cone around their view: never + * a teammate, and only targets in plain sight. + * + * @return whether anything was spotted + */ + public static boolean spot(ServerPlayer spotter, int filter) { + ServerLevel level = spotter.level(); + Vec3 eye = spotter.getEyePosition(); + Vec3 look = spotter.getLookAngle(); + int ticks = (filter & ~(OTHERS | MONSTERS)) == 0 ? MOB_TIME : PLAYER_TIME; + List spotted = new ArrayList<>(); + for (Entity entity : level.getEntities(spotter, spotter.getBoundingBox().inflate(RANGE), e -> matches(filter, e))) { + if (isFriendly(spotter, entity)) { + continue; + } + Vec3 to = entity.getBoundingBox().getCenter().subtract(eye); + double dist = to.length(); + if (dist < 1.0 || dist > RANGE || Math.toDegrees(Math.acos(to.scale(1.0 / dist).dot(look))) > CONE) { + continue; + } + if (level.clip(new ClipContext(eye, entity.getBoundingBox().getCenter(), ClipContext.Block.COLLIDER, + ClipContext.Fluid.NONE, spotter)).getType() == HitResult.Type.BLOCK) { + continue; + } + spotted.add(entity.getId()); + } + tellTeam(spotter, spotted, ticks); + return !spotted.isEmpty(); + } + + /** Sends the spotted entities to {@code spotter} and their team, {@code MCH_PacketNotifySpotedEntity}. */ + public static void tellTeam(ServerPlayer spotter, List entities, int ticks) { + if (entities.isEmpty()) { + return; + } + MarkerPacket packet = MarkerPacket.spotted(entities.stream().mapToInt(Integer::intValue).toArray(), ticks); + for (ServerPlayer player : TeamManager.teammates(spotter)) { + ModNetwork.sendToPlayer(player, packet); + } + } + + /** Teammates, and a vehicle crewed by one, are never spotted. */ + private static boolean isFriendly(ServerPlayer spotter, Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + for (Entity crew : vehicle.getPassengers()) { + if (crew.isAlliedTo(spotter)) { + return true; + } + } + return false; + } + return entity.isAlliedTo(spotter); + } + + /** + * Tells the spotter and their team where they are looking, as block coordinates. + * + * @return whether the view ended on a block within range + */ + public static boolean markPoint(ServerPlayer spotter) { + return markPoint(spotter, spotter.getEyePosition(), RANGE); + } + + /** + * As above, looking from {@code from} along the spotter's view: a targeting pod marks from + * the vehicle, out to {@code range}. + */ + public static boolean markPoint(ServerPlayer spotter, Vec3 from, double range) { + ServerLevel level = spotter.level(); + BlockHitResult hit = level.clip(new ClipContext(from, from.add(spotter.getLookAngle().scale(range)), + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, spotter)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + BlockPos pos = hit.getBlockPos(); + MarkerPacket marker = MarkerPacket.point(pos); + Component message = Component.translatable("message.mcheli.mark_point", spotter.getDisplayName(), pos.getX(), pos.getY(), pos.getZ()); + for (ServerPlayer player : level.getServer().getPlayerList().getPlayers()) { + if (player == spotter || player.isAlliedTo(spotter)) { + player.sendSystemMessage(message); + ModNetwork.sendToPlayer(player, marker); + } + } + return true; + } +} From fc80e8645953b1f7e8baa97edf25cfdc244c9c82 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:44 +0800 Subject: [PATCH 0819/1461] Make the cartridge particle from a stack on 1.21.11 --- .../mcheli/weapon/VehicleWeapons.java | 598 ++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java new file mode 100644 index 00000000..af94a9dc --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java @@ -0,0 +1,598 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.weapon.MuzzleFlash; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.WeaponStatePacket; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The armament of one vehicle: its weapon sets, which one each seat has selected, the flares, and + * the effects every client derives from the synced firing state. Ported from the weapon handling + * in {@code MCH_EntityAircraft} ({@code createWeapon}, {@code switchWeapon}, + * {@code useCurrentWeapon}, {@code updateWeapons} and {@code updateWeaponsRotation}). + */ +public final class VehicleWeapons { + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final boolean clientSide; + private final List sets = new ArrayList<>(); + private final int[] selected; + private final @Nullable Guidance[] guidance; + private final RandomSource random = RandomSource.create(); + + /** One bit per mount, set while that mount shows muzzle effects. Synced by the vehicle. */ + private int useStat; + private int prevUseStat; + + private final float[] bayAngle; + private final float[] prevBayAngle; + + private final int[] sentAmmo; + private final int[] sentRest; + + public final FlareDispenser flares; + + // Recoil that kicks the whole hull, from Recoil in the weapon file. + private int recoilCount; + private float recoilValue; + private float recoilYaw; + + public VehicleWeapons(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + this.clientSide = vehicle.level().isClientSide(); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + if (weapon != null && !group.mounts().isEmpty()) { + WeaponSet set = new WeaponSet(weapon, group.mounts(), clientSide); + set.ownSound = def.weaponSounds.get(group.weaponType()); + sets.add(set); + } + } + selected = new int[def.seatCount()]; + Arrays.fill(selected, -1); + guidance = new Guidance[sets.size()]; + for (int i = 0; i < sets.size(); i++) { + WeaponDefinition w = sets.get(i).definition(); + if (w.type == WeaponType.AA_MISSILE || w.type == WeaponType.AT_MISSILE) { + boolean aa = w.type == WeaponType.AA_MISSILE; + guidance[i] = new Guidance(aa, !aa, w.ridableOnly, w.lockTime, this::canLock); + } + } + bayAngle = new float[def.weaponBays.size()]; + prevBayAngle = new float[def.weaponBays.size()]; + sentAmmo = new int[sets.size()]; + sentRest = new int[sets.size()]; + Arrays.fill(sentAmmo, -1); + flares = new FlareDispenser(vehicle, def); + } + + public List sets() { + return sets; + } + + public @Nullable WeaponSet set(int id) { + return id >= 0 && id < sets.size() ? sets.get(id) : null; + } + + public int indexOf(String weapon) { + for (int i = 0; i < sets.size(); i++) { + if (sets.get(i).name().equalsIgnoreCase(weapon)) { + return i; + } + } + return -1; + } + + public @Nullable WeaponSet byName(String weapon) { + return set(indexOf(weapon)); + } + + public @Nullable Guidance guidance(int id) { + return id >= 0 && id < guidance.length ? guidance[id] : null; + } + + /** A vehicle does not lock its own crew or cargo. */ + private boolean canLock(Entity entity) { + return entity != vehicle && !vehicle.hasPassenger(entity); + } + + // Selection + + public int selected(int seat) { + return seat >= 0 && seat < selected.length ? selected[seat] : -1; + } + + public @Nullable WeaponSet current(int seat) { + return set(selected(seat)); + } + + /** The seat that operates a weapon: its own seat, or the pilot while that seat has no player. */ + public int operatorSeat(int id) { + WeaponSet set = set(id); + if (set == null) { + return -1; + } + WeaponMount mount = set.firstMount(); + if (mount.usableByPilot() && !(vehicle.occupant(mount.seatIndex()) instanceof Player)) { + return 0; + } + return mount.seatIndex(); + } + + /** {@code getNextWeaponID}: the next weapon {@code seat} may use, stepping forwards or back. */ + public int next(int seat, int step) { + if (sets.isEmpty() || seat < 0) { + return -1; + } + int id = selected(seat); + for (int i = 0; i < sets.size(); i++) { + id = step >= 0 ? (id + 1) % sets.size() : id > 0 ? id - 1 : sets.size() - 1; + WeaponMount mount = sets.get(id).firstMount(); + int owner = mount.seatIndex(); + if (owner == seat || seat == 0 && mount.usableByPilot() && !(vehicle.occupant(owner) instanceof Player)) { + return id; + } + } + return -1; + } + + /** {@code initCurrentWeapon}: a player took a seat. Server only. */ + public void onSeated(Entity entity, int seat) { + if (seat < 0 || seat >= selected.length) { + return; + } + selected[seat] = -1; + if (entity instanceof Player) { + switchTo(entity, seat, next(seat, 1)); + } else { + vehicle.publishWeaponSelection(); + } + } + + public void onLeft(int seat) { + if (seat >= 0 && seat < selected.length) { + selected[seat] = -1; + vehicle.publishWeaponSelection(); + } + } + + /** {@code switchWeapon}. Server only; the selection reaches clients through synced data. */ + public void switchTo(Entity user, int seat, int id) { + if (seat < 0 || seat >= selected.length || sets.isEmpty()) { + return; + } + WeaponSet before = current(seat); + if (before != null) { + before.reload(); + } + selected[seat] = Math.min(id, sets.size() - 1); + WeaponSet now = current(seat); + if (now != null) { + now.onSwitchWeapon(vehicle.hasInfiniteAmmo(user)); + } + vehicle.publishWeaponSelection(); + } + + public String encodeSelection() { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < selected.length; i++) { + if (i > 0) { + b.append(','); + } + b.append(selected[i]); + } + return b.toString(); + } + + /** Applies the server's selection on a client, running the switch delay where it changed. */ + public void decodeSelection(String encoded, @Nullable Entity localPlayer) { + String[] parts = encoded.isEmpty() ? new String[0] : encoded.split(","); + for (int i = 0; i < selected.length; i++) { + int id; + try { + id = i < parts.length ? Integer.parseInt(parts[i]) : -1; + } catch (NumberFormatException e) { + id = -1; + } + if (selected[i] != id) { + selected[i] = id; + WeaponSet set = set(id); + Entity occupant = vehicle.occupant(i); + if (set != null && occupant != null && occupant == localPlayer) { + set.onSwitchWeapon(vehicle.hasInfiniteAmmo(occupant)); + Guidance g = guidance(id); + if (g != null) { + g.clear(); + } + } + } + } + } + + // Firing + + /** + * {@code useCurrentWeapon}: fires the selected weapon of {@code seat} if it is ready. The + * trigger decides what the shot does on this side. + */ + public boolean use(Entity user, int seat, Shot shot, WeaponSet.Trigger trigger) { + int id = selected(seat); + WeaponSet set = set(id); + if (set == null || !set.canUse() || vehicle.isDestroyed()) { + return false; + } + shot.infiniteAmmo = vehicle.hasInfiniteAmmo(user); + int mount = set.currentMount(); + // As the original did, face the vehicle where the user looks for the length of the shot. + boolean followUser = vehicle.aimsWithUser(set); + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + if (followUser) { + vehicle.setYRot(user.getYRot()); + vehicle.setXRot(user.getXRot()); + } + boolean fired; + try { + fired = set.use(shot, (s, m, sh) -> { + Vec3 muzzle = muzzleOffset(s, m, 1.0F); + sh.x += muzzle.x; + sh.y += muzzle.y; + sh.z += muzzle.z; + return trigger.fire(s, m, sh); + }); + } finally { + if (followUser) { + vehicle.setYRot(hullYaw); + vehicle.setXRot(hullPitch); + } + } + if (!fired) { + return false; + } + String group = set.definition().group; + if (!group.isEmpty()) { + for (WeaponSet other : sets) { + if (other != set && group.equals(other.definition().group)) { + other.waitForGroup(shot.reload); + } + } + } + if (!clientSide) { + int bit = bitOf(id, mount); + if (bit < 32) { + useStat |= 1 << bit; + } + } + return true; + } + + /** World offset of a mount from the vehicle origin; turret mounts turn with the turret. */ + public Vec3 muzzleOffset(WeaponSet set, int mount, float partialTicks) { + WeaponMount m = set.mounts().get(mount); + Vector3 local = m.turret() ? vehicle.onTurret(def, m.position(), partialTicks) : m.position(); + Vector3f v = vehicle.toWorldOffset(local); + return new Vec3(v.x, v.y, v.z); + } + + private int bitOf(int id, int mount) { + int shift = 0; + for (int i = 0; i < id; i++) { + shift += sets.get(i).mounts().size(); + } + return shift + mount; + } + + /** Whether a mount is cooling down after a shot; missile parts are hidden meanwhile. */ + public boolean isMountUsed(int id, int mount) { + int bit = bitOf(id, mount); + return bit < 32 && (useStat & 1 << bit) != 0; + } + + public int useStat() { + return useStat; + } + + public void setUseStat(int stat) { + useStat = stat; + } + + public void switchMode(int seat) { + WeaponSet set = current(seat); + if (set != null) { + set.switchMode(); + } + } + + // Ticking + + /** {@code updateWeapons} and {@code updateWeaponsRotation}, once per tick on both sides. */ + public void tick() { + if (sets.isEmpty()) { + flares.tick(); + return; + } + int previous = prevUseStat; + if (!clientSide) { + useStat |= usedStat(); + vehicle.publishWeaponUse(useStat); + } else { + useStat = vehicle.syncedWeaponUse(); + } + prevUseStat = useStat; + + float yaw = Mth.wrapDegrees(vehicle.getYRot()); + float pitch = Mth.wrapDegrees(vehicle.getXRot()); + int bit = 0; + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + boolean longDelay = set.isLongDelay(); + boolean anyUsed = false; + for (int mount = 0; mount < set.mounts().size(); mount++) { + boolean wasUsed = bit < 32 && (previous & 1 << bit) != 0; + boolean used = bit < 32 && (useStat & 1 << bit) != 0; + if (longDelay && wasUsed && used) { + used = false; + } + anyUsed |= used; + if (!wasUsed && used && set.definition().recoil > 0.0F) { + recoilCount = 30; + recoilValue = set.definition().recoil; + recoilYaw = set.rotationYaw; + } + if (clientSide && used) { + muzzleEffects(set, mount, yaw); + } + set.tickRecoil(mount, used); + bit++; + } + set.tick(anyUsed); + if (!clientSide) { + ServerFiring.tick(vehicle, set); + } + updateRotation(set); + } + if (!clientSide) { + useStat = 0; + syncAmmo(); + } else { + for (Guidance g : guidance) { + if (g != null) { + g.tick(); + } + } + } + if (recoilCount > 0) { + recoilCount--; + } + tickBays(); + if (clientSide) { + smoke(); + } + flares.tick(); + } + + private int usedStat() { + int stat = 0; + int bit = 0; + for (WeaponSet set : sets) { + for (int mount = 0; mount < set.mounts().size() && bit < 32; mount++) { + stat |= set.isUsed(mount) ? 1 << bit : 0; + bit++; + } + } + return stat; + } + + /** Turret aim of one set, following whoever operates it, within the mount's traverse limits. */ + private void updateRotation(WeaponSet set) { + if (vehicle.isDestroyed()) { + return; + } + WeaponMount mount = set.firstMount(); + int seat = mount.seatIndex(); + Entity operator = vehicle.occupant(seat); + if (mount.usableByPilot() && !(operator instanceof Player)) { + operator = vehicle.occupant(0); + } + float hullYaw = Mth.wrapDegrees(vehicle.getYRot()); + float hullPitch = Mth.wrapDegrees(vehicle.getXRot()); + float turretYaw = vehicle.turretYaw(); + if (operator instanceof Player) { + if ((int) mount.minYaw() != 0 || (int) mount.maxYaw() != 0) { + float ty = mount.turret() ? turretYaw : 0.0F; + float ey = Mth.wrapDegrees(operator.getYRot() - hullYaw - mount.defaultYaw() - ty); + if (Math.abs((int) mount.minYaw()) < 360 && Math.abs((int) mount.maxYaw()) < 360) { + float targetYaw = Mth.clamp(ey, mount.minYaw(), mount.maxYaw()); + float wy = set.rotationYaw - mount.defaultYaw() - ty; + if (targetYaw < wy) { + wy = wy - targetYaw > 15.0F ? wy - 15.0F : targetYaw; + } else if (targetYaw > wy) { + wy = targetYaw - wy > 15.0F ? wy + 15.0F : targetYaw; + } + set.rotationYaw = wy + mount.defaultYaw() + ty; + } else { + set.rotationYaw = ey + ty; + } + } + float ep = Mth.wrapDegrees(operator.getXRot() - hullPitch); + set.rotationPitch = Mth.clamp(ep, mount.minPitch(), mount.maxPitch()); + set.rotationTurretYaw = 0.0F; + } else { + set.rotationTurretYaw = turretYaw; + } + } + + /** {@code updateRecoil}: the hull kick of a big gun, applied on the simulating side. */ + public float @Nullable [] recoilKick() { + if (recoilCount < 12) { + return null; + } + float rel = (float) Math.toRadians(recoilYaw - vehicle.roll()); + float kick = Mth.cos((float) Math.toRadians(recoilCount * 6)) * recoilValue; + return new float[]{kick * Mth.cos(rel), kick * Mth.sin(rel)}; + } + + /** {@code updateWeaponBay}: bays swing open while a seated crew member has their weapon selected. */ + private void tickBays() { + for (int i = 0; i < bayAngle.length; i++) { + WeaponBayPart bay = def.weaponBays.get(i); + boolean open = false; + for (String weapon : bay.weapons()) { + int id = indexOf(weapon); + for (int seat = 0; seat < selected.length && id >= 0; seat++) { + if (selected[seat] == id && vehicle.occupant(seat) != null) { + open = true; + } + } + } + prevBayAngle[i] = bayAngle[i]; + bayAngle[i] = Mth.clamp(bayAngle[i] + (open ? 3.0F : -3.0F), 0.0F, 90.0F); + } + } + + public float bayAngle(int bay, float partialTicks) { + return bay < bayAngle.length ? Mth.lerp(partialTicks, prevBayAngle[bay], bayAngle[bay]) : 0.0F; + } + + // Effects, client only + + /** {@code spawnParticleMuzzleFlash} plus the spent case of guns with {@code SetCartridge}. */ + private void muzzleEffects(WeaponSet set, int mount, float hullYaw) { + Level level = vehicle.level(); + WeaponDefinition w = set.definition(); + Vec3 offset = muzzleOffset(set, mount, 1.0F); + double px = vehicle.getX() + offset.x; + double py = vehicle.getY() + offset.y; + double pz = vehicle.getZ() + offset.z; + Vec3 dir = Vec3.directionFromRotation(vehicle.getXRot() + set.rotationPitch, hullYaw + set.rotationYaw); + for (MuzzleFlash mf : w.muzzleSmokes) { + double x = px + dir.x * mf.distance(); + double y = py + dir.y * mf.distance(); + double z = pz + dir.z * mf.distance(); + for (int i = 0; i < mf.count(); i++) { + double r = mf.range(); + level.addParticle(mf.size() >= 2.0F ? ParticleTypes.LARGE_SMOKE : ParticleTypes.SMOKE, + x + (random.nextDouble() - 0.5) * r, y + (random.nextDouble() - 0.5) * r, z + (random.nextDouble() - 0.5) * r, + (random.nextDouble() - 0.5) * 0.05, 0.02, (random.nextDouble() - 0.5) * 0.05); + } + } + for (MuzzleFlash mf : w.muzzleFlashes) { + int rgb = mf.color().argb() & 0xFFFFFF; + level.addParticle(new DustParticleOptions(rgb, Mth.clamp(mf.size() * 4.0F, 0.5F, 4.0F)), + px + dir.x * mf.distance(), py + dir.y * mf.distance(), pz + dir.z * mf.distance(), 0.0, 0.0, 0.0); + } + if (w.cartridge != null) { + Vec3 motion = vehicle.getDeltaMovement(); + Vec3 side = Vec3.directionFromRotation(0.0F, hullYaw + set.rotationYaw + w.cartridge.yaw() + 90.0F) + .scale(0.1 + w.cartridge.acceleration()); + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, new ItemStack(Items.GOLD_NUGGET)), + px, py, pz, motion.x + side.x, motion.y + 0.1, motion.z + side.z); + } + } + + /** {@code onUpdate_ParticleSmoke}: smoke generators trail while the pilot has them selected. */ + private void smoke() { + WeaponSet set = current(0); + if (set == null || set.definition().type != WeaponType.SMOKE || vehicle.visibleThrottle() <= 0.1F) { + return; + } + WeaponDefinition w = set.definition(); + Vec3 back = Vec3.directionFromRotation(vehicle.getXRot(), vehicle.getYRot()).reverse(); + int rgb = w.color.argb() & 0xFFFFFF; + for (int mount = 0; mount < set.mounts().size(); mount++) { + if (random.nextFloat() > vehicle.visibleThrottle() * 1.5F) { + continue; + } + Vec3 offset = muzzleOffset(set, mount, 1.0F); + for (int i = 0; i < w.smokeNum; i++) { + vehicle.level().addParticle(new DustParticleOptions(rgb, Mth.clamp(w.smokeSize * 2.0F, 0.5F, 4.0F)), + vehicle.getX() + offset.x + back.x, vehicle.getY() + offset.y + back.y, vehicle.getZ() + offset.z + back.z, + back.x * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.y * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.z * w.acceleration + (random.nextDouble() - 0.5) * 0.2); + } + } + } + + // Ammunition + + /** Sends magazine and reserve counts to the crew whenever they change. Server only. */ + private void syncAmmo() { + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + if (set.ammo() != sentAmmo[id] || set.restAmmo() != sentRest[id]) { + sentAmmo[id] = set.ammo(); + sentRest[id] = set.restAmmo(); + WeaponStatePacket packet = new WeaponStatePacket(vehicle.getId(), id, set.ammo(), set.restAmmo()); + for (Entity passenger : vehicle.getPassengers()) { + if (passenger instanceof ServerPlayer player) { + ModNetwork.sendToPlayer(player, packet); + } + } + } + } + } + + /** Forces a full resend, for a player who just boarded. */ + public void resendAmmo() { + Arrays.fill(sentAmmo, -1); + } + + public void applyAmmo(int id, int ammo, int rest) { + WeaponSet set = set(id); + if (set != null) { + set.setAmmo(ammo); + set.setRestAmmo(rest); + } + } + + /** {@code reloadAllWeapon}: tops up every magazine from its reserve. */ + public void reloadAll() { + for (WeaponSet set : sets) { + set.reloadMagazine(); + } + } + + public int[] saveAmmo() { + int[] out = new int[sets.size() * 2]; + for (int i = 0; i < sets.size(); i++) { + out[i * 2] = sets.get(i).ammo(); + out[i * 2 + 1] = sets.get(i).restAmmo(); + } + return out; + } + + public void loadAmmo(int[] saved) { + for (int i = 0; i < sets.size() && i * 2 + 1 < saved.length; i++) { + sets.get(i).setAmmo(Math.min(saved[i * 2], sets.get(i).magazineSize())); + sets.get(i).setRestAmmo(saved[i * 2 + 1]); + } + } +} From a7ac1e6f745bb672d3de87b9ed98aea2e18f43b5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:45 +0800 Subject: [PATCH 0820/1461] Add weapon/WeaponSet.java to the 1.21.11 shared sources --- .../jnhprojects/mcheli/weapon/WeaponSet.java | 505 ++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java new file mode 100644 index 00000000..3f539030 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java @@ -0,0 +1,505 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * One selectable weapon of a vehicle: every mount that fires the same weapon, with the magazine, + * reload, heat and recoil they share. A port of {@code MCH_WeaponSet} together with the parts of + * {@code MCH_WeaponBase} and {@code MCH_WeaponCreator} that are not specific to a weapon type. + * + *

Both sides keep one. The server's is authoritative for ammunition and decides what is fired; + * the client's runs with the slightly longer intervals the original used there, so the client never + * asks to fire faster than the server accepts. + */ +public final class WeaponSet { + private static final int SWITCH_WAIT = 15; + + /** Fires one mount. Returns whether the shot happened, which consumes a round. */ + @FunctionalInterface + public interface Trigger { + boolean fire(WeaponSet set, int mount, Shot shot); + } + + private final RandomSource random = RandomSource.create(); + private final WeaponDefinition def; + private final List mounts; + private final boolean clientSide; + + public final int interval; + public final int heatCount; + private final int numMode; + + private int currentMount; + private int mode; + private int ammo; + private int restAmmo; + public int currentHeat; + private int cooldownSpeed = 1; + public int countWait; + public int countReloadWait; + private final int[] lastUsedCount; + /** Ticks since each mount last fired, {@code MCH_WeaponBase.tick}. */ + final int[] ticks; + public int soundWait; + /** Whether the shot being fired may play its sound, see {@code SoundDelay}. */ + public boolean soundAllowed = true; + /** The vehicle's own sound for this weapon, from {@code WeaponSound}. */ + public @Nullable WeaponSound ownSound; + + /** Aim relative to the hull, following the crew member who operates the weapon. */ + public float rotationYaw; + public float rotationPitch; + public float prevRotationYaw; + public float prevRotationPitch; + public final float defaultRotationYaw; + /** How far behind the pilot's view a turret weapon lags, for drawing. */ + public float rotationTurretYaw; + + public float barrelSpeed; + public float barrel; + public float prevBarrel; + private final Recoil[] recoil; + + // Close air support run, server only. See ServerFiring. + @Nullable Entity casUser; + @Nullable Entity casShooter; + double casX; + double casY; + double casZ; + int casDirection; + int casStartTick; + int casCount = 3; + + public WeaponSet(WeaponDefinition def, List mounts, boolean clientSide) { + this.def = def; + this.mounts = List.copyOf(mounts); + this.clientSide = clientSide; + this.lastUsedCount = new int[mounts.size()]; + this.ticks = new int[mounts.size()]; + this.recoil = new Recoil[mounts.size()]; + for (int i = 0; i < recoil.length; i++) { + recoil[i] = new Recoil(def.recoilBufCount, def.recoilBufCountSpeed); + } + this.defaultRotationYaw = mounts.isEmpty() ? 0.0F : mounts.getFirst().defaultYaw(); + this.rotationYaw = defaultRotationYaw; + this.prevRotationYaw = defaultRotationYaw; + + int wait = def.delay; + if (def.type == WeaponType.CAS && wait > -250) { + wait = -250; + } + int heat = def.heatCount; + if (def.maxHeatCount > 0 && heat < 2) { + heat = 2; + } + if (clientSide) { + wait = clientInterval(wait); + heat++; + } + this.interval = wait; + this.heatCount = heat; + this.numMode = def.type == WeaponType.MACHINE_GUN_2 && def.explosion == 0 ? 0 : def.modeNum; + } + + /** The original gave the client longer intervals so its requests never outran the server. */ + private static int clientInterval(int wait) { + if (wait < 4) { + return wait + 1; + } else if (wait < 7) { + return wait + 2; + } else if (wait < 10) { + return wait + 3; + } else if (wait < 20) { + return wait + 6; + } + wait += 10; + return wait >= 40 ? -wait : wait; + } + + public WeaponDefinition definition() { + return def; + } + + public String name() { + return def.name(); + } + + public List mounts() { + return mounts; + } + + public WeaponMount firstMount() { + return mounts.getFirst(); + } + + public int currentMount() { + return currentMount; + } + + // Ammunition + + public int ammo() { + return ammo; + } + + public int magazineSize() { + return def.round; + } + + public int restAmmo() { + return restAmmo; + } + + public int maxAmmo() { + return def.maxAmmo; + } + + public void setAmmo(int n) { + ammo = n; + } + + /** Everything not in the magazine, capped so magazine plus reserve never exceed {@code MaxAmmo}. */ + public void setRestAmmo(int n) { + restAmmo = Math.min(n, def.maxAmmo - ammo); + } + + /** {@code supplyRestAllAmmo}: one resupply adds {@code SuppliedNum} rounds. */ + public boolean supply() { + if (restAmmo + ammo < def.maxAmmo) { + setRestAmmo(restAmmo + ammo + def.suppliedNum); + return true; + } + return false; + } + + public boolean isInPreparation() { + return countWait < 0 || countReloadWait > 0; + } + + public boolean isReloading() { + return countReloadWait > 0; + } + + public boolean canUse() { + return countWait == 0; + } + + public boolean isLongDelay() { + return def.delay > 4; + } + + public boolean isOverheated() { + return def.maxHeatCount > 0 && currentHeat >= def.maxHeatCount; + } + + public void reload() { + if (magazineSize() > 0 && ammo < magazineSize() && def.reloadTime > 0) { + countReloadWait = def.reloadTime; + if (clientSide) { + ammo = 0; + } else { + countReloadWait = Math.max(countReloadWait - 20, 1); + } + } + } + + public void reloadMagazine() { + int needed = magazineSize() - ammo; + if (needed > 0) { + needed = Math.min(needed, restAmmo); + ammo += needed; + setRestAmmo(restAmmo - needed); + } + } + + // Modes + + public int mode() { + return def.fixMode > 0 ? def.fixMode : mode; + } + + public boolean switchMode() { + if (def.fixMode > 0) { + return false; + } + int before = mode(); + mode = numMode > 0 ? (mode() + 1) % numMode : 0; + if (before == mode()) { + return false; + } + waitAfterSwitch(SWITCH_WAIT); + return true; + } + + /** {@code getName}: the display name with the mode suffix the weapon class added. */ + public String displayName() { + String suffix = switch (def.type) { + case MACHINE_GUN_2 -> mode() == 0 ? "" : " [HE]"; + case ROCKET -> mode() == 0 ? "" : " [HEIAP]"; + case AT_MISSILE -> mode() == 1 ? " [TA]" : ""; + case TV_MISSILE -> switch (mode()) { + case 0 -> " [TV]"; + case 2 -> " [TA]"; + default -> ""; + }; + case null, default -> ""; + }; + return def.displayName + suffix; + } + + public void onSwitchWeapon(boolean infiniteAmmo) { + waitAfterSwitch(clientSide ? SWITCH_WAIT + 10 : SWITCH_WAIT); + currentMount = 0; + if (infiniteAmmo) { + ammo = magazineSize(); + } + } + + private void waitAfterSwitch(int wait) { + if (countWait >= -wait) { + countWait = countWait > wait ? -countWait : -wait; + } + } + + /** Another weapon of the same {@code Group} fired; this one waits and reloads with it. */ + public void waitForGroup(boolean reload) { + countWait = interval; + countReloadWait = 0; + if (reload && magazineSize() > 0 && def.reloadTime > 0) { + countReloadWait = clientSide ? def.reloadTime : Math.max(def.reloadTime - 20, 1); + } + } + + /** + * {@code getLandInDistance}: how far out, measured flat, a shell fired now would come down, + * stepping its arc a tick at a time as the projectile does; -1 if it would not come down in + * range. Mortars show it on the HUD. + */ + public double landingDistance(Entity shooter) { + if (def.gravity >= 0.0F || mounts.isEmpty()) { + return -1.0; + } + WeaponMount mount = mounts.get(currentMount); + float yaw = Mth.wrapDegrees(shooter.getYRot() + rotationYaw + mount.yaw()); + float pitch = Mth.wrapDegrees(shooter.getXRot() + rotationPitch + mount.pitch()); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + double acc = Math.min(def.acceleration, 4.0F); + double my = dir.y * def.acceleration; + if (my <= 0.0 || acc <= 0.0) { + return -1.0; + } + double mx = dir.x * def.acceleration; + double mz = dir.z * def.acceleration; + double gravity = def.gravity * (def.acceleration / acc); + double steps = my / def.gravity; + if (steps < -12.0) { + double f = steps / -12.0; + mx *= f; + my *= f; + mz *= f; + gravity *= f * f * 0.95; + } + Level level = shooter.level(); + double x = shooter.getX(); + double y = shooter.getY() + 3.0; + double z = shooter.getZ(); + for (int i = 0; i < 50; i++) { + Vec3 from = new Vec3(x, y, z); + Vec3 to = new Vec3(x + mx, y + my, z + mz); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shooter)); + if (hit.getType() == HitResult.Type.BLOCK) { + return Math.hypot(hit.getBlockPos().getX() - shooter.getX(), hit.getBlockPos().getZ() - shooter.getZ()); + } + my += gravity; + x += mx; + y += my; + z += mz; + if (y < shooter.getY()) { + return Math.hypot(x - shooter.getX(), z - shooter.getZ()); + } + } + return -1.0; + } + + // Firing + + /** + * Fires the next mount if the set is ready. The aim is the shooter's rotation plus this set's + * turret aim and the mount's fixed offset, scattered by {@code Accuracy}. + */ + public boolean use(Shot shot, Trigger trigger) { + if (magazineSize() > 0 && ammo <= 0 || isOverheated()) { + return false; + } + WeaponMount mount = mounts.get(currentMount); + soundAllowed = soundWait == 0; + float yaw = shot.shooter.getYRot() + rotationYaw + mount.yaw(); + float pitch = shot.shooter.getXRot() + rotationPitch + mount.pitch(); + if (def.accuracy > 0.0F) { + yaw += (random.nextFloat() - 0.5F) * def.accuracy; + pitch += (random.nextFloat() - 0.5F) * def.accuracy; + } + shot.yaw = Mth.wrapDegrees(yaw); + shot.pitch = Mth.wrapDegrees(pitch); + if (!trigger.fire(this, currentMount, shot)) { + return false; + } + ticks[currentMount] = 0; + + if (def.maxHeatCount > 0) { + cooldownSpeed = 1; + currentHeat += heatCount; + if (currentHeat >= def.maxHeatCount) { + currentHeat += 30; + } + } + if (def.soundDelay > 0 && soundWait == 0) { + soundWait = def.soundDelay; + } + lastUsedCount[currentMount] = Math.abs(interval); + if (isCooldownReloadTime() && def.reloadTime - 10 > lastUsedCount[currentMount]) { + lastUsedCount[currentMount] = def.reloadTime - 10; + } + currentMount = (currentMount + 1) % mounts.size(); + countWait = interval; + countReloadWait = 0; + if (ammo > 0) { + ammo--; + } + if (ammo <= 0) { + if (shot.infiniteAmmo && restAmmo < magazineSize()) { + setRestAmmo(magazineSize()); + } + reload(); + shot.reload = true; + } + return true; + } + + /** Missiles keep their mount empty for the whole reload, which hides the missile part meanwhile. */ + private boolean isCooldownReloadTime() { + return def.type == WeaponType.AA_MISSILE || def.type == WeaponType.AS_MISSILE || def.type == WeaponType.AT_MISSILE; + } + + /** Whether {@code mount} fired recently enough to show muzzle effects or keep a missile hidden. */ + public boolean isUsed(int mount) { + if (mount >= lastUsedCount.length) { + return false; + } + int count = lastUsedCount[mount]; + return interval >= 4 && count > interval / 2 || count >= 4; + } + + public int ticksSinceShot(int mount) { + return ticks[mount]; + } + + // Ticking + + /** {@code MCH_WeaponSet.update}, once per tick on both sides. */ + public void tick(boolean used) { + if (countReloadWait > 0) { + countReloadWait--; + if (countReloadWait == 0) { + reloadMagazine(); + } + } + for (int i = 0; i < lastUsedCount.length; i++) { + if (lastUsedCount[i] > 0) { + if (lastUsedCount[i] != 4 || currentMount == 0 && canUse() && (ammo > 0 || def.maxAmmo <= 0)) { + lastUsedCount[i]--; + } + } + } + if (currentHeat > 0) { + if (currentHeat < def.maxHeatCount) { + cooldownSpeed++; + } + currentHeat = Math.max(currentHeat - (cooldownSpeed / 20 + 1), 0); + } + if (countWait > 0) { + countWait--; + } else if (countWait < 0) { + countWait++; + } + prevRotationYaw = rotationYaw; + prevRotationPitch = rotationPitch; + if (countWait != 0) { + for (int i = 0; i < ticks.length; i++) { + ticks[i]++; + } + } + if (soundWait > 0) { + soundWait--; + } + + if (used && barrelSpeed < 75.0F) { + barrelSpeed = Math.min(barrelSpeed + 25 + random.nextInt(3), 74.0F); + } + prevBarrel = barrel; + barrel += barrelSpeed; + if (barrel >= 360.0F) { + barrel -= 360.0F; + prevBarrel -= 360.0F; + } + if (barrelSpeed > 0.0F) { + barrelSpeed = Math.max(barrelSpeed - 1.48F, 0.0F); + } + } + + /** {@code updateWeapon}: the recoil kick of one mount. */ + public void tickRecoil(int mount, boolean used) { + Recoil r = recoil[mount]; + r.prev = r.value; + if (used && r.count <= 0) { + r.count = r.max; + } + if (r.count > 0) { + if (r.max <= 1) { + r.value = 1.0F; + } else if (r.max == 2) { + r.value = r.count == 2 ? 1.0F : 0.6F; + } else { + if (r.count > r.max / 2) { + r.count -= r.speed; + } + r.value = Mth.sin((float) r.count / r.max * Mth.PI); + } + r.count--; + } else { + r.value = 0.0F; + } + } + + public float recoil(int mount, float partialTicks) { + Recoil r = recoil[Math.min(mount, recoil.length - 1)]; + return Mth.lerp(partialTicks, r.prev, r.value); + } + + private static final class Recoil { + final int max; + final int speed; + int count; + float value; + float prev; + + Recoil(int max, int speed) { + this.max = max; + this.speed = speed; + } + } +} From 9be73ea924c5d68876cecf8c615d9744b6e84f96 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:45 +0800 Subject: [PATCH 0821/1461] Add workshop/TestFlight.java to the 1.21.11 shared sources --- .../mcheli/workshop/TestFlight.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java new file mode 100644 index 00000000..4eea5656 --- /dev/null +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java @@ -0,0 +1,62 @@ +package com.jnhprojects.mcheli.workshop; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.phys.Vec3; + +/** + * Flying a vehicle straight from the workshop. The pack was just saved on the machine this server + * runs on, so loading it again is enough. That only holds for the owner of a single player or LAN + * game: a dedicated server does not get a pack until an operator approves it, and then it is no + * longer a test, so the screen does not offer it there and the server refuses it too. + */ +public final class TestFlight { + /** How far up to look for room when the vehicle does not fit where the player stands. */ + private static final int MAX_RISE = 48; + + private TestFlight() { + } + + public static void start(ServerPlayer player, String vehicle) { + MinecraftServer server = player.level().getServer(); + if (!server.isSingleplayerOwner(player.nameAndId())) { + player.displayClientMessage(Component.translatable("message.mcheli.workshop.test_flight_owner_only"), true); + return; + } + if (player.isPassenger()) { + player.displayClientMessage(Component.translatable("message.mcheli.workshop.test_flight_riding"), true); + return; + } + ContentReload.reload(server); + AircraftDefinition def = MCHeli.content().any(vehicle); + if (def == null) { + player.sendSystemMessage(Component.translatable("message.mcheli.workshop.test_flight_no_vehicle", vehicle)); + MCHeli.packs().reportProblems(player::sendSystemMessage); + return; + } + ServerLevel level = player.level(); + AircraftEntity aircraft = ModEntities.typeFor(def).create(level, EntitySpawnReason.COMMAND); + if (aircraft == null) { + return; + } + aircraft.setType(def); + Vec3 start = player.position(); + for (int rise = 0; rise <= MAX_RISE; rise++) { + aircraft.snapTo(start.x, start.y + rise, start.z, player.getYRot(), 0.0F); + if (level.noCollision(aircraft, aircraft.getBoundingBox())) { + break; + } + } + aircraft.startTestFlight(start); + level.addFreshEntity(aircraft); + player.startRiding(aircraft, true, true); + } +} From 692806c0bed4d435a43b1391f5b30117116e1b2d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:46 +0800 Subject: [PATCH 0822/1461] Add assets/mcheli/blockstates/drafting_table.json to the 1.21.11 shared sources --- .../resources/assets/mcheli/blockstates/drafting_table.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/blockstates/drafting_table.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/blockstates/drafting_table.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/blockstates/drafting_table.json new file mode 100644 index 00000000..82992af9 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/blockstates/drafting_table.json @@ -0,0 +1,5 @@ +{ + "multipart": [ + { "apply": { "model": "mcheli:block/drafting_table" } } + ] +} From 984c6525e57c72c35c3c46fa6dcc66580ce6f14c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:46 +0800 Subject: [PATCH 0823/1461] Add assets/mcheli/items/drafting_table.json to the 1.21.11 shared sources --- .../main/resources/assets/mcheli/items/drafting_table.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/items/drafting_table.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/items/drafting_table.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/drafting_table.json new file mode 100644 index 00000000..a3afc683 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/drafting_table.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "mcheli:block/drafting_table" + } +} From 2f2ce142dee9cea8b3f23836b8a250dd65690e87 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:47 +0800 Subject: [PATCH 0824/1461] Add assets/mcheli/items/helicopter.json to the 1.21.11 shared sources --- .../src/main/resources/assets/mcheli/items/helicopter.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/items/helicopter.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/items/helicopter.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/helicopter.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/helicopter.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From eef5a676d700b90fac126b96a18b44df05d78e74 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:47 +0800 Subject: [PATCH 0825/1461] Add assets/mcheli/items/plane.json to the 1.21.11 shared sources --- .../src/main/resources/assets/mcheli/items/plane.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/items/plane.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/items/plane.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/plane.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/plane.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From 60782923019b671e6a92cbc91b66161a7f2fe3c6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:48 +0800 Subject: [PATCH 0826/1461] Add assets/mcheli/items/tank.json to the 1.21.11 shared sources --- .../src/main/resources/assets/mcheli/items/tank.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/items/tank.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/items/tank.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/tank.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/tank.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From 344016fb40a6d024a1fdb2a15bfdacec4e26daf5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:48 +0800 Subject: [PATCH 0827/1461] Add assets/mcheli/items/vehicle.json to the 1.21.11 shared sources --- .../src/main/resources/assets/mcheli/items/vehicle.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/items/vehicle.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/items/vehicle.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/vehicle.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/items/vehicle.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From 1a2fb3edab1cee4bbcbd2668a4b6c7619fda4d27 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:49 +0800 Subject: [PATCH 0828/1461] Add assets/mcheli/models/block/drafting_table.json to the 1.21.11 shared sources --- .../mcheli/models/block/drafting_table.json | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/models/block/drafting_table.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/models/block/drafting_table.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/models/block/drafting_table.json new file mode 100644 index 00000000..8dfa5bcc --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/models/block/drafting_table.json @@ -0,0 +1,27 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/oak_planks", + "legs": "minecraft:block/iron_block", + "board": "minecraft:block/oak_planks", + "paper": "minecraft:block/white_concrete" + }, + "elements": [ + { "from": [1, 0, 2], "to": [2.5, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 2], "to": [15, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [1, 0, 12.5], "to": [2.5, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 12.5], "to": [15, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [0, 10, 1], "to": [16, 11.5, 15], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { + "north": {"texture": "#board"}, "south": {"texture": "#board"}, "east": {"texture": "#board"}, "west": {"texture": "#board"}, + "up": {"texture": "#board"}, "down": {"texture": "#board"} } }, + { "from": [2, 11.5, 2.5], "to": [14, 11.6, 13.5], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { "up": {"texture": "#paper"} } } + ] +} From cbf2ca66d587aadff262e4b5a496c53281267d1e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:49 +0800 Subject: [PATCH 0829/1461] Add assets/mcheli/particles/glow.json to the 1.21.11 shared sources --- .../src/main/resources/assets/mcheli/particles/glow.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/particles/glow.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/particles/glow.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/particles/glow.json new file mode 100644 index 00000000..296e5397 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/particles/glow.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "mcheli:glow" + ] +} From c528bfb48590e4c2bc97dceb788503cc5b9f50b0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:50 +0800 Subject: [PATCH 0830/1461] Add assets/mcheli/particles/smoke.json to the 1.21.11 shared sources --- .../resources/assets/mcheli/particles/smoke.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/particles/smoke.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/particles/smoke.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/particles/smoke.json new file mode 100644 index 00000000..977381be --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/particles/smoke.json @@ -0,0 +1,16 @@ +{ + "textures": [ + "minecraft:big_smoke_0", + "minecraft:big_smoke_1", + "minecraft:big_smoke_2", + "minecraft:big_smoke_3", + "minecraft:big_smoke_4", + "minecraft:big_smoke_5", + "minecraft:big_smoke_6", + "minecraft:big_smoke_7", + "minecraft:big_smoke_8", + "minecraft:big_smoke_9", + "minecraft:big_smoke_10", + "minecraft:big_smoke_11" + ] +} From 5f6f45a7a8ec49fcc2427215d4081b807691b14d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:50 +0800 Subject: [PATCH 0831/1461] Add assets/mcheli/post_effect/thermal.json to the 1.21.11 shared sources --- .../assets/mcheli/post_effect/thermal.json | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/post_effect/thermal.json diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/post_effect/thermal.json b/minecraft-1.21.11/src/main/resources/assets/mcheli/post_effect/thermal.json new file mode 100644 index 00000000..593e905d --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/post_effect/thermal.json @@ -0,0 +1,38 @@ +{ + "targets": { + "swap": {} + }, + "passes": [ + { + "vertex_shader": "minecraft:core/screenquad", + "fragment_shader": "mcheli:post/thermal", + "inputs": [ + { + "sampler_name": "In", + "target": "minecraft:main" + } + ], + "output": "swap" + }, + { + "vertex_shader": "minecraft:core/screenquad", + "fragment_shader": "minecraft:post/blit", + "inputs": [ + { + "sampler_name": "In", + "target": "swap" + } + ], + "uniforms": { + "BlitConfig": [ + { + "name": "ColorModulate", + "type": "vec4", + "value": [ 1.0, 1.0, 1.0, 1.0 ] + } + ] + }, + "output": "minecraft:main" + } + ] +} From 0ed48ae5f8db17a625693096d869598df10ce9ef Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:50 +0800 Subject: [PATCH 0832/1461] Write the thermal shader in the GLSL dialect of 1.21.11 --- .../assets/mcheli/shaders/post/thermal.fsh | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/shaders/post/thermal.fsh diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/shaders/post/thermal.fsh b/minecraft-1.21.11/src/main/resources/assets/mcheli/shaders/post/thermal.fsh new file mode 100644 index 00000000..24f82a83 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/assets/mcheli/shaders/post/thermal.fsh @@ -0,0 +1,37 @@ +#version 330 + +// Thermal view, white hot. A post shader only sees colours, so heat is faked: bright and warm +// colours (fire, lava, skin, lamps) read hot, the sky and water cold, and outlines are drawn +// where the brightness changes sharply, like the original's "pencil" shader. + +uniform sampler2D InSampler; + +in vec2 texCoord; + +layout(std140) uniform SamplerInfo { + vec2 OutSize; + vec2 InSize; +}; + +out vec4 fragColor; + +const vec3 Gray = vec3(0.3, 0.59, 0.11); + +float heat(vec3 c) { + float luma = dot(c, Gray); + float warmth = clamp(c.r - c.b, 0.0, 1.0); + return clamp(luma * 0.7 + warmth * 0.6, 0.0, 1.0); +} + +void main() { + vec2 oneTexel = 1.0 / InSize; + float c = heat(texture(InSampler, texCoord).rgb); + float l = heat(texture(InSampler, texCoord - vec2(oneTexel.x, 0.0)).rgb); + float r = heat(texture(InSampler, texCoord + vec2(oneTexel.x, 0.0)).rgb); + float u = heat(texture(InSampler, texCoord - vec2(0.0, oneTexel.y)).rgb); + float d = heat(texture(InSampler, texCoord + vec2(0.0, oneTexel.y)).rgb); + float edge = clamp(abs(r - l) + abs(d - u), 0.0, 1.0); + + float value = smoothstep(0.08, 0.85, c) + edge * 0.6; + fragColor = vec4(vec3(clamp(value, 0.0, 1.0)), 1.0); +} From c59bd867a691c67c15c3a3f2e4e4e062cd7e32d7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:51 +0800 Subject: [PATCH 0833/1461] Add assets/mcheli/textures/particle/glow.png to the 1.21.11 shared sources --- .../assets/mcheli/textures/particle/glow.png | Bin 0 -> 705 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-1.21.11/src/main/resources/assets/mcheli/textures/particle/glow.png diff --git a/minecraft-1.21.11/src/main/resources/assets/mcheli/textures/particle/glow.png b/minecraft-1.21.11/src/main/resources/assets/mcheli/textures/particle/glow.png new file mode 100644 index 0000000000000000000000000000000000000000..339b97292b3a86cec21a5a2c2d4e9159da1241c6 GIT binary patch literal 705 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFm-skIEGZjoxNt=CF&@0?BmDQ z$G05=nLjo)u{~0d=sc02G=rmQ;>?7sgruZLOlR&iZR9TC);p3q<+=OyiMy`WR_B%6 ze!zcpW%0hxt6uJ`UOHv3s-)z$2S*IQ{M+2aBkl2V^1+p-UiNMId~?@~iHS`VvGKGtbJ(?@PMX`Q_JU)-AEGyIw3c zQrRtbha-<|yVkj!cRxOeDwmZ>DLa>nOo)xwXnye(C@s6DY4^dsUB4m0R;efKDR26HaPb4r4<;3I zGNE&B-ud`Y+f9cz$|Q5W??V51i?`b=Utv|8`!Zyb_`%f=Ny@jd)@AxOp)i%w?!a<|&&Tjr8oN`JQToU~(`->fg1 zZdP9Nqf5}cIn2IwE7PsJ)h7Sugyi*n%i6MQ@`K~}e`S@^nPlVK#j7xFV z+_j8uPnwhRR~zlgnR{cs;OihI&w3VnZD^oOJGP2v)|RI;bPnXz1l3;M6Sw&D^QW!V zDOSc=cfEdS|A;M20|xneiH#;17nkTB-(PMJ>hSJ9`!Q!Lt-~I+X1}kkT&Mak7@_E? z^^gDD5jm{g_J>^m^K5+4zM_`ByS7X6x8|EK^8>{;-q`=xrFCc5-2VzEHJ@mS{q(#h zwlHh)?U((R_Hk6-{(1h>N~aR1kZ_?NOSWy;`Ple&>FE<27@gj_D0q6`GXA*nq@huc lV^{v7@AFerZkX0Ht_;k6d;0MSU0~8=@O1TaS?83{1OQn@Sb6{e literal 0 HcmV?d00001 From 1081b3555b4813061f99fd7febb847607e26113b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:51 +0800 Subject: [PATCH 0834/1461] Add data/mcheli/damage_type/bullet.json to the 1.21.11 shared sources --- .../src/main/resources/data/mcheli/damage_type/bullet.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/damage_type/bullet.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/damage_type/bullet.json b/minecraft-1.21.11/src/main/resources/data/mcheli/damage_type/bullet.json new file mode 100644 index 00000000..92ee6766 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/damage_type/bullet.json @@ -0,0 +1,5 @@ +{ + "exhaustion": 0.1, + "message_id": "mcheli.bullet", + "scaling": "when_caused_by_living_non_player" +} From 3f7db27eff72c2c4d61249fdf607884b0fd4ea9e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:52 +0800 Subject: [PATCH 0835/1461] Add data/mcheli/loot_table/blocks/drafting_table.json to the 1.21.11 shared sources --- .../mcheli/loot_table/blocks/drafting_table.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json b/minecraft-1.21.11/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json new file mode 100644 index 00000000..610afa08 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { "type": "minecraft:item", "name": "mcheli:drafting_table" } + ], + "conditions": [ + { "condition": "minecraft:survives_explosion" } + ] + } + ] +} From 80604addcfd3d5a9aad27fe579faf2c49c294be0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:52 +0800 Subject: [PATCH 0836/1461] Add data/mcheli/recipe/chain.json to the 1.21.11 shared sources --- .../main/resources/data/mcheli/recipe/chain.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/chain.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/chain.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/chain.json new file mode 100644 index 00000000..484f5463 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/chain.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": "#c:ingots/iron" + }, + "pattern": [ + "I I", + "III", + "I I" + ], + "result": { + "id": "mcheli:chain" + } +} From 56ce8922967ca1ad48cfe564d53dd95a98bee316 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:53 +0800 Subject: [PATCH 0837/1461] Add data/mcheli/recipe/container.json to the 1.21.11 shared sources --- .../resources/data/mcheli/recipe/container.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/container.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/container.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/container.json new file mode 100644 index 00000000..4c3bdcd9 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/container.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "#c:chests/wooden", + "I": "#c:ingots/iron" + }, + "pattern": [ + "CCI" + ], + "result": { + "id": "mcheli:container" + } +} From 5f5a1374df33799548fc165f04a43e2e80ce06b6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:53 +0800 Subject: [PATCH 0838/1461] Add data/mcheli/recipe/drafting_table.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/drafting_table.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/drafting_table.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/drafting_table.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/drafting_table.json new file mode 100644 index 00000000..f3d6324e --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/drafting_table.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "minecraft:crafting_table", + "F": "#minecraft:wooden_fences", + "P": "#minecraft:planks", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "R ", + "PCP", + "F F" + ], + "result": { + "id": "mcheli:drafting_table" + } +} From 1fdbab668af5f15ced7cd97220756f8a60e2baa9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:54 +0800 Subject: [PATCH 0839/1461] Add data/mcheli/recipe/fgm148.json to the 1.21.11 shared sources --- .../resources/data/mcheli/recipe/fgm148.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fgm148.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fgm148.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fgm148.json new file mode 100644 index 00000000..c2c61fd6 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fgm148.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "III", + "GR " + ], + "result": { + "id": "mcheli:fgm148" + } +} From 8e0b9e9f9f755f0dd120a519f262f0a5d2e1dc35 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:54 +0800 Subject: [PATCH 0840/1461] Add data/mcheli/recipe/fgm148_bullet.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/fgm148_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fgm148_bullet.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fgm148_bullet.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fgm148_bullet.json new file mode 100644 index 00000000..c87b4800 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fgm148_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:gunpowders", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + " R ", + " I ", + " G " + ], + "result": { + "count": 2, + "id": "mcheli:fgm148_bullet" + } +} From 3411cb533de72c47b3bfc3de9766659896756301 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:54 +0800 Subject: [PATCH 0841/1461] Add data/mcheli/recipe/fim92.json to the 1.21.11 shared sources --- .../resources/data/mcheli/recipe/fim92.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fim92.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fim92.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fim92.json new file mode 100644 index 00000000..33a068e1 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fim92.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "G ", + "III", + "RI " + ], + "result": { + "id": "mcheli:fim92" + } +} From 1668c182150c82514b7fb25631d1b0af153cd042 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:55 +0800 Subject: [PATCH 0842/1461] Add data/mcheli/recipe/fim92_bullet.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/fim92_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fim92_bullet.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fim92_bullet.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fim92_bullet.json new file mode 100644 index 00000000..c472fd6e --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fim92_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:gunpowders", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "R ", + " I ", + " G" + ], + "result": { + "count": 2, + "id": "mcheli:fim92_bullet" + } +} From ea1d426459d5710131c9e3b91cb03584821a9770 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:55 +0800 Subject: [PATCH 0843/1461] Add data/mcheli/recipe/fuel.json to the 1.21.11 shared sources --- .../main/resources/data/mcheli/recipe/fuel.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fuel.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fuel.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fuel.json new file mode 100644 index 00000000..d076b041 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/fuel.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": "minecraft:coal", + "I": "#c:ingots/iron" + }, + "pattern": [ + "ICI", + "III" + ], + "result": { + "id": "mcheli:fuel" + } +} From 332359974e0f482c4037cc28617a1f98bce02bac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:56 +0800 Subject: [PATCH 0844/1461] Add data/mcheli/recipe/gltd.json to the 1.21.11 shared sources --- .../resources/data/mcheli/recipe/gltd.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/gltd.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/gltd.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/gltd.json new file mode 100644 index 00000000..104ac108 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/gltd.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": "#c:storage_blocks/iron", + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + " B ", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:gltd" + } +} From e51e8e68ede532ec70e6d778ac9c67ae82269ad5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:56 +0800 Subject: [PATCH 0845/1461] Add data/mcheli/recipe/m18_green.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/m18_green.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_green.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_green.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_green.json new file mode 100644 index 00000000..1046d9cf --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_green.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:green_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_green", + "minecraft:item_model": "mcheli:m18_green", + "minecraft:max_stack_size": 1 + } + } +} From 8972d4c20a666fe460b323f95e84a09e1c1fbd90 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:57 +0800 Subject: [PATCH 0846/1461] Add data/mcheli/recipe/m18_red.json to the 1.21.11 shared sources --- .../resources/data/mcheli/recipe/m18_red.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_red.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_red.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_red.json new file mode 100644 index 00000000..f98ac51f --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_red.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:red_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_red", + "minecraft:item_model": "mcheli:m18_red", + "minecraft:max_stack_size": 1 + } + } +} From 9a1855cb27bc2e810b41ff205264e012ce997023 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:57 +0800 Subject: [PATCH 0847/1461] Add data/mcheli/recipe/m18_violet.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/m18_violet.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_violet.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_violet.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_violet.json new file mode 100644 index 00000000..b05f48a7 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_violet.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:purple_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_violet", + "minecraft:item_model": "mcheli:m18_violet", + "minecraft:max_stack_size": 1 + } + } +} From bc07d9aceefbc18467211fa21f87080a40749f7b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:58 +0800 Subject: [PATCH 0848/1461] Add data/mcheli/recipe/m18_yellow.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/m18_yellow.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_yellow.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_yellow.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_yellow.json new file mode 100644 index 00000000..57ecd202 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/m18_yellow.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:yellow_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_yellow", + "minecraft:item_model": "mcheli:m18_yellow", + "minecraft:max_stack_size": 1 + } + } +} From 934b6b7f4650e0bf0cda9ea20b7bcb01e50eeeae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:58 +0800 Subject: [PATCH 0849/1461] Add data/mcheli/recipe/parachute.json to the 1.21.11 shared sources --- .../resources/data/mcheli/recipe/parachute.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/parachute.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/parachute.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/parachute.json new file mode 100644 index 00000000..69eab191 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/parachute.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "S": "#c:strings", + "W": "#minecraft:wool" + }, + "pattern": [ + "WWW", + "S S", + " W " + ], + "result": { + "id": "mcheli:parachute" + } +} From ce581fbc118852c306e35d41f7e64f68bb3c5cf8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:59 +0800 Subject: [PATCH 0850/1461] Add data/mcheli/recipe/rangefinder.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/rangefinder.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/rangefinder.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/rangefinder.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/rangefinder.json new file mode 100644 index 00000000..58a13385 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/rangefinder.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "III", + "RGR", + "III" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From 5ec9140d68e1b82b34753a629da395e6dd603541 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:30:59 +0800 Subject: [PATCH 0851/1461] Add data/mcheli/recipe/rangefinder_recharge.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/rangefinder_recharge.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json new file mode 100644 index 00000000..d6bac29a --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "equipment", + "ingredients": [ + "mcheli:rangefinder", + "#c:dusts/redstone" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From a0afb91b41e1d30b7ad265c551631f7bb1e67a93 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:00 +0800 Subject: [PATCH 0852/1461] Add data/mcheli/recipe/uav_station.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/uav_station.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/uav_station.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/uav_station.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/uav_station.json new file mode 100644 index 00000000..24b97d55 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/uav_station.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:storage_blocks/redstone" + }, + "pattern": [ + "III", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station" + } +} From 01e97875c633ec062c4fa5a52f0b7ca7e58fb863 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:00 +0800 Subject: [PATCH 0853/1461] Add data/mcheli/recipe/uav_station2.json to the 1.21.11 shared sources --- .../data/mcheli/recipe/uav_station2.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/uav_station2.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/uav_station2.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/uav_station2.json new file mode 100644 index 00000000..74bf0d20 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/uav_station2.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station2" + } +} From 1f2d6b09fdd6e83f76da8c06c601080527cabd71 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:00 +0800 Subject: [PATCH 0854/1461] Add data/mcheli/recipe/wrench.json to the 1.21.11 shared sources --- .../main/resources/data/mcheli/recipe/wrench.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/mcheli/recipe/wrench.json diff --git a/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/wrench.json b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/wrench.json new file mode 100644 index 00000000..b8c9bcff --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/mcheli/recipe/wrench.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": "#c:ingots/iron" + }, + "pattern": [ + " I ", + " II", + "I " + ], + "result": { + "id": "mcheli:wrench" + } +} From cdfb5f5e2dc5a08aa73c62fbc78b187e5d5761eb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:01 +0800 Subject: [PATCH 0855/1461] Add data/minecraft/tags/damage_type/is_projectile.json to the 1.21.11 shared sources --- .../data/minecraft/tags/damage_type/is_projectile.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json diff --git a/minecraft-1.21.11/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json b/minecraft-1.21.11/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json new file mode 100644 index 00000000..1042db9e --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "mcheli:bullet" + ] +} From b224f5d03239ba81df25b9e9071730560809947e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:01 +0800 Subject: [PATCH 0856/1461] Add mcheli_logo.png to the 1.21.11 shared sources --- .../src/main/resources/mcheli_logo.png | Bin 0 -> 23745 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-1.21.11/src/main/resources/mcheli_logo.png diff --git a/minecraft-1.21.11/src/main/resources/mcheli_logo.png b/minecraft-1.21.11/src/main/resources/mcheli_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..95c72198d76d40c8c513965b3c40c83712ee54cf GIT binary patch literal 23745 zcmYhi2|SeF7e9VyVeAH3vJQ%}WZ#z=Nfe=wO16}(vV_PoGbpr(RV-1m9k^W3+#H05LyW&;3lo-#Xe4geT* z2?H$5(8uLlh3?RM+^G{rwjoZ7>A@j(O+%YqnPXv(3ohVbbBWfxnA*g|DmnsC@J^w*!<}JV&wSul!dC2 zLt!s!f3yeeO~1PMF_^Jlz;>iuWCky}UX7-%PPN%Be-|7!waf1JPBX3xr#?ui9P13> z$qlu={F6Yvx3fAWU%aDK(?Y+HWfE`}YVg{{-?_QA-zRNN3C{!luNx30``K#p+_uQ> z>eQRnxLpl*lQh}^f|_$(xZUaW2}b|Ugt*Vii7|GD(eD?ATQeAdss?DCCf2+dn4{E>X`G>og9cU(oP_I^ZE-5r} zL$cd%@N9*d{n6Y1edqq}=#faPjf=N8_kVc0X><2V!``Y`1a;L;aie~C55EJA$;rD_ z&kdaGyb~*_^bJN`_<+I-f+g-_Pm+@Jz-2=K$@pazHV!pBj|MXu-z1?~3@E&s_Dxw+ z92;*-DiOx~iCAY8D2jwb{qhA0gY6RcXeH;m*Y%8rguRIWcl*o#Mkx&k+pBibQwTD2 zr-B54I&-0d<*7H!Z78CWv&IEz#3sjbIULO(;?6VZ78M??L$=jkO%I);xltZaQ~pnt zUn*JiI(<_XU$ZyjothE`?mI_|32hq;*zWfvr}Z&_@7Cn&*L=n+0eWAeoYi9C^)o3_ z&{zt$RQ0t4w8XW%tUD%-Fq~j3*Cz)%Deq+9U!cK_ojAHUn>amG+~Yn>Lcc!vsj(Wi z3`aWI51cXt-lc*06>9ogPn$e7Wl9##ZlwHvNVzV{82#kuvNgfPGQiIaHAJmXH=kOTnIgcebhPTT{t> z=YUFFVKHVIW*yI;KDQAw(>&1}pc;aBPk7rj*1(FU4H>|)QW~HRG!AIh9cv0!txf;) zIjeH+pT(%MBx8c&g+k%EmvE(DiP5-wMs|oVnVtNCi*G__Ylz+&YIY7t<-odau`1l_ z&#!M72OStVbHv`jRk(=#)x0eE>Xdh)r(FZv0fKLwvvfUbgVDcxe!=+1S+iQE`Ex(s zy}f>M>w{v5h@B=C6f(1oZBH3>oGB4Lu3@0{RPdSBt+SH?e;#H=LpI{#qB#XBTSvMu zMZ%-yxZo1N?n1^RO84&t3*W&8y@MI4%I5-FPSY+0x|AZK9B#YZi#%6vhejHL*QB&Z zzKe?^@g-`t_6-4Rzlu(^ZQrj?O8?3f#l`GogmJb0Wi*Z;J@{6=k`4_%*0Juy*%e>( z+UwzdGkfiyurI94uGg53Va9KU$^3;{wsvm+upRrgJ-q&UB`AL;-ybr+O#jmpgLM*d&o7 zQm`mNp%d&;-<1_0L)~wRr#xRW^$wTq3;6g`BqO!k`;r~&yAL@6*aAI_6bc?|bQNg+PJr@S`nVCBJZTpXKMrzl$@%#R00=F#gE<4gmOB5-}{2myYcpwac+^dih z%FCr#w{X3_H_d~oqod2iA;To=hd(a*3lglvM7PTqh@@y(iM$v`O%x{06_yYm(u%?z zep2Ct{nR|TvFVc^tg5G#XWMpuwEuOvUATna%2pX7S`3}$g0?)5=3#-~443%;lK?Mp zPGJ!mBMJ9bav)oZXyv6BO&*5yqTI;FTw~e56gg5#%Kiv7^thnA7TdS*=2=*eu(L;K zNz9CUnax7(zvTOwzb*g3jku#*jYyUPF{zx5B5*^{nJ8+5=dP`@pZr}w{V_WFKU=+) zJ9<>!ePU*l4W)f{mX zBPIJWAm{iu{{sXiJ?-V|Gq~K@0%#+?ea*TOmCNSzmC2l;3hm~bp6o!f%ynz`-qd}{T^UA=TGd&kP%M^Nu z%JrVMt)9aQ#zA-IWvMXR9w0L)7+i@!aA zq+#bnvoL;lDfV@_D|51xTH#;f(4nL^H+lKgu2lc!czAB;YV&290S2!~yEL$yxRpOz z@)qHw^`Y~-k;~~H@81UW*WY0OrRgbKK62F|+wq!6PHoSUDl?ipj{TtY$r|JY>bSmS zG1^&wx(1q}=)IUf(^`*8k|j9brbxg@(4SOCSG-b(MDfZUmo}n(JjFW{^)cYt@uGdZ zhzb28jw2D}P9f3#g%i9V${e$$K;i>ow-;*-kxvgw4}LHj;>ve4L<^e|oNOy_&wXMU}S&)`0s{Ce!Q!GLeN zDd*GG!i&{X-s08|%dVXMu8{SYc$@`wlFM7TylQsy-3ipC0=WfdE5S15A+V8 z?7Hg`S3l?>U!-T4Up)TGOXs;9F>y#!t?+=!V(!pYA#ZsyxYy3Za+s-KmvsKy#h6QM zN6gASZ_1s;VI)b@$2*XsxhZT%aLfSieZbln7R3x35UjE$lANx5%BngZ)Ho)1DEaC8 zd)me|l1o=Pq{Qz$xW#0?fpFJ!z`hCUw>5uy}R73dwF z+Q`0|)t_DW{QPi&s`<`YpP9%QwkqztZ1E|%C05Ry$FLW1^?Dn^r_=9MnQdqeXpnr4 z=S9Sv+cGph@Q0EG>Bs|D+qh8M_I;p$(oJ06Ma!|9dC>gMbaixU{N;Gy%%1B+A>yNp zEQkefx4`^Jj?%_Ir;`S!cdZ9I4GJGxo5EgAWFNEb&bq7nm*2+6Qu5k00qf99;(2+W z3N&wi(2oY_L<~R%VoIZl$Vd@n6ebS9HF!Ze-*20dT=?s;{8nYPN5iTGv95pnv+>V= zyvJUVDvTfaZ3 zwsA7D|Nec^%nL4b)Qj3*b>?xw4UD!d35V-oK7>5PQga*tzbITe68En0a>IE1xzij0 z=5DcW^L_`E)*7~NPjB@uylJ-Ypj-|2iSs+7#HoYt*S=IS?G(B(fLNaz9P3u9r~9zf zPnL%@eY&61(qn>%y?dC8Nfx(!{h=_kE*g1gVW|C6yJq6eD9!mF5k!>}@<*0V=};Yy zYP^v?oHuQA<<_U$X6WlIehFrFcL2u3)O+;$&%E``FA33+(-7Y64U-5z(Qwi-H@ECK zEb4|SOQCU;r=;~Y7*RzhbaU9pJ%O{eQe!uOP1Vfb%HN~|tLeJ{%-&|8zgF9hd zv{VYCaW9x~q2Gv!>a^I*#}0D+5O&U$FyUy+5|6<*IqqJUMY*v?%CUnC*o~UKv*r+l zw|h~x_{EfSEqFoWXGs6=4~`j&d{GySNZ;YO)15Gv55B8GYqOo@4xUpxyOa6V(L?UD z;w#ntaw`>CoZa-Yp>EAfIi8ATDz*#oD*K#`2b#kk11C+kj8ZLb-s*Ap{8*%@IrO?m zQ&3EMIpuKa$a;$QG(+*X{`FKruj2TJV(tC*44l#SR(PEMm$Qsx};E zcSNRzpGDv7Wj`wUElnse%wqN+oDFCrU+aUw2ZJ_1DPgy!bMrTLk~^ZVBy&oMP)xaY zJF{#4cgNo~(|}vk5!=8ucYLKR{_{Dv>@x zOF=x{YR**sX>iIqQ~al)ii;KdUuk1%rE)!q{n}KZx;URq`8TeMY9t4xaVEH>6-pb~ zuK^$61wIb~#cUbkn z-(4YISY7*y|B+cTxul0{f44`HUFNU9z0!C*+pzy#0Qdax`{%j>L0Mc>v7=fA#1E>g zM>Gz%+&-K&@>Oyq6jp*Rbx_ES#>F$?w9nbYHPK|xVM`9JUcIvqA06x0Ef6XHrkMAf z%iw4^N%mUsKXaYK^umY&hHZBchX1yRS$gP6Up(W6b40PknUZ}^M=RyexQDy6oqxEJ zCyw^onf+pXT>YeU9e0w*fih(w?c|vCMPE-pN}YPChlFtVN39n@3qFM&r|jhv=PVo_ zo-VX8QsJNBw|4bGeQ)!&T2%XObKYCvT3K7}`t5@;%dZdtuik&|*S>n^{fDv($8k}( ziYR3$bpkawGKz@$ropJ%pnte^wx`g~)=(H0A9>PfvNgs#V>p9%=8(&oi z87H}!imoreHzc@RBnbBh6SXGwlASb!wWQkb>`f~QOq$>PUH0na`hrq_Qq0EOyMaU} z125~XUJ~=m+XZ;Xs&f}-I;ZCQUxf+?qUntoM$3Kc%zCV&W1E(KMy$8*C|0q- z+6lMFKb4o_En)3*nt3WW<-4WI3w~?&)A=wME1ddAdSi{5rp7$)!Cr&IiSeFbgRui4 zWH_j7okJM@Np{vuUMZ?OKgIc=*+jaH*>tiEmlzp|F)GD)Mr95+6#VS2K4S3rV2C?f zIJ233-J0bbQsM6Si!Tb%B9X_U4L#vr4~V(`E>cBNWw-eAJ&<+>x?k&2J_tE`4?N)S zSCKZj!T;N=uHbp7!FSN{v$^J`avS@TSD;}2-LK;idS zfXVgn^f|l9qWrtxKCk4Re|Yool}xjyFQn*bE*4#(6KwF3U$`1cQuD=1^fGLmWY*CI z8^4NY1xnLN5@+o?oEar}q2cGN^elk2<`q z3*{hR{|;|Yfy#%v*RGyW5`TUyn;PCL(aC|9WL3 z`gL^K5Pa~}8P2|p?iE8TYVG=tr2)P&pIaqISr(G9#2utJ;jIO^pYG28Mcy-W8Gp_L z^|+l#>|(DpA|K_mhM|u^mJ6}YQzu|N)0%hnr=T6GrH3dHdeZ zSc&`*r5c>+<8Ut(L%~fd?PFJT=w9vC+h6druns2Lq3u*q`&vz^_r6K~_*AOd2fWj$2E{S_mH7N2;qD$jtk4h#|LYi1NtG zGR!(iZSeITE)PkT%@ljDo|}HFitCf8#C^H!tzb1Qg`T4VWku3Wp&mF+Z*yW@ppxDCQOXg`8oSIPV;g~ zKgT?eV>Z_u_z*ADcc0lu@%f?WqShik+c!;=%lKeV;gUyAB<4z4pR>r5CZ1yg=Ma4O zNT3EV(bh;$C)`OTeimBHFz;?Nz1&8T@)k2>}>f=v6~QOCV$*9Kaj({6IH&N9!Lhz;W^ z?=h6R+g@zh;$+EePUdj^6SIMDHU(aNRT}-(N3d^i;Mif;%+OrOC~Hj_;@N$;X}c8Et#(&BX+mb zcQ%4k!j|J?*PN-}jZ#*7cLS-TTeBYwD7{|?vDp#MdqO|bzWlM`>dAf5Q26GiCR$gE z9Euq!y&q~UiJZf=|8CE}Or1;Ngf_%q zFP$*xE(KCAe0-vfOBXPm&C@WpK}e-65Y#&2BNXZ_xtg#9fOYRIL%EA;JtMuuTmmN zL|?M7>v8xZ3lYNt-(9cWzSVXo=(Tq(;m|bY;$<`E@IMhocGrH~cgE+u@)g_k!RIo% zYLB+>F~<{{)%PwW@7=fe>Yv=5ooI2&>Pe_S?l4M!Zpm1EGexglz8HNlvIGFd$}tk2+>H@1j#L_mCAY^(L4m^j?#WHRXiy zgZFodk94#^;YIR66ljLHk8^wloviGj`{x^Xhf(7fOu$tiS|)ZWqhrmqZaY5NOHmcC z7g95=Qq>s3Yi`YB%GhRl4Ar^7|H?4;T0$f>_|>$;(RS>_Sucix`ud*{_3bZ$p52J9 zKJsp6j$KK__)hM4HKnfLVOyvOdVCF@F@uylB>>X>fy>tf5$D&t-S-1mZD$X?6ag8( z=ntk0Z0&3*&OOtV-{oqKDtLXx6uqQP%5M+W;dzq7sO|~-y6|rGz0sd31o9`3qOq4A zv&}P3Ic^Hmm0S@tR?Z_wSCO#arg!-POct~%j6Z^tTwg~}ls4v`lo-p6aRTha=Ig)e z3Z`l?=9CgZez%p-^b>n&E$MHNGK00{(e>i27j2Z>mL~1J#fq__TW@xy1~wbU8V6_* zTG%t!QZurzXN25)oS}R34#95M`Od4;0jdVZ0K}n?X<(c=QjLpv+ay9UW=41U$F3wl zg&E9NeKuF5Fx^|5wer9hjE;5C{;V@k+lmS3!Ksz}0Z6dVA&88u!>)AN-?Tl)-Y8^4?kub=Ox>i;%gSk?SOq zK{~4EoGn4P9YY@C4?b6+zz3Wp(bqxcHCUn;KzDf@0G4z0{lzm*Y(;@e;Abr*s4Yxq zgOQjtD#4$xR_{njI2jo@O-Zyr(GbH`uuxJPx%AmX!{GhGsnD(3=s?=vu9vvAYiyZ2 zspWeOm8tbJJC6O6qb+mZu*k`RMG3wu@l7w>OqnFd)m4OiSz0gj!N|whwI{*sPv5ep z<;x-x;R?CEwE)PErQ`;4-BDr(qM-y<;KT3>+xxWmp~#~IrtY&;)QVDWb_ zv2JHt&s>?hns3S#Att?sm2LOUXY|r)wO*#y?^d-G26k9ht<2FM%O72dH?Ca^Fvg6R zT~QWh0U(hJJaLCXb9Q8o|0tAkedML-ak*i3K>9Y=#ePRAS)zA?!3&;jGIXfvUdtwK zo;ze+?Zg#vvA)fN7XI(_^D8J)MQ4U=_GLWTvE_e>I=r$T|$uYD|N zBd7n`0N15jia}dY;iZ6?TXOu(6|uM*0G+V!b!2m9^ijy8KPqh-z8u9q`iAvlyey*j zmk(l6{C?S1QGZ-3+sG0t+bge_t(Hi*1pjIuUOro%AlFVyr;Pf{=2_K$xE$O6ep1%+ zDn*>1J5u=r@ide^1b`<1Wb)uBzojawR}BAHF!BTg3nX)7`hq?Rc7HEVoxSf7+}9Fo z_))#8M6EzDAzbhib?Q;QI<#yfmcqU(6G}L%3k%fuhSK}br0ivw7WRE#w7oTGZ2M{R z?9Oy0oz|YiZRi3pKhS(r_IL>9ITSH4xb2N5~5pVOL$Sw1a`t*YZ zuRkBE=ORrrNC@9rQ`0v+6@SiqxL)yD{pcHVX-bfNO|T>+@xf9!(Pwz$!PKSPbDgi8kaT6Biu^u%F=)RldQC*cD;$r}CVMgq z-NXoHUcCB=z<4=#EaxihUB>kwW650!~H?6NQZYL!s1}t0~K!PtaxX`<>wn( zwhCdiSFs=e&3$O4v*qRWCzrGXbYD#8ikur{4&;^NnNL0(1H1Tu+C*W3vst5H_rfJv ziI-t9msiwGkI0`%!AAG?-K8=i&oxnw5{a-V0FXjYo4O9cB7fXO93*c4%DyRZ_`w(- z5}6{4QYHR94M3q|3tnK-6b^JYx9rpcmv`ONRg0$yM79(2V@AnXiRt>AYE#EXPay0m zrF6N~q^*DXOP^&x)lu~9KOQ&$jj+UY_j^P!FckL|cbteI8k5d}@qdk?-{r)vs1URP zc?-7Oylh7=U!DrSA+V!evbs>WsnSRL+U}T3wNIH=x~)v;%-%h+@FTu#cT!1V5bk=| zqkcl)lqDIocEd3#3OE#~P=I;`aAe>sq9E8dAao2=kBPtYTk|+JjI%dfehc}8yXC8Y zEI?`Biu&y1VEiH^6X`Sx9BpU)yCuHF3g(01JFh=i%{5hIYhm4K_oxqZsVYm(a?SLL zn_0t$^Qj+Q84jA(%~2cAjoM3A2{{p3IZ7#DcOWUC9z zG|c3)J~c&nE%jC#%h}`Qmsg$@c&;@hILM9Jcl4z?@1a-Z>MePau@Q{g=eO5%ho~>{ zs>?5GtBX8*QeZGc$yOMt4gL>cK0?I@9{?^ApdeR~GXS}rR^JPDB_bZ+X>;0(;X5Xi zlV5Dy6?0?Y=v$f7I(g4+f5!G!b7cD%%LoexHE{?NHf~hi`T3>7f_AvJ?n+KcJwt|tc;PBc6w*~UjJ%*s>P2AAr7H$n zUeQdKwPp8ya;Dtj>cJ|~HO&E@*S}RC&X<4BX1_Pju0f6^T=F>UQ!v`^R9H5qEUbP- zpR;zGw`zXqSKvnZN6)3G!(~^0IhOq^wkzBC66=-P-PcuX7Unc+y1Q9_G8Ri8?tahr zhkjkodBD7ORdwPelOH&t#!>hQ5UE{f+CPAC+RIR&?&-)yJh}bhZmxoN%5yJ$c@0Cl zEI#||)u}A0_yoo$4S6B{csA21k<49gw2of2K@es1Vb#{GiC{uya&y8#D|?HoNA6?F zech|qg1f)#QL;I=igQfG@yWMYYH6!&Wxiwb>z#U(Zja0epVm3)qB^5~0&qs3gum-?@zhqw2=hX` z64O*qx2NB)Z5AYwDGFf}H42B>{CJ;uY`!o0{Q zDt?)Lb0uge$+^fXRsjLL|8KNlafb>2rxzVy-AmvHl5_QNu+Gp0g|+oa;?QmYAf3f| zZ~-s8wId23VW9{vfaHuq8Qpa`Zk-iuMEvHGs$H3R7!zUlwvIae+a(Oz#YMr2D6=2H z4Tytq8aG&8cZTE6>_vmpQ+sEh=+n93xHh(SyOPLTeONW8>mr{=3(fltZ<+|QBo}bwIar22&R+KB=R(DojMAA{+tjbg9 zS?{Zft+!=?J$`dAXu?LpA0zdy=H6#EZAgV#r@fMif~z8OZEmNqB&+jid%@PE3%jdZ zG3FZmZz6*_=CiXnDAKf>%q7BRmY+u`k-$E&mG-H$!3%HFVoBORW>d>|h!=q}*W z%s$bKBp$SilMgtH)=dkPUTRk$KQe7RJi+JxAjD)i1)@<^U&{g)MgX6hBG9hCb_BZK z5eDiIpN9fBhw>bt+&eSFoe2o543et&fjh;QIf@NCtl++s|D(oKdOWIQaK8`c_qZlEYOd@Uf$GQ_|n zyNgRA{{tauVKHNVFUy>_s2e&I<<@_~pTH`K-HKo4LjiZ1D>Wy{HT= zA9t7lKu$bC0DlKJKyKON1@qfuJV1e-1jZ)`wtfoDd+*KmctTjnEfRP#Z`wHq8e{uj z7#D(Suq?=Nr!3j*=C#CgzIqHMVA0-z~(%1T;z_KdG_`U>Ce7wxt_bij_zV zAj^V=J8+WNo(Nbrgb8(SJ7+Rdzclp`keH~IsugMa;YNN^`j&5gB(+E#*RM05jv(o? zmv$mZjyfQ18m0}h(^=lW2XSqD#3Qoc&JIPL5qx^Dhg}lQlW4UV(#l&}ccox4^+(@$ zR2dpnaw4? zpAY**m=y5I67pZL&vcJ-6{(vg6Rc74kQ5C`a1 zxc2}0`}>TDr8^4cRn1Bq_h9eKl;$85Z{NvK4pVfc4Oz?MaF9vg%C|dN%^uI%IVFWa zH~SvxHxsIRuN?mDF91WhgU`Ev*j~}Q3bI#En0z{UE$Z#j$O4e)Njz|tT9j?n+Y1i> zo}%iuCz+?>PDXREkW_j~BbhY;F(wv`)DeQmkbg#kqps}hi(e`ml zjjvRY5ICa588=E#^tOU??NAhv!t=Dm&3LmD*J|LnR(4>0aT-lN2zqHwAW@0M7l!+! z1u)5hVD>tK_=V}H`s7od^>-{+#DCVM1^xYKlVSWlOR{vzxxhZwCyI-AuIP>2UOXW@sFQv&*dp)Aaa|}_K=7v>J`5v5 zRAm6bh-(A1mMDl*QUbV|9~Xeo7-Ad-X6e%VaYGM=%ir?>iM3c1(PGaS=E9uD27aj( z0o=ILA@zl)4`*M@WajtPGrUEB`6wcKnL6ktPAUx#i`-9!~>kDRYrmYG_vMY9%N3`Z<6R?-s&pHOK{ynv` z)YdiEU5%Q3S8LU~#$B9`O6tm*vggh@i1;{{GU^p*r*u}w<_*T+kL67dm95qjBCC4Qd z!)Hqw$*0~N>&PyD_R*6*b+OF{ZGu=6Aw`Sj8{Jg%MCK@`? zyt&>#yt&?9v$_6{wz;0xy~e)!D05q+#7%FrfuYUSGO4dE(fM#~GGZAY;#`9*o}A^* z+GE|R6EPJTj9+MpXctdDsfNn!Td(s9bi9}X1H)QN&Tz01wPR$ERo96GXbC4k3<*FG z(@@aa1dNyl9?3#c1y}-WS&$CMnE5iNU?!N4r zMs4Vwd--RM8TeFd8vvq3l{pyCFjw`8S01lw`{3EYe=%_E#aEKeN6+6HI_up*I%SSR z!7~=Fc-N~?9fMdt!PPZvD8k&ydVgABX!wM-Xu{ffxVBPf!q%7FKM^a8%Ew!qF?j66 z=;E743(XsohJbhx4%Bv70Sk-80v{Xbk41v{QxMRXg>aImEacbKVbE0eXVfYHB4vR; z69_{JT?ez`u=)KRAPA68yksM?vVr1RCoZBDHWb^4`~A|%JRtTt!eylufaa^c>2g5k zT5s(v{hp+j@7=o=)8kHDa`8zTgdwK7PF{%xQS#s+nsi%km^2Rj?8W@-X65bXp-^Yo8 zzyv6q|JBD`h9%A;Fmj;qI+%a~9hi_6DC8sF29;%RSRk?v*l$|oq@E07r;2_lf{tUN z&fZf`fVs>Sa$FJsX85Em zqj}-yv5!DBcG6!wNXP{W1DR~Za(jWEB4_;3SYl-+e^Qe0V8U)9sLZN|6P>)8=_25W z#zGS(xSarZV+D=(xq;w-I#|{K?~&vmdOmD(2G9wDovTovwkQZb0xksI_rkFf$G5SG z40C`)hRuDxCZyr7cna#sOm zk^6H(i+z_JNOZ~qr4Af_5tx&!Fti@1q4owyHvcNed!YflKES1eMr}tpS!l28E+nmon}Zb5T%xlPpvo!8qDNBAP;z-`}ZgTIDcON7o!JS$NL5dkhdvbCE+ zU^GGU52Yl_G;N+MX;U`Lw%<5h`raBzZuw9AWvvRb-DT?lG92@Y{Vgb5ja?r=m*@~* ze3ZGDJe{h9)p``A@{tR?2;u^R(F;3_1*PLl9&KO0@oS=%TR(Mt*%~>9--4k{n-D-_ zS)ZlfUJ|I9VMAe}WPQM#0Ws(I~}zE;WU1>k2;Ssa^mS>P6S48L(_co25EzZ?;f zMHOe}2Mk@Jko+aEEf2Gqn`X`0b3g~cKB8uX<)>YeUzhQsA%Uv`u861WAJi@I9~?kT zeB$+U@fQ&54u^OydndMo@L4ckf_!wG;Q{-K06#uSm%jfYc72p*nR_I_!K}M(V>KYg z$bC}_=H~`L@~QfuyLhQ?A0fZJvQ-7(7QzoVqntti70z$5qQwsyw@W0`8t(WRRJO(cEak6P2ijfT^bH18rT7? z3w|gDMNFZ2;lf{Y!s4wy7lDwy4SQqw7DLZhwYeqvNQ1Q6$TOhBO^$_KM>GHpc5?Cv zJQA=de?fgTKgfp$!1jhe&R(S9F4PctV2e;ciWt z9GBiAYwlQRHDH8f*Y1F7SK^dEDUH3`$R$Qc7LA0dSJQ%P9xwicxZpJVMf%hv_SJtP z$d}AoUSK^!;oQ<8Ts=FBB&|Xi#5xoFjF2F2kBZG@G->sjqKuQyya}CuFV?SzHTda1 zk0h~yvI;@ax>7F!XcUACC_FmGqVDA>QRUiuAw|c_!uq3ZLY7X_tE#g!!>v^nbt_K> zznkZu7jCoXv&2Ns#~_#NGYO~Vi$%=k3lj3G@15@wHNEcsI4|g9OHYi>q~~P(q-Wo- zf7!LskReTizCo#rzCmDHNazL6mWh?JfE8ruzfY`X*XNL-SALxcU449ieLbCT_LZJ& zZ-D%MnMZq)v+R2r683ZRy|dHyPUUluqoRCUI4Wxy>y)QC>Xcxh1|b-q2-DE0iY z9FN&Sibp+Lw*dH#Bq0F-?t%gvJS0AdIoIF0Bl@(WDf=k>%k|FMjkC=iW z0^~5j#QxXEeFO;HfD=RBF(q2T!$F!3J$zn$@y@1KN)Q#<77Poat0sC5f}b!V7Jh;z z^_UaT&LrpR&Pjaz>%rcTDUHuyXKfhdoh}&>Mz?0KIk24DwZ3gYn}lAM2FE zja|1-eqD~gml8LY%BI1`{{kcT_OtbIy^p<@NW5oXWk)?bVIuo5^JeM9XZtr*KSHHmQBB!mpo#@WbqVn}eeTRevP;?w`j z;^l_~7ey`h0@}54`a;v(jjR(){?Xe;qLLZ^Veo?!oc^)f#-dW!h(Gjh|G|%7qzWO)Qv7KoLE?+P_ zy4@-jaAmpYFyCy?@8&*Bi(yuSV`U4sEqK{n&m~-c^;vA!mGIx53jqX9%N<2^S^6>q z+V8lF*#~168aF1Y{OOh4!`w^q+ElHM8sA~;JZvNKKYzD&PU4)mbPGGsP5OF3_(Jk& z1e?VMg%r+=qdmS2<=J6bBif+5&J&`Q8W9ln&3Hto}yNpBXI?sMsXl9O;RM%V%$t_^q z)E~>dv6L|BS9AHF+uJR{ue;;)<+>2UWmK^#ep6!b{z6mGQX!3vQoZxk~BU=UyNUNkSUp_jiznx$H9) z${&ePS_SdWWMv!*Sijh(KtutloC}bunLs3zJ^lBEHERZFf)zxCptxa2WPdIHnB__IYAe zXl}qU2wL7c|M3UlJ4CRka}!;9|7Y@L1rWKY!CtAG$aLA^<;lSipG(Q3u4KZ0nBPFLJhFK=MPP< z3CWl!#Z$P5{yhP2loN5|Pk(mpfq$U_Cir|B2%U7es0k*sXEbA(r<6;dsT*vz`1fvq7;J&Efca!>BMot2?fN+a zw4e1U*BeuO!`BD1wi0CM!NPmeh9Nr=4_V3HtiTMSgiesSz#o0qxT{jiPwx;tp-o2? zF`(cQDBFjetp3TtB#I%=AO$WTibYOga0&0e;^44aU5jJ)>hu2C^|gsh%&3LZd8T)l zRqCHngNm?AgBP|a@3*vIE@Qit1hd7yx! z06CKdGuyNKi<3!I64QG_M;7o+I0XMqgerXanR!_*-%3@lO<&nZPG0!Sre;IFZg|J} zgk4jE$}ZY(1a5GD6jN13a0)n~Z+{*9JcGOiv^x-fpfOaO>|;P2bS3s>W5l4da^k3u zb_`tXoiU7ROSEFU#ZqX6>X$TPVSyVXSP}dzy5Ayl=lpBDX^GDX*3=vIO|%=mN>_h4 zpP{c5)#K(V9H5qZhe9daZZY5S-KAjxFyEp0)10LNy7PpKNjpOO9V*UU2)%Tb#D|PB zj^yn-Fdt653!1yCEO3Tm*0IO{=2T`oBeLtbT+{mb$=H0_txwK_YbYov-^*;LHG41& z3K*Th52}+ovzMN+7pul7(E%(0Qp)GofaL-#TZ}m*onsviV+Do>N>jPXxvy?(qeD&< zaWjU>R*LaW)@ zQl$!>*`ch{#=K|=A;m8NF%Wd0L7r6QMt=(>7RJC}EI(nmB&HK;f|yLUWh&r@zx+d7 zjkE4x4#Zfy62(PhnzzNKr@!uJp~o#sRO*Hn+Ds#cX&+{irU&D9bLQwl(-jN!bb-AS z&lq8M7`~1ELCm;HzR271aA_y*L~o{j+e~I-m|@8C3Lk(N@(~pzpc5C6QETwiniwNY zEVC%H3_921S*ohD%@%tK9!J%77REvk!DOt3sno9ojh#&yE82>!+&4?*wkZry=(V|f zRV##eK!hB7R>Y$6YfzZm8OH|>9rON9Yw}{8M^8J5Wy7#?(uc)?mlJA z2ph;?x&W*rFA_zN+kG%YrEw(u6~CE~8OH30-?y8$obSBt`K>3EnXm;pRVsA^O65k! zZmwD#W86PPx0l$HeEdcOV8p=JbX45DzsqZW=$Ne(fz$s8l+f0)OghNFjikFcKQq!* zgecPNn-0ezbTMc0kmXNtb$zYc=56m1B^yTKFFXn=$AyZpumZL9gH6~cxqHyb&Eqw| z)c{f2S5!FsE*)r;Bry?<1;~<^=(EQ`q%b?n1#zAal95nJnUPC7kFpRla$M?jiFat4 z)pZ*ALS_MtcGg*;y-SvXDVZMJV0Z?nQ=!1+NTF!ZP4x#^z=tg+5$kIqA2~g z_P|$kMb<}NDnD1Q|2crK!t63ZJJ~9y)=ZDLHf09rrI)wcD;LCU-ZvDe0bfA2u zw6!ZH5_7oY-RY5I31`nFtbMo56v6&nXnPJ-6ZyD7J!5(I^)ez(zw?R~aEWFEC5>}n zoT1$vAv9iggyn@VikN&A#sOzhHk1@Ph-@bXM-zR_&`&v!Ko8PHvLRWS^on-Bxj_$R zHqX@7rL2aj)kn-z-0?vZU4V#MGwp-6{n7>c)P#f=zA>O2MNDdm?BPJ92{8HZuON3A z)15`Hq5d~sZPRDgr$tKog2y6E>*<>!^sV~6JOX{eIgs{KWoMke$%iE*ZZs7bq)HzO zPcnn_iT8j#^s2P)L+Y~Gvirk{S> zF46X%2gAX%X~FqPQ0hmO>uP`q+fUW!S8RR?R6q!Z6tSycMXB4J#qLqLv33`xG6^)J z;E}q!y5-P;`aJZohKZJ}i`Jq2k#1$++?kCqND?W^NcMAr{>cnu4JhajIE@v}wkKqa z72UnbV3A>@g%T=E36{?YMu29Q+caS0)HX^l>C^eTJj2$IDQjzhcSHfJ=NS|y5Y&0 zQm;SE-18_ZQ*J>LiAMIw!-t~EMBo|{7+kk5>h@Tax-tFkP6O?p<}aeMpnBtBgb>Va zVkK2sN&Pgyuc54%7>>A2~3E`${{`bX=@)j0%U}`ISsLm2=sG#(ItY+Mfa< z?tK0%y{Rh@MpTDI_j3qg8{#Ayc_vu>?`E|Eol9fu6~%_lK%xPFJ{2mO=Rd?pNFj&ALTOe)uT+ z?9X@4@!Kn=cu$MA>5c307>-CB0K9NZrZ3!lW@xgg&o81)Bx|0L>v=Au-E2qRw&uXH zlZ8^ek}NgtfqI=c!|7N6kOOkVw6p6B4RH|Q>s-VHl|}e`b~}vie_sf?ufcAreVFa2 z7@dy2_&pM_>-C40>eWqx^%vzP>;9dxE?F9xO?gw$%~vg+L)hDOrX?n>*6;1stNS!! zOrg0Wu@)Gk@xvm1a>HqIXlj07s)gSffeK9-|d|vO!#CTYe}#_z$iya zqD5JFk?c|Kt>n7(o>+bPpI10P<4y}ZYY-BM ze{1fGuIX*i&V$Bg+R7|_VNwONZ*@m1JpsFIzW$q1Z?pLKa*F~9UoGl}bqbt$c5lCE zL3LHP`#zuSw&a)9FR>EmPR(?l%*k09!C0SrZbhlzI)*({N*mjwOE~IRwzF735Q;5I zLM9PY+Qw{)*5>~|C0utrRsa9Lwv4Wk6`6%Dvb$N`8=+73u5gWzz4y5GCW-7-wnEA( z*Ua9tOE$R(ab0yUF86o({?6n4dB*#^&+DA$^Zj}~pD({UI2-g62iE6a9wPEu-AwhP z&G(q>e!ML*?9fUYlh%j_gZMLQqW6b=B-@9rWF8N#83@u3USk?$(57~v1=-R4Z`a4$ zloA9q03qgOsOXr{w0e1E^01~*iS)F3X~g)h96rmO@Vndl?8NEU_jY5sd)M#lP{Q+Q zW8|2bBDkkN%E4lu1u)y`B-{uT+CHY>hSF#{_uBN4Y( zeTkKPyErwzDOn}n5-PNA#oJrC-MFiw`Z1lcKRx?KCBD1)QCkx8ayZri^5l#BU`;}?=o zdC4Pi&tAm9tyiYEh)kP_T^mY=7)cs>$l_HHACf4&s`3OQk>gPZma5|8rMz2bHKNBM zZiU2tVa`HIvt%w7yeT~&}nn3P-CcgNK(gvtK$P1)QnieB6 z1)p~9`EI@|CvI6w1A<6$x;DSY319E{5Bz})PF+z;f;$2U4$svk9g7ua7u1fC-(do(T9U~R z4`Du$7Y}(M=BjY2t`i05v1RlYR-}kl{Hixi#0^R&3tAaU?x?F|v;v&WnGj$U1$5-a zYVd5vDge8rO84K46br_vuE&b-ZpFu%to$I!Fp7`U<{{)evR!^+G>5?oY)UxXEMTDZ zB8vdr4~JHgU#LOW!v(RkZZSxe@b!rGZgDsjyfRYgv`%9;1WWl9`CIkU`(qVI|8s;6 z9@Z7Wm_^lcN9HOXB_f^jSesVC6|xIVBzcEJIT8OlIX!P z$$@j-TP!#qQ123vHzYiVPt5YY3P^ZaDs9{)U!?!=`-^SpQgGW95Ow52ayHg-w|Ill z?-V5G_gA^&_7nXDMcVy)yt2waJ&>KplB_0cK02`dd= z?*53iSy2d)IZwtL#0VLx(2DfuAzdenz@o>bn-5ivgjXNxb+l$*PC>qrM^pxM_6D$RlAu*}RgJf0_h*8X zKs=|v#5!C)`tDB6kVpK_vKR|8PFFujy~R-GuYyWF==eRAwm(TLUQaA#k=CSFo|mAn#Yt8;i={EK>@#-=wQiRon_*H>p>;Kl_-3s~ZI z@T(tuBt$juD{0%uItd)<`yJg7n>0Y4|1tZudK=nH@Q#|O_S}5_Zr1ISoGAuP#?Ouk z*!vfn8rol0<`Ig9Fz_oh{@p9_l8+?Mj%0&j4V@CjcRj+|?L{7R?8ePk$w}GpDU9JKt_f{_u5H(xPT4Ziz3_-m=Vhr0p@T>G)(K6cM4p!y#qdn(W^6W(t;@}WZv zMBcwWfiFzTJJz%XIC1B@QDDtS4jL@!6fT>3ny5OqnCtwbf70rDO9c!nV{b;C+Mam{ zl-I}IS2WItI$Ycf^unFyqb?!iRF>~^e|{r$A{Jd0J53;|dEShxJmUVKF6i0(o7w#( zeh^9(M0QPr67s3ahq|WOV|fsWUZ-pe*T}fMX+6&Acs>?!b98-k&gwyW(T~0Nz_1>d zVFDfl#zGTN7qI~pAxGk(>3S;Vk8Acd&9Ivp>`4fBfAuK5t_lrrn2~1o#^J4k-G+YA-09X*TdE@*IL8vlv? za&^^{{M`fPD%a|K@Brk?cQ3``D!U5k`n36B#{du%MbU9wsUgjdsAsA9MmLx_8J{)2 ztEhc_yTMKA@nd?;d((QMX{3AR?1I~gj{;bnnm+r_7K z(zgfFF6?rr0@^g1(30V(?NIHE-iOZf%8|c%1{q@`ZF++lN2q-0F2Awg?#iYfnCTYx zLAlP0ginh*0ZYpa%_qo{Y(J+2*V^lLE7kOQ{vCN@1 zqsk=zZszN(4O%k4M<7}t>0p%J&3BcyAn6D9`#t)jb}FoB=jE~1UgquM$Z{%)_U z)Wr-sd*)N~bWcoRj;9oQO{LAo|8Vm- zDbw=yz5)9uz>@h4wn;h~A4qju`ntS3&w6R;?b`>Uva)kth_33YG!)lZzR!;Xz_LSoceQ=wcSo2t+X2Z^3r#dle8%!2^HH6uTpc8{5Pd^*^nKFaE1<_H6o@~ zFs@67UJaHXab@5|QFwaAvQ)kdeNq)-1~dC4MaewL%TY;Mcx?EQJpY<8flgI~=@BQu zN~=nK&FlU35o%Y&IL-uCiHgM@PcH(#Z<+E&C!POl&GmiCHplXemNjfe=Sr9hZ&c)- zb|79h$Rh3KIssV_YZ@;vtFOu6k%j-Q_hsgT$YpAV^)^oXqns&>X&n>u=@us3dDoVszgzbr=dE~?>9iz&jRw%OBX+~9xnB`3vg}7r ze|JZTfOURU#L9_-sS6z@TI!zak9+`3%d4~eCO?gT-rxQ{u%yOXBh=sXCr_@F`dz%+ z>l|xsq?f{*vz{(Pjs#rcYL#@M(0C^j#}53K7;dP1x9xY_0?tgIWsM2GYm7#qF81xD zzeL-GRySb!2=de*H(ZkVc?W7$5nLjcBdMRJ7TYu;iD{#vuMcgq$!i+%wE!O_wN@x5 z)~cwLG_LZWWq+enONllwbI9%HP`6fFdZehp)=@ueZ&Zl|fNV3%E$gE6jnO)uM_1(w z7fCyJl_o;8_7hIxe7tk)%n4@M9ZCkfd%KciRpN>MTNGJTs!-$3OEzEu7bSkP#RzIf ztKGT*IkUut#X3LzyxuX%&p_#XFLJDpacj;Z)T+8-==O<6J}D{YTx)o=fk$ZWOMsQ4 z*YVBv%kPUPU4JzlyI+^fxgWF5HiSqzFMX}G4+mIJ$R9&JMDPWjY$ws28LGsw@#P0a zNzk)qd#ewOM}|^XU1>UxCf3`il0Q6IVfqv~&h{-a4Xi+d@{4mL1Gk^c+4kIYE-ch+ z%DZWJP$40DtNzuUHjH90?jV35kQL>@)k7!Eo4`Wp9yz8{VssC)ab6R=KUDc|1+?}D zTBZA$J?r^LPlsYP_V`VLv^#q6aDo*Sxa{1k<1XUvDH5AgNV>rURmx7;b_PfEAIONG z7tP$8k`B}8_TL{!bDJK&jMj9f!cnI)zA@ETg%vCIfm5Bh!cX61!G8iIdx_HBTb2X* zuBKpP{N*0m{mY-Pr1>UG)LG^{rt*SM_#R3gd?wkNW(t&kz|punVDLA$jK%-dBycbN zx3s29CyhF56Lj}4v{N<<0a?r|_I;8(8ID8~>IA58ltj)drx5)Qd>7Ig!h$&Zft&6aseGw2)*or#;OV4(YudxP1tQm2>tsQP`aCLm;9j2 zCmYiJ&-F#*ivU79uBZKJpI+igFwLrbnRQVFABE+soY4SAcEl9QYc_6VZ#uf;@~;vy zscQH*#58%OJ@keGcyyQ!RCba>nc5ucO)zpUm>u_aL)4`o?rm`*D%W$0xfhd{Mh~e# z%Ky8A%a7?#l0O?O_|z{LB5R(F?VDv{hyA1B7p4b0mj_A&r=B|lFZrOt)IqNlBD<AZ!Fi@ep8VX+FM_69k6sC%CUcc!-rIU+_D_RG3T++&thk> z4ua$7Cv|jV5BJKn>hm72#-wuA0MpolL+te}s2U6e}?{kGb){YJn*7OOZ&YCcUGpCAo&p=9#r@x-KgsFT0Ig{!o3-kq*2NhCg?;@- zH%7RK`5wfY!M*Aif8p+O@7}Zysul$SQa7QXUVYSt-v*9n6;%;>%pG2F2bz7PM4;VynSwu88H_0t8&ovDA}^(6aKk6rH%PBSGE>L~ry` z#x+*2XcG50;-%bBV_wkz1a9k;@ z*j|6+ww)vg15{|^8;oVF9*%BOl*sLF)|Y{kaboT{zxa`9>q%&@d(%xG>Un&}Wcp+q zjODBzo{J@ul!mPv4|oz=(`#N6+8+u6A)VAxU0%qBeKM6eP+$hMM&!p*&m=-ox8Rwb zL2z|J-r4jd?hi6aRz77Lew>Mc=0CV#z?@&6z&PZHlHpTkp}pMWy2`Ywsu3{C53_TdFoW* zNkKO7SztoG{B^9)fEwzhAe)D*mJ(#6!CHjZe4TEE#7YPB>9mA-j5h7`##x*>_+4#B zNk!k;T^#mK$#jJyQM$_a>_F{d<`oU?VGcf8cRf9yW;GoA>C4T6kB+!HxPHTi=4Uco zBGeNb4}KV*oz%IWIfd0>ac_64XWRL^ZLbv)+Tm5D9u$6ZPiL87aaz0sR`GRwS$3`= z9!|ndj?2BDRIXzc6OGqT&e)uggHoN~h{0Y8^U4wG#%3N7Ha1WUdzy!fYq7k{ZAyme zHN)NNF;#N!@3b>=TKz2zl<#YH$$S=ezrL-Ns94_^*zxUyZ{r3p2zzLTyY*mxEk`bH z98NL8^xx&(kU%5EYp)}#JL3A?3g%->EvLrk|JLo~61=|p98vqTu2j0jQ*O?;(@(2K z?U~xR&RIhd1w1U7A(%?;`fT=_&&W+{+!iY|($ZGWvb@|x%Poy+<~}l)Cl$9Ob*9x% zNH+_duL&EL%Sy8Umh+g3`K~R!N81jCjUVtBQ8&vVLZ1I8EU%R^P9(0B8YB!^_AbsQ z2NEBO@qrA0s&F;#y8PYdu+F;!pT>UkpccH9gyCl=} zubU#lkDll2m&Ww>U#`{<-R-~?(|nvw`p?0?!CG5Xxy-@B?r(>bG&B=Z7zA`fb^SgC ze)p1ax6;SrduJ0C?~*Lj8wPFOKU60o-?2ek@&nDkCJyL)00N(G7Zee`u^?D|d)ep9 zLYoRya@o6JMj`N9o6|_+wM~Lo*fXw3lbo3)0{5De=;+I0f|6`)ZbO*yL{|SQ7?Evz z#n$9dTz;El5{GlsH=2rme%{uSJt5?o@n8ux8APsJn~^{L3!Zg?suDsLEGyiRkPSf& zy6u!CS%rV-O+64^K_Yse3un=hcg4dAuOl5R>&>?G`9Ny%ef*I}vZC4dOxO zGTquFsIHFm|9b6SWrrX42|5_lREoze8e>D&&opKY z3|_X_^aheC*h%i3VsS@Fu0qLc=Qx*y_|}H@gqA`MY+z!!>gXW_e<;>E>uh($;;X|W z7J26Oo3OZKzW1zPIun_4lE2^G!kH50Fz?FTOZ;J`Bjs|}xI{{)GzT_d3aFwI+Ma6p z41%z0aW7X~I`C9FC*NZDLEqJ>L8Fq|+<6bjRJ2!yo_iKgjO%bzcs#`G$bY8Mc09(X zRJK+FgUB_*K1pn9C53@vs!JZI3!&LHzBF2iona9())UF_K4lQ_6%|Ey-sy{06;Y^$ z1{ia2RiGXUe@+9k-s|AjR507#GEI|=rR* z1(pR5W2d|r4mh@#EDQ=^Ymsso#Gb`fpOG}z{IA~O<+qTH&u8@>An$(`V&ArQmvSTS z{aB+_6#ObpLE|L^$JwSCi>47K!y-cSAwl84T*4Hy(9CfVBIli;tIDx}_UT*UR>qHK zFB4+}3j^6aiamQ5?<1nnUr4Dv0Vnp7N;pKNGVBmt9c$l~?X%Mxtqy_C6c93gSr%}9 zy77(7FT~)SQft!NhHsy~ir+*tE$tMf%v%6cT#%A7JdWDOuy0nqJ{+>j21QaQ*wC&1 z-r`}*gGytVJX2fmbf~akgK5oBZUDmgq58u33<={MC!b^wpZ>W{HRJuE;5~X2c?T|E zQ8;@Mv<9a|sfmrQT)a0~I7y>dYbh4$zvN^eJP2=_2)Kk!H zbM8QV@l>IpH!U)dX=vc(`#X|)BohIXY;;s-5Yu9H+Msar#G~>FGY2k}1CD?fGeWY{ zDdH51UvgW2svM!;HqIU^_LTAdd&uoXo9=#{ z{u#97VMe$TX2OWIPKj-OS`$rB=~qQbjF+_*3qg>=`hI=UvRNArHFO(zCN1n&SVq#< z?d`r|)~X*H-PgfhpG~N-E-CSWmc>r$${4&MA^)@{c!SJ>d80q2oDgCX6If`bCBQq|vDCNs@slX#c4u~vWRKY#8`#%sYs1fEp*QU5+dk4Yn?I~jxX!iujA7A5 zy+4IRJ(AOAbL>zJKlV=iu;}j=h{5G56(ebLg4-DEHDhd#4S6pfWwyrkyEHJ91h)Og V`gRq~qyPf8|I~FK)~Q-Y{U4=vt$qLi literal 0 HcmV?d00001 From 090191cb6abec00a979cd3ea0d908dce8ff2a1a2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:02 +0800 Subject: [PATCH 0857/1461] Declare the 1.21.11 pack formats --- minecraft-1.21.11/src/main/resources/pack.mcmeta | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 minecraft-1.21.11/src/main/resources/pack.mcmeta diff --git a/minecraft-1.21.11/src/main/resources/pack.mcmeta b/minecraft-1.21.11/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..d0514452 --- /dev/null +++ b/minecraft-1.21.11/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "${mod_name} resources", + "min_format": 75, + "max_format": 94 + } +} From 69abe83d23944ca6f45200d099b069ea574dc19a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:02 +0800 Subject: [PATCH 0858/1461] Add ForgeClient.java to the Forge 1.21.11 glue --- .../jnhprojects/mcheli/forge/ForgeClient.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java diff --git a/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java new file mode 100644 index 00000000..fb861c5c --- /dev/null +++ b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -0,0 +1,117 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.client.CameraAngles; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.client.event.EntityRenderersEvent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.client.event.RegisterParticleProvidersEvent; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.client.event.RenderLivingEvent; +import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; + +/** Client events on Forge. Only loaded on a physical client. */ +final class ForgeClient { + private ForgeClient() { + } + + static void init(BusGroup modBus) { + ClientHooks.init(effect -> Minecraft.getInstance().gameRenderer.setPostEffect(effect)); + FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); + EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + RegisterParticleProvidersEvent.BUS.addListener(event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + RegisterKeyMappingsEvent.BUS.addListener(event -> ClientHooks.keyMappings().forEach(event::register)); + RegisterClientReloadListenersEvent.BUS.addListener(event -> ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); + AddGuiOverlayLayersEvent.BUS.addListener(event -> event.getLayeredDraw() + .add(ForgeLayeredDraw.POST_SLEEP_STACK, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + TickEvent.ClientTickEvent.Post.BUS.addListener(event -> ClientHooks.clientTickEnd()); + ViewportEvent.ComputeCameraAngles.BUS.addListener(event -> ClientHooks.computeCameraAngles(new Angles(event))); + ViewportEvent.ComputeFov.BUS.addListener(event -> event.setFOV(ClientHooks.fov(event.getFOV()))); + RenderHandEvent.BUS.addListener(ForgeClient::hideHand); + RenderLivingEvent.Pre.BUS.addListener(event -> { + ClientHooks.beforeRenderLiving(event.getState(), event.getPoseStack(), ClientHooks.cameraPartialTicks()); + return false; + }); + InputEvent.InteractionKeyMappingTriggered.BUS.addListener(ForgeClient::onInteractionKey); + ClientPlayerNetworkEvent.LoggingIn.BUS.addListener(event -> ClientHooks.loggedIn(event.getPlayer())); + ClientPlayerNetworkEvent.LoggingOut.BUS.addListener(event -> ClientHooks.loggedOut()); + } + + private static boolean hideHand(RenderHandEvent event) { + return ClientHooks.hidesHand(); + } + + private static boolean onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + return switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> false; + case CONSUME -> { + event.setSwingHand(false); + yield true; + } + case CONSUME_AND_SWING -> true; + }; + } + return event.isPickBlock() && ClientHooks.onPickBlockKey(); + } + + private record Angles(ViewportEvent.ComputeCameraAngles event) implements CameraAngles { + @Override + public Camera camera() { + return event.getCamera(); + } + + @Override + public float partialTick() { + return event.getPartialTick(); + } + + @Override + public float yaw() { + return event.getYaw(); + } + + @Override + public float pitch() { + return event.getPitch(); + } + + @Override + public float roll() { + return event.getRoll(); + } + + @Override + public void setYaw(float yaw) { + event.setYaw(yaw); + } + + @Override + public void setPitch(float pitch) { + event.setPitch(pitch); + } + + @Override + public void setRoll(float roll) { + event.setRoll(roll); + } + } +} From 1bb2d0ce106c31c2fe8b469cba9afa2387102fa4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:03 +0800 Subject: [PATCH 0859/1461] Add ForgeNetwork.java to the Forge 1.21.11 glue --- .../mcheli/forge/ForgeNetwork.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java diff --git a/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java new file mode 100644 index 00000000..54a48e84 --- /dev/null +++ b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraftforge.network.Channel; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.payload.PayloadFlow; + +/** + * Registers the shared packets on a Forge payload channel. Each packet travels under its own id, + * as on NeoForge and Fabric; the channel only adds Forge's version check. + */ +final class ForgeNetwork { + private static Channel channel; + + private ForgeNetwork() { + } + + static void register() { + PayloadFlow flow = ChannelBuilder + .named(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "main")) + .networkProtocolVersion(ModNetwork.PROTOCOL_VERSION) + .payloadChannel() + .play() + .serverbound(); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + flow = serverbound(flow, packet); + } + flow = flow.clientbound(); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + flow = clientbound(flow, packet); + } + channel = flow.build(); + } + + static Channel channel() { + return channel; + } + + private static PayloadFlow serverbound( + PayloadFlow flow, ModNetwork.Serverbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg, ctx.getSender())); + ctx.setPacketHandled(true); + }); + } + + private static PayloadFlow clientbound( + PayloadFlow flow, ModNetwork.Clientbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg)); + ctx.setPacketHandled(true); + }); + } +} From 83cae969b4d40491b7ec110a06a89a3ccb76ae59 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:03 +0800 Subject: [PATCH 0860/1461] Add the Forge platform for 1.21.11, with its older ModList call --- .../mcheli/forge/ForgePlatform.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java diff --git a/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java new file mode 100644 index 00000000..584079ac --- /dev/null +++ b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java @@ -0,0 +1,70 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.network.PacketDistributor; + +import java.nio.file.Path; +import java.util.Set; +import java.util.function.BiFunction; + +final class ForgePlatform implements Platform { + @Override + public String loaderName() { + return "forge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.SERVER.noArg()); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.PLAYER.with(player)); + } + + @Override + public BlockEntityType blockEntityType(BiFunction factory, Block block) { + return new BlockEntityType<>(factory::apply, Set.of(block)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From af341f9b82e160a5a6ce7852af002d5102869506 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:04 +0800 Subject: [PATCH 0861/1461] Add ForgeVehicleIngredient.java to the Forge 1.21.11 glue --- .../mcheli/forge/ForgeVehicleIngredient.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java diff --git a/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java new file mode 100644 index 00000000..51a8d1a9 --- /dev/null +++ b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraftforge.common.crafting.ingredients.AbstractIngredient; +import net.minecraftforge.common.crafting.ingredients.IIngredientSerializer; +import org.jspecify.annotations.Nullable; + +/** {@link VehicleIngredient} as a Forge ingredient, read from the {@code type} key. */ +final class ForgeVehicleIngredient extends AbstractIngredient { + static final MapCodec CODEC = VehicleIngredient.codec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final StreamCodec STREAM_CODEC = + VehicleIngredient.streamCodec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final IIngredientSerializer SERIALIZER = new IIngredientSerializer<>() { + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + public void write(RegistryFriendlyByteBuf buffer, ForgeVehicleIngredient value) { + STREAM_CODEC.encode(buffer, value); + } + + @Override + public ForgeVehicleIngredient read(RegistryFriendlyByteBuf buffer) { + return STREAM_CODEC.decode(buffer); + } + }; + + private final String vehicle; + + private ForgeVehicleIngredient(String vehicle) { + super(VehicleIngredient.kindItems()); + this.vehicle = vehicle; + } + + String vehicle() { + return vehicle; + } + + @Override + public boolean test(@Nullable ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IIngredientSerializer serializer() { + return SERIALIZER; + } + + @Override + public boolean equals(Object o) { + return o instanceof ForgeVehicleIngredient other && vehicle.equals(other.vehicle); + } + + @Override + public int hashCode() { + return vehicle.hashCode(); + } +} From 6c8b61be86ef6bb73dfeb146512a7766660313e6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:04 +0800 Subject: [PATCH 0862/1461] Replace the Forge 1.21.11 placeholder entry point with the real one --- .../jnhprojects/mcheli/forge/McheliForge.java | 71 ++++++++++++++++--- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java index 390995f9..4028f536 100644 --- a/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java +++ b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java @@ -1,19 +1,70 @@ package com.jnhprojects.mcheli.forge; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; import net.minecraftforge.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegisterEvent; -/** - * Placeholder entry point for Minecraft 1.21.11. It only proves that the module builds and loads; - * the Forge glue from forge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +/** The Forge entry point: everything here only connects Forge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliForge(FMLJavaModLoadingContext context) { + BusGroup modBus = context.getModBusGroup(); + Platform.install(new ForgePlatform()); + MCHeli.init(); - public McheliForge() { - LOGGER.info("MCHeli Rework placeholder loaded on Forge for Minecraft 1.21.11; no content yet"); + RegisterEvent.getBus(modBus).addListener(event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(ForgeRegistries.Keys.INGREDIENT_SERIALIZERS, + helper -> helper.register(VehicleIngredient.ID, ForgeVehicleIngredient.SERIALIZER)); + }); + ForgeNetwork.register(); + FMLCommonSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(CommonHooks::commonSetup)); + + AddPackFindersEvent.BUS.addListener(event -> event.addRepositorySource(CommonHooks.packSource())); + ServerAboutToStartEvent.BUS.addListener(event -> CommonHooks.serverAboutToStart(event.getServer())); + ServerStoppedEvent.BUS.addListener(event -> CommonHooks.serverStopped()); + TickEvent.ServerTickEvent.Post.BUS.addListener(event -> CommonHooks.serverTickEnd(event.server())); + RegisterCommandsEvent.BUS.addListener(event -> CommonHooks.registerCommands(event.getDispatcher())); + PlayerEvent.PlayerLoggedInEvent.BUS.addListener(event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + PlayerEvent.PlayerLoggedOutEvent.BUS.addListener(event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + ForgeClient.init(modBus); + } + } + + /** Only once Forge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } } } From e5aa6922f9dbdad7971d5ce5500c0c06e337f1b6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:05 +0800 Subject: [PATCH 0863/1461] Open the post effect setter on Forge 1.21.11 for the thermal view --- forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg diff --git a/forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg b/forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 00000000..8fa0e652 --- /dev/null +++ b/forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,2 @@ +# Vanilla keeps the post effect setter private on 1.21.11; the thermal view needs it. +public net.minecraft.client.renderer.GameRenderer setPostEffect(Lnet/minecraft/resources/Identifier;)V From ebed56270e8e38920599b1570cdf80fd4a2d4157 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:05 +0800 Subject: [PATCH 0864/1461] Drop the placeholder wording from the Forge 1.21.11 metadata --- forge-1.21.11/src/main/resources/META-INF/mods.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/forge-1.21.11/src/main/resources/META-INF/mods.toml b/forge-1.21.11/src/main/resources/META-INF/mods.toml index 47ec16fb..1fdc3b48 100644 --- a/forge-1.21.11/src/main/resources/META-INF/mods.toml +++ b/forge-1.21.11/src/main/resources/META-INF/mods.toml @@ -13,7 +13,6 @@ authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft Forge. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. From 6e490f34826ddb63134e3ccde9ab267b124f7cca Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:06 +0800 Subject: [PATCH 0865/1461] Define c:feathers on Forge 1.21.11, which only has forge:feathers --- .../src/main/resources/data/c/tags/item/feathers.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 forge-1.21.11/src/main/resources/data/c/tags/item/feathers.json diff --git a/forge-1.21.11/src/main/resources/data/c/tags/item/feathers.json b/forge-1.21.11/src/main/resources/data/c/tags/item/feathers.json new file mode 100644 index 00000000..4554f87e --- /dev/null +++ b/forge-1.21.11/src/main/resources/data/c/tags/item/feathers.json @@ -0,0 +1,5 @@ +{ + "values": [ + "#forge:feathers" + ] +} From 833b13c9f1c0341d53b4235cd2532420af06f208 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:06 +0800 Subject: [PATCH 0866/1461] Define c:gunpowders on Forge 1.21.11, which only has forge:gunpowder --- .../src/main/resources/data/c/tags/item/gunpowders.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 forge-1.21.11/src/main/resources/data/c/tags/item/gunpowders.json diff --git a/forge-1.21.11/src/main/resources/data/c/tags/item/gunpowders.json b/forge-1.21.11/src/main/resources/data/c/tags/item/gunpowders.json new file mode 100644 index 00000000..16c21708 --- /dev/null +++ b/forge-1.21.11/src/main/resources/data/c/tags/item/gunpowders.json @@ -0,0 +1,5 @@ +{ + "values": [ + "#forge:gunpowder" + ] +} From f8af6aaf4605a43a4003b3527bb531c4f3200318 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:06 +0800 Subject: [PATCH 0867/1461] Set up the Forge 1.21.11 runs with the official mappings and its access transformer --- forge-1.21.11/build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/forge-1.21.11/build.gradle b/forge-1.21.11/build.gradle index 51b53eb9..4c2e90a6 100644 --- a/forge-1.21.11/build.gradle +++ b/forge-1.21.11/build.gradle @@ -7,12 +7,19 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + minecraft { mappings channel: 'official', version: minecraft_version + useDefaultAccessTransformer() runs { configureEach { workingDir = rootProject.layout.projectDirectory.dir("run-${minecraft_version}") + systemProperty 'eventbus.api.strictRuntimeChecks', 'true' + systemProperty 'mcheli.legacyZip', legacyZip } register('client') @@ -32,4 +39,5 @@ repositories { dependencies { implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}") + annotationProcessor "net.minecraftforge:eventbus-validator:${eventbus_version}" } From 5eaf82aac719db2618aa87639b8475051937e96d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:07 +0800 Subject: [PATCH 0868/1461] Stop treating Forge 1.21.11 as a placeholder --- forge-1.21.11/gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-1.21.11/gradle.properties b/forge-1.21.11/gradle.properties index f5af7bee..c0485578 100644 --- a/forge-1.21.11/gradle.properties +++ b/forge-1.21.11/gradle.properties @@ -1,8 +1,8 @@ -# Minecraft 1.21.11 on Forge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 1.21.11 on Forge loader=forge minecraft_version=1.21.11 minecraft_version_range=[1.21.11,1.21.12) forge_version=61.2.4 loader_version_range=[61,) +eventbus_version=7.0.6 java_version=21 -scaffold=true From af8c476575fb8651a5e3a03de0df8d79c3228b49 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:07 +0800 Subject: [PATCH 0869/1461] Replace the NeoForge 1.21.11 placeholder entry point with the real one --- .../mcheli/neoforge/McheliNeoForge.java | 96 ++++++++++++++++--- 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java index 8244fd29..b5bd9c45 100644 --- a/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java +++ b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java @@ -1,19 +1,93 @@ package com.jnhprojects.mcheli.neoforge; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Placeholder entry point for Minecraft 1.21.11. It only proves that the module builds and loads; - * the NeoForge glue from neoforge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.AddPackFindersEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import net.neoforged.neoforge.registries.RegisterEvent; + +/** The NeoForge entry point: everything here only connects NeoForge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliNeoForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliNeoForge(IEventBus modBus) { + Platform.install(new NeoForgePlatform()); + MCHeli.init(); + + modBus.addListener(RegisterEvent.class, event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(NeoForgeRegistries.Keys.INGREDIENT_TYPES, + helper -> helper.register(VehicleIngredient.ID, NeoForgeVehicleIngredient.TYPE)); + }); + modBus.addListener(RegisterPayloadHandlersEvent.class, McheliNeoForge::registerPackets); + modBus.addListener(FMLCommonSetupEvent.class, event -> event.enqueueWork(CommonHooks::commonSetup)); + modBus.addListener(AddPackFindersEvent.class, event -> event.addRepositorySource(CommonHooks.packSource())); + + NeoForge.EVENT_BUS.addListener(ServerAboutToStartEvent.class, event -> CommonHooks.serverAboutToStart(event.getServer())); + NeoForge.EVENT_BUS.addListener(ServerStoppedEvent.class, event -> CommonHooks.serverStopped()); + NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> CommonHooks.serverTickEnd(event.getServer())); + NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> CommonHooks.registerCommands(event.getDispatcher())); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedInEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedOutEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + NeoForgeClient.init(modBus); + } + } + + /** Only once NeoForge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } + } + + /** Handlers run on the main thread, which is NeoForge's default. */ + private static void registerPackets(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(Integer.toString(ModNetwork.PROTOCOL_VERSION)); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(registrar, packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(registrar, packet); + } + } + + private static void serverbound(PayloadRegistrar registrar, ModNetwork.Serverbound packet) { + registrar.playToServer(packet.type(), packet.codec(), + (msg, context) -> packet.handler().accept(msg, (ServerPlayer) context.player())); + } - public McheliNeoForge() { - LOGGER.info("MCHeli Rework placeholder loaded on NeoForge for Minecraft 1.21.11; no content yet"); + private static void clientbound(PayloadRegistrar registrar, ModNetwork.Clientbound packet) { + registrar.playToClient(packet.type(), packet.codec(), (msg, context) -> packet.handler().accept(msg)); } } From bfeb89c8f2d0a13c5efc35b076b44f81a43efb33 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:08 +0800 Subject: [PATCH 0870/1461] Add NeoForgeClient.java to the NeoForge 1.21.11 glue --- .../mcheli/neoforge/NeoForgeClient.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java diff --git a/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java new file mode 100644 index 00000000..9c8d7d61 --- /dev/null +++ b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.neoforge.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Minecraft; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; +import net.neoforged.neoforge.client.event.InputEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; +import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent; +import net.neoforged.neoforge.client.event.RenderHandEvent; +import net.neoforged.neoforge.client.event.RenderLivingEvent; +import net.neoforged.neoforge.client.event.ViewportEvent; +import net.neoforged.neoforge.client.gui.VanillaGuiLayers; +import net.neoforged.neoforge.common.NeoForge; + +/** + * Client events on NeoForge. Only loaded on a physical client. The camera hook is a mixin + * ({@code GameRendererMixin}): NeoForge's camera event fires before vanilla positions the camera, + * so a cockpit eye set from it would be overwritten. + */ +final class NeoForgeClient { + private NeoForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$setPostEffect(effect)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + modBus.addListener(RegisterMenuScreensEvent.class, event -> ClientHooks.registerMenuScreens(event::register)); + modBus.addListener(RegisterParticleProvidersEvent.class, event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + modBus.addListener(RegisterKeyMappingsEvent.class, event -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener(AddClientReloadListenersEvent.class, event -> ClientHooks.reloadListeners().forEach(event::addListener)); + modBus.addListener(RegisterGuiLayersEvent.class, + event -> event.registerAbove(VanillaGuiLayers.SLEEP_OVERLAY, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post.class, event -> ClientHooks.clientTickEnd()); + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeFov.class, event -> event.setFOV(ClientHooks.fov(event.getFOV()))); + NeoForge.EVENT_BUS.addListener(RenderHandEvent.class, event -> event.setCanceled(ClientHooks.hidesHand())); + NeoForge.EVENT_BUS.addListener(RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getRenderState(), event.getPoseStack(), event.getPartialTick())); + NeoForge.EVENT_BUS.addListener(InputEvent.InteractionKeyMappingTriggered.class, NeoForgeClient::onInteractionKey); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingIn.class, event -> ClientHooks.loggedIn(event.getPlayer())); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingOut.class, event -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } +} From fc90f5d7edfb16b03e14e39170fc7f22f41299f1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:08 +0800 Subject: [PATCH 0871/1461] Add NeoForgePlatform.java to the NeoForge 1.21.11 glue --- .../mcheli/neoforge/NeoForgePlatform.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java diff --git a/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java new file mode 100644 index 00000000..920adbdb --- /dev/null +++ b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; +import net.neoforged.neoforge.network.PacketDistributor; + +import java.nio.file.Path; +import java.util.Set; +import java.util.function.BiFunction; + +final class NeoForgePlatform implements Platform { + @Override + public String loaderName() { + return "neoforge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.getDist() == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPacketDistributor.sendToServer(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + + @Override + public BlockEntityType blockEntityType(BiFunction factory, Block block) { + return new BlockEntityType<>(factory::apply, Set.of(block)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From deaf2a5f2eb60d16bd10e1e43a99e033b631d57e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:09 +0800 Subject: [PATCH 0872/1461] Add NeoForgeVehicleIngredient.java to the NeoForge 1.21.11 glue --- .../neoforge/NeoForgeVehicleIngredient.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java diff --git a/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java new file mode 100644 index 00000000..195be00a --- /dev/null +++ b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import net.minecraft.core.Holder; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.neoforged.neoforge.common.crafting.ICustomIngredient; +import net.neoforged.neoforge.common.crafting.IngredientType; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a NeoForge ingredient, read from the {@code neoforge:ingredient_type} key. */ +record NeoForgeVehicleIngredient(String vehicle) implements ICustomIngredient { + static final IngredientType TYPE = new IngredientType<>( + VehicleIngredient.codec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle), + VehicleIngredient.streamCodec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle)); + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> items() { + return VehicleIngredient.items(); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IngredientType getType() { + return TYPE; + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } +} From 0ad54d46c03ffbcf9e35b00436385892cb97e346 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:09 +0800 Subject: [PATCH 0873/1461] Add CameraInvoker.java to the NeoForge 1.21.11 glue --- .../mcheli/neoforge/mixin/CameraInvoker.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java diff --git a/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java new file mode 100644 index 00000000..4c289d07 --- /dev/null +++ b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java @@ -0,0 +1,12 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** NeoForge adds a roll to {@code Camera#setRotation}, but leaves it protected. */ +@Mixin(Camera.class) +public interface CameraInvoker { + @Invoker("setRotation") + void mcheli$setRotation(float yRot, float xRot, float roll); +} From 0468e2777aa1f14eafeddc1a8b27255deaaa4640 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:10 +0800 Subject: [PATCH 0874/1461] Add GameRendererInvoker.java to the NeoForge 1.21.11 glue --- .../mcheli/neoforge/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java diff --git a/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..635e1a93 --- /dev/null +++ b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#setPostEffect} private on 1.21.11. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("setPostEffect") + void mcheli$setPostEffect(Identifier effect); +} From 97ad444b90a41c8293b46da218dacea0d8535255 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:21 +0800 Subject: [PATCH 0875/1461] Add the NeoForge camera hook for 1.21.11, around updateCamera and extractCamera --- .../neoforge/mixin/GameRendererMixin.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java diff --git a/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java new file mode 100644 index 00000000..5537f2d6 --- /dev/null +++ b/neoforge-1.21.11/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java @@ -0,0 +1,39 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, at the two places Forge fires its camera angle event: once vanilla has placed + * the camera for the frame, and again before the camera is copied into the render state. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "updateCamera", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setup(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/Entity;ZZF)V", + shift = At.Shift.AFTER)) + private void mcheli$afterCameraUpdate(DeltaTracker deltaTracker, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, deltaTracker.getGameTimeDeltaPartialTick(true), GameRendererMixin::setRotation); + } + + @Inject(method = "extractCamera", at = @At("HEAD")) + private void mcheli$beforeExtractCamera(float worldPartialTicks, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, worldPartialTicks, GameRendererMixin::setRotation); + } + + /** NeoForge turns the camera the other way round its roll axis than Forge does. */ + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((CameraInvoker) camera).mcheli$setRotation(yaw, pitch, -roll); + } +} From 3bf0d799fe1b83cf0788bba9eba84bcfcacf742e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:21 +0800 Subject: [PATCH 0876/1461] Drop the placeholder wording from the NeoForge 1.21.11 metadata --- .../src/main/resources/META-INF/neoforge.mods.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/neoforge-1.21.11/src/main/resources/META-INF/neoforge.mods.toml b/neoforge-1.21.11/src/main/resources/META-INF/neoforge.mods.toml index 2585f0b9..c8095914 100644 --- a/neoforge-1.21.11/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge-1.21.11/src/main/resources/META-INF/neoforge.mods.toml @@ -8,17 +8,19 @@ modId="${mod_id}" version="${mod_version}" displayName="${mod_name}" displayURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge" -logoFile="mcheli_logo.png" +iconFile="mcheli_logo.png" authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. ''' +[[mixins]] +config="mcheli.mixins.json" + [[dependencies.${mod_id}]] modId="neoforge" type="required" From 284ab2abc1aa084b8b1033bf91b8f56b188e9da0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:22 +0800 Subject: [PATCH 0877/1461] List the NeoForge 1.21.11 mixins --- .../src/main/resources/mcheli.mixins.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 neoforge-1.21.11/src/main/resources/mcheli.mixins.json diff --git a/neoforge-1.21.11/src/main/resources/mcheli.mixins.json b/neoforge-1.21.11/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..77522ff3 --- /dev/null +++ b/neoforge-1.21.11/src/main/resources/mcheli.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.neoforge.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "CameraInvoker", + "GameRendererInvoker", + "GameRendererMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From 7e50599449530501724940252f0130115722a869 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:22 +0800 Subject: [PATCH 0878/1461] Set up the NeoForge 1.21.11 runs --- neoforge-1.21.11/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neoforge-1.21.11/build.gradle b/neoforge-1.21.11/build.gradle index 6dded114..60829c34 100644 --- a/neoforge-1.21.11/build.gradle +++ b/neoforge-1.21.11/build.gradle @@ -7,12 +7,17 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + neoForge { version = neoforge_version runs { configureEach { gameDirectory = rootProject.file("run-${minecraft_version}") + systemProperty 'mcheli.legacyZip', legacyZip } client { client() From 87b26064aae9fb8f9adacdd59b9d1c5c163f8582 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:23 +0800 Subject: [PATCH 0879/1461] Stop treating NeoForge 1.21.11 as a placeholder and use the jspecify it pins --- neoforge-1.21.11/gradle.properties | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neoforge-1.21.11/gradle.properties b/neoforge-1.21.11/gradle.properties index 54840e89..1b046e04 100644 --- a/neoforge-1.21.11/gradle.properties +++ b/neoforge-1.21.11/gradle.properties @@ -1,8 +1,9 @@ -# Minecraft 1.21.11 on NeoForge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 1.21.11 on NeoForge loader=neoforge minecraft_version=1.21.11 minecraft_version_range=[1.21.11,1.21.12) neoforge_version=21.11.48 loader_version_range=[21.11,) java_version=21 -scaffold=true +# NeoForge 1.21.11 pins this version strictly. +jspecify_version=1.0.0 From 102924e40df9f56f1cf8315cffd3f01387e90e56 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:23 +0800 Subject: [PATCH 0880/1461] Build creative tabs and block entity types with Fabric API on 1.21.11 --- .../mcheli/fabric/FabricPlatform.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java new file mode 100644 index 00000000..707dd713 --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.nio.file.Path; +import java.util.function.BiFunction; + +final class FabricPlatform implements Platform { + @Override + public String loaderName() { + return "fabric"; + } + + @Override + public Path gameDir() { + return FabricLoader.getInstance().getGameDir(); + } + + @Override + public Path configDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + public String modVersion() { + return FabricLoader.getInstance().getModContainer(MCHeli.MOD_ID) + .map(c -> c.getMetadata().getVersion().getFriendlyString()).orElse(""); + } + + @Override + public boolean isClient() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPlayNetworking.send(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ServerPlayNetworking.send(player, payload); + } + + /** Fabric orders tabs by registration, which already puts ours one after another. */ + @Override + public BlockEntityType blockEntityType(BiFunction factory, Block block) { + return FabricBlockEntityTypeBuilder.create(factory::apply, block).build(); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return FabricItemGroup.builder(); + } +} From ad8600b20ca0f290c437d7724f0addad12fd3fe0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:24 +0800 Subject: [PATCH 0881/1461] Add the Fabric vehicle ingredient with the 1.21.11 Fabric API names --- .../fabric/FabricVehicleIngredient.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java new file mode 100644 index 00000000..eeeba6f3 --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Holder; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a Fabric ingredient, read from the {@code fabric:type} key. */ +record FabricVehicleIngredient(String vehicle) implements CustomIngredient { + static final CustomIngredientSerializer SERIALIZER = new CustomIngredientSerializer<>() { + private final MapCodec codec = + VehicleIngredient.codec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + private final StreamCodec streamCodec = + VehicleIngredient.streamCodec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + + @Override + public Identifier getIdentifier() { + return VehicleIngredient.ID; + } + + @Override + public MapCodec getCodec() { + return codec; + } + + @Override + public StreamCodec getPacketCodec() { + return streamCodec; + } + }; + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> getMatchingItems() { + return VehicleIngredient.items(); + } + + @Override + public boolean requiresTesting() { + return true; + } + + @Override + public CustomIngredientSerializer getSerializer() { + return SERIALIZER; + } + + @Override + public SlotDisplay toDisplay() { + return VehicleIngredient.display(vehicle); + } +} From c0d0b7ff1a1d5ed740570d17c59d6933e45a834d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:24 +0800 Subject: [PATCH 0882/1461] Replace the Fabric 1.21.11 placeholder entry point, with the older payload registry names --- .../mcheli/fabric/McheliFabric.java | 69 +++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java index 7c4837c1..9f9e2331 100644 --- a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java @@ -1,19 +1,74 @@ package com.jnhprojects.mcheli.fabric; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; import net.fabricmc.api.ModInitializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; /** - * Placeholder entry point for Minecraft 1.21.11. It only proves that the module builds and loads; - * the Fabric glue from fabric-26.3 replaces it when this version is ported for a final release. + * The Fabric entry point: everything here only connects Fabric to the shared code. The imported + * content pack is added by {@code PackRepositoryMixin}, since Fabric has no pack finder event. */ public final class McheliFabric implements ModInitializer { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); - @Override public void onInitialize() { - LOGGER.info("MCHeli Rework placeholder loaded on Fabric for Minecraft 1.21.11; no content yet"); + Platform.install(new FabricPlatform()); + MCHeli.init(); + + for (ResourceKey> registry : Registration.registries()) { + register(Registration.typed(registry)); + } + CustomIngredientSerializer.register(FabricVehicleIngredient.SERIALIZER); + registerPackets(); + CommonHooks.commonSetup(); + + ServerLifecycleEvents.SERVER_STARTING.register(CommonHooks::serverAboutToStart); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> CommonHooks.serverStopped()); + ServerTickEvents.END_SERVER_TICK.register(CommonHooks::serverTickEnd); + CommandRegistrationCallback.EVENT.register((dispatcher, context, selection) -> CommonHooks.registerCommands(dispatcher)); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> CommonHooks.playerLoggedIn(handler.getPlayer())); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> CommonHooks.playerLoggedOut(handler.getPlayer())); + } + + @SuppressWarnings("unchecked") + private static void register(ResourceKey> key) { + Registry registry = (Registry) BuiltInRegistries.REGISTRY.getValue(key.identifier()); + if (registry == null) { + throw new IllegalStateException("No registry " + key.identifier()); + } + Registration.registerAll(key, (id, value) -> Registry.register(registry, id, value)); + } + + /** Client handlers are registered by {@link McheliFabricClient}; handlers run on the main thread. */ + private static void registerPackets() { + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + } + + private static void clientbound(ModNetwork.Clientbound packet) { + PayloadTypeRegistry.playS2C().register(packet.type(), packet.codec()); + } + + private static void serverbound(ModNetwork.Serverbound packet) { + PayloadTypeRegistry.playC2S().register(packet.type(), packet.codec()); + ServerPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg, context.player())); } } From 887da103a13a3f96864eec1559d0d908df7dcd2e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:25 +0800 Subject: [PATCH 0883/1461] Add the Fabric client glue with the 1.21.11 Fabric API names --- .../mcheli/fabric/McheliFabricClient.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java new file mode 100644 index 00000000..8fd926e0 --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java @@ -0,0 +1,56 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.fabric.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.packs.PackType; + +/** + * Client side of the Fabric entry point. The camera, hand, key and rider hooks are mixins (see + * {@code com.jnhprojects.mcheli.fabric.mixin}), since Fabric API has no events for them. + */ +public final class McheliFabricClient implements ClientModInitializer { + + // EntityRendererRegistry is deprecated in favour of a vanilla method that is not public yet. + @SuppressWarnings("deprecation") + @Override + public void onInitializeClient() { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$setPostEffect(effect)); + ClientHooks.registerEntityRenderers(EntityRendererRegistry::register); + ClientHooks.registerBlockEntityRenderers(BlockEntityRenderers::register); + ClientHooks.registerMenuScreens(MenuScreens::register); + ParticleFactoryRegistry.getInstance().register(ModParticles.GLOW.get(), GlowParticle.Provider::new); + ParticleFactoryRegistry.getInstance().register(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + ClientHooks.keyMappings().forEach(KeyBindingHelper::registerKeyBinding); + ClientHooks.reloadListeners().forEach(ResourceLoader.get(PackType.CLIENT_RESOURCES)::registerReloader); + HudElementRegistry.attachElementAfter(VanillaHudElements.SLEEP, ClientHooks.HUD_LAYER, ClientHooks::drawHud); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + + ClientTickEvents.END_CLIENT_TICK.register(client -> ClientHooks.clientTickEnd()); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> ClientHooks.loggedIn(client.player)); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ClientHooks.loggedOut()); + } + + private static void clientbound(ModNetwork.Clientbound packet) { + ClientPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg)); + } +} From 90f3fb6fb0611c8766751bca828513c3f7e3f2c9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:25 +0800 Subject: [PATCH 0884/1461] Add RollableCamera.java to the Fabric 1.21.11 glue --- .../java/com/jnhprojects/mcheli/fabric/RollableCamera.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java new file mode 100644 index 00000000..ff30f0ff --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java @@ -0,0 +1,7 @@ +package com.jnhprojects.mcheli.fabric; + +/** Implemented on {@code Camera} by {@code CameraMixin}. Outside the mixin package, which Mixin refuses to load classes from. */ +public interface RollableCamera { + /** Like vanilla's {@code setRotation}, with a roll in degrees as Forge applies it. */ + void mcheli$setRotation(float yRot, float xRot, float roll); +} From 87d2f56c584616d9ed3a427c0c55efb01d347b10 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:25 +0800 Subject: [PATCH 0885/1461] Add the Fabric camera roll mixin for 1.21.11 --- .../mcheli/fabric/mixin/CameraMixin.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java new file mode 100644 index 00000000..d16eb926 --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +/** + * Vanilla's camera has no roll. This is {@code Camera#setRotation} with the roll Forge patches in, + * so a banked aircraft tilts the view the same on every loader. + */ +@Mixin(Camera.class) +abstract class CameraMixin implements RollableCamera { + @Shadow + @Final + private static Vector3f FORWARDS; + @Shadow + @Final + private static Vector3f UP; + @Shadow + @Final + private static Vector3f LEFT; + + @Shadow + private float xRot; + @Shadow + private float yRot; + @Shadow + @Final + private Vector3f forwards; + @Shadow + @Final + private Vector3f up; + @Shadow + @Final + private Vector3f left; + @Shadow + @Final + private Quaternionf rotation; + + @Override + public void mcheli$setRotation(float yRot, float xRot, float roll) { + this.xRot = xRot; + this.yRot = yRot; + this.rotation.rotationYXZ((float) Math.PI - yRot * (float) (Math.PI / 180.0), -xRot * (float) (Math.PI / 180.0), + roll * (float) (Math.PI / 180.0)); + FORWARDS.rotate(this.rotation, this.forwards); + UP.rotate(this.rotation, this.up); + LEFT.rotate(this.rotation, this.left); + } +} From daa51e0d37bb56272f8e6a11c0a32d342b24bee7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:26 +0800 Subject: [PATCH 0886/1461] Add GameRendererInvoker.java to the Fabric 1.21.11 glue --- .../mcheli/fabric/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..ac63c87b --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#setPostEffect} private on 1.21.11. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("setPostEffect") + void mcheli$setPostEffect(Identifier effect); +} From c6aef2dc31e85c3d0d69c668a1ee69339caa554e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:26 +0800 Subject: [PATCH 0887/1461] Add the Fabric camera, hand and field of view mixin for 1.21.11 --- .../fabric/mixin/GameRendererMixin.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java new file mode 100644 index 00000000..84e93b0c --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * The camera hook, at the two places Forge fires its camera angle event (once vanilla has placed + * the camera for the frame, and again before the camera is copied into the render state), and + * the first person hand, which is hidden in some seats. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "updateCamera", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setup(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/Entity;ZZF)V", + shift = At.Shift.AFTER)) + private void mcheli$afterCameraUpdate(DeltaTracker deltaTracker, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, deltaTracker.getGameTimeDeltaPartialTick(true), GameRendererMixin::setRotation); + } + + @Inject(method = "extractCamera", at = @At("HEAD")) + private void mcheli$beforeExtractCamera(float worldPartialTicks, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, worldPartialTicks, GameRendererMixin::setRotation); + } + + /** Zoom through a sight, where Forge fires its field of view event. */ + @Inject(method = "getFov", at = @At("RETURN"), cancellable = true) + private void mcheli$getFov(Camera camera, float partialTicks, boolean useFovSetting, CallbackInfoReturnable cir) { + cir.setReturnValue(ClientHooks.fov(cir.getReturnValueF())); + } + + @Inject(method = "renderItemInHand", at = @At("HEAD"), cancellable = true) + private void mcheli$renderItemInHand(CallbackInfo ci) { + if (ClientHooks.hidesHand()) { + ci.cancel(); + } + } + + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((RollableCamera) camera).mcheli$setRotation(yaw, pitch, roll); + } +} From 1b93bd8a56af30c8ace80b02b498062bdc093ad9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:27 +0800 Subject: [PATCH 0888/1461] Add the Fabric rider pose mixin for 1.21.11 --- .../mixin/LivingEntityRendererMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java new file mode 100644 index 00000000..4539dd43 --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.renderer.state.CameraRenderState; +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.callback.CallbackInfo; + +/** Tilts riders with their vehicle, where Forge fires its living entity render event. */ +@Mixin(LivingEntityRenderer.class) +abstract class LivingEntityRendererMixin { + + @Inject(method = "submit(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;" + + "Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/CameraRenderState;)V", + at = @At("HEAD")) + private void mcheli$beforeSubmit(LivingEntityRenderState state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera, + CallbackInfo ci) { + ClientHooks.beforeRenderLiving(state, pose, ClientHooks.cameraPartialTicks()); + } +} From ddd9dc355d84f7cc3561be043e198a27ba643a12 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:27 +0800 Subject: [PATCH 0889/1461] Add the Fabric use and pick block mixin for 1.21.11 --- .../mcheli/fabric/mixin/MinecraftMixin.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java new file mode 100644 index 00000000..b4685f81 --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.HitResult; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** The use and pick block keys, which Forge and NeoForge let mods take through an input event. */ +@Mixin(Minecraft.class) +abstract class MinecraftMixin { + @Shadow + public @Nullable LocalPlayer player; + @Shadow + public @Nullable HitResult hitResult; + @Shadow + private int rightClickDelay; + + @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) + private void mcheli$startUseItem(CallbackInfo ci) { + if (player == null) { + return; + } + ClientHooks.UseKey result = ClientHooks.onUseKey(InteractionHand.MAIN_HAND); + if (result != ClientHooks.UseKey.PASS) { + // Vanilla sets the same delay before it looks at the hands. + rightClickDelay = 4; + if (result == ClientHooks.UseKey.CONSUME_AND_SWING) { + player.swing(InteractionHand.MAIN_HAND); + } + ci.cancel(); + } + } + + @Inject(method = "pickBlock", at = @At("HEAD"), cancellable = true) + private void mcheli$pickBlock(CallbackInfo ci) { + if (hitResult != null && hitResult.getType() != HitResult.Type.MISS && ClientHooks.onPickBlockKey()) { + ci.cancel(); + } + } +} From 0c06bcfd575902d554b013f1fbcd9860c640f441 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:28 +0800 Subject: [PATCH 0890/1461] Add PackRepositoryMixin.java to the Fabric 1.21.11 glue --- .../fabric/mixin/PackRepositoryMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java new file mode 100644 index 00000000..965719b9 --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.CommonHooks; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.Arrays; + +/** + * Adds the imported original content to every pack repository, client resources and server data + * alike, which is what Forge and NeoForge do through their pack finder event. + */ +@Mixin(PackRepository.class) +abstract class PackRepositoryMixin { + + @ModifyVariable(method = "", at = @At("HEAD"), argsOnly = true) + private static RepositorySource[] mcheli$addImportedContent(RepositorySource[] sources) { + RepositorySource[] withOurs = Arrays.copyOf(sources, sources.length + 1); + withOurs[sources.length] = CommonHooks.packSource(); + return withOurs; + } +} From a043a193ea3b87b2d20161b568f20193fe0e5bf8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:28 +0800 Subject: [PATCH 0891/1461] Add RuntimeSoundInstanceMixin.java to the Fabric 1.21.11 glue --- .../mixin/RuntimeSoundInstanceMixin.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java diff --git a/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java new file mode 100644 index 00000000..f085c684 --- /dev/null +++ b/fabric-1.21.11/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java @@ -0,0 +1,20 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.concurrent.CompletableFuture; + +/** Streams content pack sounds from disk through Fabric's sound instance hook. */ +@Mixin(RuntimeSoundInstance.class) +abstract class RuntimeSoundInstanceMixin implements FabricSoundInstance { + + @Override + public CompletableFuture getAudioStream(SoundBufferLibrary loader, Identifier id, boolean repeatInstantly) { + return ((RuntimeSoundInstance) (Object) this).openStream(loader, id, repeatInstantly); + } +} From f748bc8a16053357e38ac5cc55db71e1db39fb25 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:29 +0800 Subject: [PATCH 0892/1461] Drop the placeholder wording from the Fabric 1.21.11 metadata --- fabric-1.21.11/src/main/resources/fabric.mod.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fabric-1.21.11/src/main/resources/fabric.mod.json b/fabric-1.21.11/src/main/resources/fabric.mod.json index 1b87b793..8346f4c2 100644 --- a/fabric-1.21.11/src/main/resources/fabric.mod.json +++ b/fabric-1.21.11/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "${mod_id}", "version": "${mod_version}", "name": "${mod_name}", - "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", + "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", "authors": ["JayMar921 (rework)", "EMB4 (original author)", "Fureniku (CurseForge maintainer)"], "contact": { "homepage": "https://github.com/JnH-Projects/MCHeli-Rework-Forge", @@ -14,8 +14,10 @@ "icon": "mcheli_logo.png", "environment": "*", "entrypoints": { - "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"] + "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"], + "client": ["com.jnhprojects.mcheli.fabric.McheliFabricClient"] }, + "mixins": ["mcheli.mixins.json"], "depends": { "fabricloader": "${loader_version_range}", "fabric-api": "*", From f2154d607543cf57a20349ba0ae8620ebb6d1763 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:29 +0800 Subject: [PATCH 0893/1461] List the Fabric 1.21.11 mixins --- .../src/main/resources/mcheli.mixins.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-1.21.11/src/main/resources/mcheli.mixins.json diff --git a/fabric-1.21.11/src/main/resources/mcheli.mixins.json b/fabric-1.21.11/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..3dcbaf14 --- /dev/null +++ b/fabric-1.21.11/src/main/resources/mcheli.mixins.json @@ -0,0 +1,20 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "PackRepositoryMixin" + ], + "client": [ + "CameraMixin", + "GameRendererInvoker", + "GameRendererMixin", + "LivingEntityRendererMixin", + "MinecraftMixin", + "RuntimeSoundInstanceMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From 33845070c701c981ffb8e683f7c678c5a370a555 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:30 +0800 Subject: [PATCH 0894/1461] Set up the Fabric 1.21.11 runs --- fabric-1.21.11/build.gradle | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fabric-1.21.11/build.gradle b/fabric-1.21.11/build.gradle index cbaa0761..a4111c63 100644 --- a/fabric-1.21.11/build.gradle +++ b/fabric-1.21.11/build.gradle @@ -7,10 +7,15 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + loom { runs { configureEach { runDir = "../run-${minecraft_version}" + vmArg "-Dmcheli.legacyZip=${legacyZip}" } server { programArgs '--nogui' @@ -20,7 +25,7 @@ loom { dependencies { minecraft "com.mojang:minecraft:${minecraft_version}" - // 1.21.11 is obfuscated; Mojang's names keep the code the same as on Forge and NeoForge. + // 1.21.11 is obfuscated; Mojang's names keep the shared code the same as on Forge and NeoForge. mappings loom.officialMojangMappings() modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" From 0da1639a7d0f58dd85e2a389477a488b35793292 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:30 +0800 Subject: [PATCH 0895/1461] Stop treating Fabric 1.21.11 as a placeholder --- fabric-1.21.11/gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fabric-1.21.11/gradle.properties b/fabric-1.21.11/gradle.properties index 860d7a50..3daa5f28 100644 --- a/fabric-1.21.11/gradle.properties +++ b/fabric-1.21.11/gradle.properties @@ -1,4 +1,4 @@ -# Minecraft 1.21.11 on Fabric (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 1.21.11 on Fabric loader=fabric minecraft_version=1.21.11 minecraft_version_range=~1.21.11 @@ -6,4 +6,3 @@ fabric_loader_version=0.20.1 loader_version_range=>=0.16.0 fabric_api_version=0.141.6+1.21.11 java_version=21 -scaffold=true From 4df85a340657e02b104d0033d68e4376c67f0fe2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:31 +0800 Subject: [PATCH 0896/1461] Build the 1.21.11 modules --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e6390ea9..30aac210 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mod_license=MCHeli Rework Licence 1.0 (see LICENSE) # Platform modules built by this project, one per Minecraft version. Each has its own # gradle.properties with the Minecraft, loader and Java versions. -platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-26.2,neoforge-26.2,fabric-26.2,forge-26.1,neoforge-26.1,fabric-26.1,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 +platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-26.2,neoforge-26.2,fabric-26.2,forge-26.1,neoforge-26.1,fabric-26.1,forge-1.21.11,neoforge-1.21.11,fabric-1.21.11,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 # Libraries the common module compiles against. Every supported Minecraft version ships them. slf4j_version=2.0.17 From f4955ceafd30a088c8d4e12694bbeaf114a41801 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:31 +0800 Subject: [PATCH 0897/1461] Note the 1.21.11 port and the Forge recipe fix in the changelog --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6aec401..034c2ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,12 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). the vehicle screen, the drafting table, team manager, UAV stations and the workshop. On 1.21.1 the PVP button of the team manager switches the server's PVP setting until a restart, since that version has no game rule for it, and target markers ignore view bobbing. -- Minecraft 26.2 and 26.1 on Forge, NeoForge and Fabric, with everything 26.3 has. On 26.1 teams pick - from the sixteen chat colours, since that version has no separate team colours. +- Minecraft 26.2, 26.1 and 1.21.11 on Forge, NeoForge and Fabric, with everything 26.3 has. On 26.1 and + 1.21.11 teams pick from the sixteen chat colours, since those versions have no separate team colours. ### Changed -- Forge, NeoForge and Fabric on 26.3, 26.2, 26.1 and 1.21.1 are all built on every pull request. +- Forge, NeoForge and Fabric on 26.3, 26.2, 26.1, 1.21.11 and 1.21.1 are all built on every pull request. - Clients and servers on different loaders cannot play together, which the roadmap used to promise. Each loader refuses the other at its own handshake, before the mod runs. A pack zip still works on every loader when copied by hand. @@ -30,6 +30,8 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). - Fabric clients crashed at startup on 26.3 and 1.21.1, because `RollableCamera` sat in the mixin package. - Glow particles were invisible on Fabric 1.21.1: vanilla only draws the particle types on its own list. - On 1.21.1 the E-767 recipe failed to load, since its UAV station ingredient used the newer format. +- On Forge 1.21.1, recipes that use gunpowder or feathers could not be crafted, since Forge names those + tags differently from the conventional ones our recipes use. The Forge modules on 1.21.x now define them. ## [1.1.0-beta.8.1] - 2026-11-06 From a17d278e0fe1ae0df301866417af6980168089ce Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:32 +0800 Subject: [PATCH 0898/1461] List Minecraft 1.21.11 in the README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 157fa823..ee522e0c 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ MCHeli Rework icon -Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3**, **26.2**, **26.1** and -**1.21.1**, on **Forge**, **NeoForge** and **Fabric**. +Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3**, **26.2**, **26.1**, +**1.21.11** and **1.21.1**, on **Forge**, **NeoForge** and **Fabric**. MCHeli Rework is a ground-up port of MCHeli 1.0.3, a mod that stopped at Minecraft 1.7.10. Every system is rewritten for the modern game, while the original vehicle definitions, models, @@ -89,13 +89,14 @@ Reserved original, the code is source available rather than open source. | 26.3 | 66.1.0 or newer in 66.x | 26.3.0.99 or newer | Loader 0.20 and Fabric API | 25 | | 26.2 | 65.1.5 or newer in 65.x | 26.2.0.93 or newer | Loader 0.20 and Fabric API | 25 | | 26.1 | 62.0.9 or newer in 62.x | 26.1.2.126 or newer | Loader 0.20 and Fabric API | 25 | +| 1.21.11 | 61.2.4 or newer in 61.x | 21.11.48 or newer | Loader 0.16 and Fabric API | 21 | | 1.21.1 | 52.1.0 or newer | 21.1.200 or newer | Loader 0.16 and Fabric API | 21 | The launcher bundles the right Java for each version. ## Installing -1. Install Forge, NeoForge or Fabric for Minecraft 26.3, 26.2, 26.1 or 1.21.1. On Fabric, also install +1. Install Forge, NeoForge or Fabric for Minecraft 26.3, 26.2, 26.1, 1.21.11 or 1.21.1. On Fabric, also install [Fabric API](https://modrinth.com/mod/fabric-api). 2. Download the jar for your loader from the [releases](https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases): From 7dab8c2d91acbce4773b135e3558e29ede1c03bc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:32 +0800 Subject: [PATCH 0899/1461] Tick the 1.21.11 port on the roadmap --- docs/ROADMAP.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ac9fe2b0..0b2bb4c5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -309,10 +309,11 @@ added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest - [x] Every resource the repository holds ships in the jar (betas 4 to 8 lacked recipes, the thermal shader, the smoke particle and the drafting table model) - [x] Phases 8 to 12 ported to all three loaders on 1.21.1 -- [ ] Every module in `platforms` (26.3, 26.2, 26.1 and 1.21.1 are in) +- [ ] Every module in `platforms` (26.3, 26.2, 26.1, 1.21.11 and 1.21.1 are in) - [x] `minecraft-26.2` with its Forge, NeoForge and Fabric glue - [x] `minecraft-26.1` with its Forge, NeoForge and Fabric glue -- [ ] `minecraft-1.21.11`, `minecraft-1.21.5`, +- [x] `minecraft-1.21.11` with its Forge, NeoForge and Fabric glue +- [ ] `minecraft-1.21.5`, `minecraft-1.20.6`, `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its placeholder modules with real glue - [ ] `common` compiles for Java 17 (no pattern matching `switch`, no `SequencedCollection` From 243c7b60015e5a5c8697e899807f2b6304418f12 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:33 +0800 Subject: [PATCH 0900/1461] Describe what differs on 1.21.11, including the cutout render type swap --- docs/ARCHITECTURE.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3b5ce567..2a5d12ed 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -51,7 +51,7 @@ loader. The top layer is a thin module per loader and Minecraft version, which c lower layers together with its own glue into one jar. See [DEVELOPMENT.md](DEVELOPMENT.md#three-layers). -Only 26.3, 26.2, 26.1 and 1.21.1 have the middle layer so far. The other supported versions have placeholder +Only 26.3, 26.2, 26.1, 1.21.11 and 1.21.1 have the middle layer so far. The other supported versions have placeholder loader modules (`scaffold=true`) that compile neither lower layer: just an entry point that logs, against that version's loader and toolchain. Porting a version means adding its `minecraft-` folder, copying the loader glue in and dropping the flag. @@ -119,6 +119,26 @@ NeoForge rolls the camera the other way round from Forge, so its mixin negates t - **Camera hooks.** `GameRenderer.update` also takes whether game time advances, and Fabric's camera mixin shadows `Vector3f` constants rather than `Vector3fc`. +### Minecraft 1.21.11 + +`minecraft-1.21.11` is `minecraft-26.1` with these differences. It is obfuscated in production, so +Forge sets the official mappings and Fabric uses the remapping Loom plugin. + +- **Cutout render types.** `entityCutout` culls here and does not on 26.x, and `entityCutoutNoCull` + is what 26.x calls `entityCutout`. The names exist on both sides, so a straight copy compiles and + turns parts inside out. +- **Screens.** `render`, `renderBg` and `renderLabels` rather than the `extract` methods, on + `GuiGraphics` with `drawString` rather than `GuiGraphicsExtractor` with `text`. Container screens + set their size after `super`. +- **Pipelines.** `withBlend` and `withDepthWrite` rather than colour target and depth stencil states. +- **Interaction.** A click on the hull arrives through `interactAt`, as on 1.21.1. +- **Camera.** `GameRenderer.updateCamera` calls `Camera.setup`, and NeoForge and Fabric hook after that + and at the head of `extractCamera`. Field of view is `GameRenderer.getFov`. +- **Fabric API.** The older names: `KeyBindingHelper`, `ParticleFactoryRegistry`, `FabricItemGroup`, + `PayloadTypeRegistry.playS2C`, and on `CustomIngredient` `getMatchingItems` and `toDisplay`. +- **Conventional tags.** Forge on 1.21.x has `forge:gunpowder` and `forge:feathers` but not the `c:` + names our recipes use, so its modules define `c:gunpowders` and `c:feathers` from those. + ### Minecraft 1.21.1 `minecraft-1.21.1` is a copy of `minecraft-26.3` ported back, so the two read side by side. During From ba53c15578bf4f75f1247f3ac3fbbdcf4a786cdd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:33 +0800 Subject: [PATCH 0901/1461] List the 1.21.11 modules and say to compare what the loaders report --- docs/DEVELOPMENT.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 90746ae8..5c47f1de 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -18,6 +18,9 @@ | Forge | 62.0.9 for Minecraft 26.1 | | | NeoForge | 26.1.2.126 for Minecraft 26.1 | | | Fabric | Loader 0.20.1, Fabric API 0.145.1+26.1 | | +| Forge | 61.2.4 for Minecraft 1.21.11 | Built with ForgeGradle 7 and the official mappings. | +| NeoForge | 21.11.48 for Minecraft 1.21.11 | | +| Fabric | Fabric API 0.141.6+1.21.11 | Loom remaps the jar, which is obfuscated on 1.21.11. | | Forge | 52.1.20 for Minecraft 1.21.1 | Built with ForgeGradle 7 and the official mappings. | | NeoForge | 21.1.265 for Minecraft 1.21.1 | | | Fabric | Fabric API 0.116.17+1.21.1 | Loom remaps the jar, which is obfuscated on 1.21.1. | @@ -47,7 +50,7 @@ Shared properties live in the root `gradle.properties`: `mod_version`, the mod m of loader modules (`platforms`) and the library versions `common` compiles against. Each loader module has its own `gradle.properties` with its loader, Minecraft and Java versions. During the betas up to beta.8, `platforms` listed only `forge-26.3`. Roadmap phase 13 adds modules back as each -one catches up, currently the three each on 26.3, 26.2, 26.1 and 1.21.1; the others stay in the repository +one catches up, currently the three each on 26.3, 26.2, 26.1, 1.21.11 and 1.21.1; the others stay in the repository until they are ported. `mods.toml`, `neoforge.mods.toml`, `fabric.mod.json` and `pack.mcmeta` are filled in from them at build time. What every loader module shares (source folders, jar contents, manifest, file @@ -70,6 +73,8 @@ minecraft-26.2/ the same shared code on Minecraft 26.2 forge-26.2/, neoforge-26.2/, fabric-26.2/ the loader glue for 26.2 minecraft-26.1/ the same shared code on Minecraft 26.1 forge-26.1/, neoforge-26.1/, fabric-26.1/ the loader glue for 26.1 +minecraft-1.21.11/ the same shared code on Minecraft 1.21.11 +forge-1.21.11/, neoforge-1.21.11/, fabric-1.21.11/ the loader glue for 1.21.11 minecraft-1.21.1/ the same shared code, backported to Minecraft 1.21.1 forge-1.21.1/, neoforge-1.21.1/, fabric-1.21.1/ the loader glue for 1.21.1 -/ placeholders for the other supported versions (see below) @@ -116,7 +121,7 @@ of them on the command line. After pulling the split, re-import the Gradle proje ### Placeholder modules Every [supported version](ROADMAP.md#supported-versions) has a loader module, but only 26.3, 26.2, -26.1 and 1.21.1 are ported. The other 13 (`forge-`, `neoforge-` and `fabric-` for 1.21.11, +26.1, 1.21.11 and 1.21.1 are ported. The other 10 (`forge-`, `neoforge-` and `fabric-` for 1.21.5 and 1.20.6, plus `forge-` and `fabric-` for 1.20.2 and 1.19.4) are placeholders: each pins its loader, mappings and Java version, and holds only an entry point that logs a line at startup and the mod metadata. Their `gradle.properties` sets `scaffold=true`, which tells @@ -135,7 +140,8 @@ Porting a version for a final release: add `minecraft-` (copied from th version), copy the loader glue from the ported module of the same loader, remove `scaffold=true` and add the module to `platforms`. -A clean compile proves little about a port. Mixin targets, the Forge access transformer, shaders and +A clean compile proves little about a port. Compare what the loaders report as well: a recipe that names an +undefined tag is logged and dropped on one loader while the others load it. Mixin targets, the Forge access transformer, shaders and data files only fail at runtime, so start a client and a dedicated server on every loader of that version, and check a built jar. A loader can also pin a library more strictly than the version the root `gradle.properties` asks for; override it in that module's `gradle.properties`, as NeoForge 26.2 From 300a9ecfa1dda2585a84342ee71094768ff33cb1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 6 Nov 2026 23:31:34 +0800 Subject: [PATCH 0902/1461] Add 1.21.11 checks for remapped jars, culling, zoom and Forge recipes --- docs/TESTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/TESTING.md b/docs/TESTING.md index c50b0e03..43ac9ec3 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -48,6 +48,14 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi another loader is refused by the loaders themselves, with a message naming the one it needs. - [ ] A `serverconfig/mcheli-server.toml` written by 1.1.0-beta.2 (Forge) keeps its values. +### 1.21.11 only + +- [ ] From the built jars on a launcher profile: all three loaders start, which proves Fabric's remapped + jar and its mixins, and thermal turns on and off. +- [ ] Vehicle parts are not inside out, from outside and from a seat. +- [ ] Sight zoom works on all three loaders, and rider pose and camera roll look right in a banked aircraft. +- [ ] A recipe with gunpowder or feathers crafts on Forge, here and on 1.21.1. + ### 26.1 only - [ ] From the built jars on a launcher profile: thermal on and off on all three loaders, and the From 967bdb34e19b624208295c7dbaaa95a9fd39c928 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:06 +0800 Subject: [PATCH 0903/1461] Add ClientConfig.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/ClientConfig.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ClientConfig.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ClientConfig.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ClientConfig.java new file mode 100644 index 00000000..ff258847 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ClientConfig.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Per player look and feel, in {@code config/mcheli-client.toml}. Nothing here changes how the + * game plays, so each player picks for themselves and the server never sees it. + */ +public final class ClientConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue NIGHT_GLOW; + private static final ConfigSpec.IntValue PACK_CACHE_SIZE; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("effects"); + NIGHT_GLOW = builder + .comment("Whether tracers, rocket motors, flares and explosions glow. The glow is faint by day", + "and strongest at night, in caves and under water.") + .define("nightGlow", true); + builder.pop(); + + builder.push("packs"); + PACK_CACHE_SIZE = builder + .comment("How much the content packs downloaded from servers may take up in mcheli/cache, in megabytes.", + "The packs used longest ago are deleted first; the current server's packs are always kept.", + "A single pack larger than this is not downloaded.") + .defineInRange("cacheSize", 256, 16, 16384); + builder.pop(); + + SPEC = builder.build(); + } + + private ClientConfig() { + } + + /** Reads {@code config/mcheli-client.toml}, writing it with the defaults if it is new. */ + public static void load(Path configDir) { + Path file = configDir.resolve(MCHeli.MOD_ID + "-client.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + public static boolean nightGlow() { + return SPEC.isLoaded() ? NIGHT_GLOW.get() : NIGHT_GLOW.getDefault(); + } + + public static long packCacheBytes() { + return (long) (SPEC.isLoaded() ? PACK_CACHE_SIZE.get() : PACK_CACHE_SIZE.getDefault()) << 20; + } +} From 8b9dff3d85a58b170264f714a6f26d3a8eaf9d67 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:06 +0800 Subject: [PATCH 0904/1461] Add CommonHooks.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/CommonHooks.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/CommonHooks.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/CommonHooks.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/CommonHooks.java new file mode 100644 index 00000000..c88ad40a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/CommonHooks.java @@ -0,0 +1,83 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.network.PackSync; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.repository.RepositorySource; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.level.storage.LevelResource; + +import java.util.List; + +/** + * What the mod does on game events that both sides see. Each loader module calls these from its + * own events; nothing here knows which loader that is. + */ +public final class CommonHooks { + private CommonHooks() { + } + + /** Once the registries are frozen, on the main thread: what vanilla keeps in its own tables. */ + public static void commonSetup() { + DispenserBlock.registerBehavior(ModItems.THROWABLE.get(), new ThrowableItem.DispenseBehavior()); + } + + /** + * Adds the imported original content to the resource packs (client) and data packs (server). + * Without the original zip there is nothing to add. + */ + public static RepositorySource packSource() { + return onLoad -> { + // Fabric builds some pack repositories before our initializer has run. + LegacyContent content = MCHeli.legacyContent(); + if (content != null) { + content.pack().ifPresent(onLoad); + } + }; + } + + /** Before the worlds load: read the world's server config, then load the content packs it allows. */ + public static void serverAboutToStart(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + MCHeli.content().replaceWith(MCHeli.packs().load(true)); + } + + public static void serverStopped() { + ServerConfig.unload(); + } + + public static void serverTickEnd(MinecraftServer server) { + PackSync.pump(server); + } + + public static void registerCommands(CommandDispatcher dispatcher) { + McheliCommand.register(dispatcher); + } + + /** + * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack. Every + * MCHeli recipe goes into a player's recipe book when they join: the drafting table lists them + * from there, and they can be found in the crafting table's book too. + */ + public static void playerLoggedIn(ServerPlayer player) { + PackSync.onLogin(player); + // A remote pilot whose drone did not come back with them is shown and made mortal again. + if (!(player.getVehicle() instanceof AircraftEntity uav && uav.isRemotePilot(player))) { + AircraftEntity.clearRemotePilot(player); + } + List> ours = player.level().getServer().getRecipeManager().getRecipes().stream() + .filter(holder -> holder.id().location().getNamespace().equals(MCHeli.MOD_ID)) + .toList(); + player.awardRecipes(ours); + } + + public static void playerLoggedOut(ServerPlayer player) { + PackSync.onLogout(player); + } +} From a2e55ff79f8de792eb0ee060366228d89c1b17b2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:07 +0800 Subject: [PATCH 0905/1461] Add ContentPacks.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/ContentPacks.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ContentPacks.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ContentPacks.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ContentPacks.java new file mode 100644 index 00000000..d3bfdee5 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ContentPacks.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackFolder; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * The content packs in {@code /mcheli/packs}, loaded together with the imported 1.0.3 + * content, which always comes first. + * + *

At startup every pack in the folder is loaded, so the vehicles exist before any world does. + * When a server starts (single player included) and on {@code /mcheli reload}, the folder is + * scanned again and the server config applied: the allow list, the size limit per pack and the + * limit for all packs together. The packs that pass are what players get sent. + * + *

A zip pack is read in place, so packs stay open while their definitions and assets are in + * use. The set a reload replaces is closed on the reload after that, so a client in the same game + * that is still drawing from it never reads from a closed zip. + */ +public final class ContentPacks { + private static final int PROBLEMS_IN_CHAT = 8; + private static final String SOUNDS = "assets/mcheli/sounds/"; + + private final Path folder; + private final PendingPacks pending; + private final LegacyContent legacy; + private volatile List active = List.of(); + private volatile List problems = List.of(); + private volatile Set sounds = Set.of(); + private List retired = List.of(); + + ContentPacks(Path gameDir, LegacyContent legacy) { + this.folder = gameDir.resolve(MCHeli.MOD_ID).resolve("packs"); + this.pending = new PendingPacks(gameDir.resolve(MCHeli.MOD_ID).resolve("pending")); + this.legacy = legacy; + } + + /** {@code /mcheli/packs}. */ + public Path folder() { + return folder; + } + + /** The packs whose definitions are loaded, in load order. The import is not among them. */ + public List active() { + return active; + } + + /** Everything that went wrong in the last load, packs left out included. */ + public List problems() { + return problems; + } + + /** + * Scans the folder and loads the import and the packs. Never throws; whatever goes wrong is + * logged and kept in {@link #problems()}. + * + * @param serverRules whether to apply the server config; off at startup, when no world exists + * @return the new content, for the caller to swap in + */ + public synchronized ContentRegistry load(boolean serverRules) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not create {}", folder, e); + } + + PackFolder.Scan scan = PackFolder.scan(folder); + List found = new ArrayList<>(scan.problems()); + List accepted = new ArrayList<>(); + long total = 0; + for (ContentPack pack : scan.packs()) { + long size; + String refusal; + try { + size = pack.archiveSize(); + refusal = serverRules ? refusal(pack, size, total) : null; + } catch (IOException e) { + size = 0; + refusal = "left out, could not be read: " + e; + } + if (refusal == null) { + accepted.add(pack); + total += size; + } else { + found.add(new PackProblem(pack.id(), refusal)); + close(List.of(pack)); + } + } + for (PackProblem problem : found) { + MCHeli.LOGGER.warn("Content pack {}", problem); + } + + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, legacy.assets())); + for (ContentPack pack : accepted) { + sources.add(pack.contentSource()); + } + ContentRegistry registry = ContentRegistry.load(sources); + found.addAll(registry.problems()); + + close(retired); + PendingPacks.cleanReplaced(folder); + retired = active; + active = List.copyOf(accepted); + problems = List.copyOf(found); + sounds = soundsOf(accepted); + MCHeli.LOGGER.info("Content packs in use: {}", active.isEmpty() ? "none" : active); + return registry; + } + + /** Sound names as {@code RuntimeAssets.soundFile} finds them: {@code heli.ogg} and {@code heli1.ogg} are both {@code heli}. */ + private static Set soundsOf(List packs) { + Set names = new HashSet<>(); + for (ContentPack pack : packs) { + for (String file : pack.files()) { + if (file.startsWith(SOUNDS) && file.endsWith(".ogg")) { + String name = file.substring(SOUNDS.length(), file.length() - ".ogg".length()); + names.add(name); + if (name.endsWith("1")) { + names.add(name.substring(0, name.length() - 1)); + } + } + } + } + return Set.copyOf(names); + } + + /** Whether a pack in use brought this sound, so that the sound manager has not heard of it. */ + public boolean hasSound(String name) { + return sounds.contains(name); + } + + /** What players have sent this server, waiting for an operator. */ + public PendingPacks pending() { + return pending; + } + + private static @Nullable String refusal(ContentPack pack, long size, long total) { + if (!ServerConfig.isPackAllowed(pack.id())) { + return "left out, not on the server's allow list (packs.allowList in mcheli-server.toml)"; + } + if (size > ServerConfig.maxPackBytes()) { + return "left out, %.1f MB is more than the server allows for one pack (packs.maxPackSize, %d MB)" + .formatted(size / 1048576.0, ServerConfig.maxPackBytes() >> 20); + } + if (total + size > ServerConfig.maxTotalPackBytes()) { + return "left out, it would take the packs past the server's total limit (packs.maxTotalSize, %d MB)" + .formatted(ServerConfig.maxTotalPackBytes() >> 20); + } + return null; + } + + /** Sends the problems of the last load as chat lines, the first few of them. */ + public void reportProblems(Consumer out) { + List list = problems; + for (int i = 0; i < Math.min(list.size(), PROBLEMS_IN_CHAT); i++) { + out.accept(Component.translatable("message.mcheli.pack.problem", list.get(i).pack(), list.get(i).message())); + } + if (list.size() > PROBLEMS_IN_CHAT) { + out.accept(Component.translatable("message.mcheli.pack.more_problems", list.size() - PROBLEMS_IN_CHAT)); + } + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } +} From 7f6f2b4af78269a6353aec3641031d865218b3f5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:07 +0800 Subject: [PATCH 0906/1461] Add ContentReload.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/ContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ContentReload.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ContentReload.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ContentReload.java new file mode 100644 index 00000000..7846b7bf --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.network.PackSync; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +/** + * Reads the vehicle and weapon definitions from disk again while the game runs, for + * {@code /mcheli reload}: the server config, then the imported content and the content packs. + * Vehicles in the world pick up their new definition on their next tick (see + * {@code AircraftEntity#definition}). Every player gets the new pack list, downloads what changed + * and rebuilds their content the same way. + */ +public final class ContentReload { + private ContentReload() { + } + + /** @return the newly loaded content, already in use */ + public static ContentRegistry reload(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + ContentRegistry fresh = MCHeli.packs().load(true); + MCHeli.content().replaceWith(fresh); + MCHeli.LOGGER.info("Content reloaded: {} vehicles, {} weapons", fresh.all().size(), fresh.weapons().size()); + PackSync.sendListToAll(server); + return fresh; + } +} From d21ba9fec0c016794102c50b0bf0c8bb4ebec724 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:07 +0800 Subject: [PATCH 0907/1461] Add LegacyContent.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/LegacyContent.java | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/LegacyContent.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/LegacyContent.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/LegacyContent.java new file mode 100644 index 00000000..495bb502 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/LegacyContent.java @@ -0,0 +1,239 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ImportTarget; +import com.jnhprojects.mcheli.content.LegacyImporter; +import net.minecraft.network.chat.Component; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PathPackResources; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackCompatibility; +import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.world.flag.FeatureFlagSet; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Stream; + +/** + * The original vehicles, models, textures and sounds, imported from the player's own copy of + * MCHeli 1.0.3. + * + *

That content is EMB4's and is not redistributed with this mod. Players download the original + * zip from the MCHeli CurseForge page and put it in {@code /mcheli}. On start the zip is + * converted once by {@link LegacyImporter} into {@code mcheli/imported}, which then serves as a + * required resource pack and data pack. The zip is converted again only when it changes or the + * importer's output format does; once imported it may be deleted. + * + *

Development runs point the {@code mcheli.legacyZip} system property at the zip instead. + */ +public final class LegacyContent { + /** The file name the original download has. Any zip with the same layout works. */ + public static final String ZIP_NAME = "MC1.7.10_mcheli_1.0.3.zip"; + + private static final String PACK_ID = "mcheli_content"; + private static final String MARKER = "import.properties"; + /** The pack layout this Minecraft version reads. */ + private static final ImportTarget TARGET = ImportTarget.MODERN; + + private final Path folder; + private final Path packRoot; + private @Nullable Component problem; + + private LegacyContent(Path folder) { + this.folder = folder; + this.packRoot = folder.resolve("imported"); + } + + /** Imports the zip if needed. Never throws; a missing or broken zip leaves the mod without content. */ + static LegacyContent prepare(Path gameDir) { + LegacyContent content = new LegacyContent(gameDir.resolve(MCHeli.MOD_ID)); + try { + content.importIfNeeded(); + } catch (IOException | UncheckedIOException e) { + MCHeli.LOGGER.error("Could not import the original MCHeli content", e); + content.problem = Component.translatable("message.mcheli.content.import_failed", e.getMessage()); + } + return content; + } + + /** The imported {@code assets/mcheli} folder. It does not exist when nothing was imported. */ + public Path assets() { + return packRoot.resolve("assets").resolve(MCHeli.MOD_ID); + } + + /** Why the vehicles are missing, for the player; empty when the content is in place. */ + public Optional problem() { + return Optional.ofNullable(problem); + } + + /** + * The imported folder as a pack, for both the client resource packs and the server data packs. + * Empty when nothing was imported. + */ + public Optional pack() { + if (!Files.isDirectory(packRoot)) { + return Optional.empty(); + } + PackLocationInfo location = new PackLocationInfo(PACK_ID, + Component.translatable("pack.mcheli.content"), PackSource.BUILT_IN, Optional.empty()); + Pack.Metadata metadata = new Pack.Metadata(Component.translatable("pack.mcheli.content.description"), + PackCompatibility.COMPATIBLE, FeatureFlagSet.of(), List.of()); + // Required, so it is enabled in every world and cannot be switched off. It sits at the + // bottom so that regular resource packs can still replace its textures and models. + return Optional.of(new Pack(location, new PathPackResources.PathResourcesSupplier(packRoot), metadata, + new PackSelectionConfig(true, Pack.Position.BOTTOM, false))); + } + + private void importIfNeeded() throws IOException { + Path zip = findZip(); + if (zip == null) { + if (Files.isDirectory(packRoot)) { + MCHeli.LOGGER.info("Using the MCHeli content imported earlier into {}", packRoot); + } else { + MCHeli.LOGGER.error("MCHeli content not found. Download {} from the MCHeli CurseForge page and put it in {}", + ZIP_NAME, folder.toAbsolutePath()); + problem = Component.translatable("message.mcheli.content.missing", ZIP_NAME, folder.toAbsolutePath().toString()); + } + return; + } + + Properties source = describe(zip); + if (source.equals(readMarker())) { + return; + } + + MCHeli.LOGGER.info("Importing MCHeli content from {}", zip); + Path staging = folder.resolve("imported.tmp"); + deleteTree(staging); + LegacyImporter.Summary summary = LegacyImporter.run(zip, staging, TARGET); + try (OutputStream out = Files.newOutputStream(staging.resolve(MARKER))) { + source.store(out, "Written by MCHeli Rework. Delete this folder to import again."); + } + swapIn(staging); + + MCHeli.LOGGER.info("Imported {} files, {} sounds, {} item models and {} recipes into {}", + summary.files(), summary.sounds(), summary.itemModels(), summary.recipes(), packRoot); + for (String skipped : summary.skippedRecipes()) { + MCHeli.LOGGER.info("Recipe left out: {}", skipped); + } + } + + /** + * Replaces the previous import with the fresh one in {@code staging}. + * + *

On Windows a folder that was just deleted keeps its name for a moment while another + * program (a file watcher, a virus scanner) still has a handle in it, and renaming onto that + * name fails with "access denied". So the old import is moved out of the way rather than + * deleted, the renames are retried for a few seconds, and the old copy is deleted last, on a + * best effort basis; a copy left behind is removed on the next import. + */ + private void swapIn(Path staging) throws IOException { + Path old = folder.resolve("imported.old"); + deleteQuietly(old); + if (Files.exists(packRoot)) { + try { + moveWithRetry(packRoot, old); + } catch (IOException e) { + deleteTree(packRoot); + } + } + moveWithRetry(staging, packRoot); + deleteQuietly(old); + } + + private static void moveWithRetry(Path from, Path to) throws IOException { + for (int attempt = 1; ; attempt++) { + try { + Files.move(from, to); + return; + } catch (FileSystemException e) { + if (attempt >= 15) { + throw e; + } + try { + Thread.sleep(50L * attempt); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw e; + } + } + } + } + + private static void deleteQuietly(Path root) { + try { + deleteTree(root); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not delete {}; it is removed on the next import", root, e); + } + } + + private @Nullable Path findZip() throws IOException { + String configured = System.getProperty("mcheli.legacyZip"); + if (configured != null && !configured.isBlank()) { + Path zip = Path.of(configured); + if (Files.isRegularFile(zip)) { + return zip; + } + MCHeli.LOGGER.warn("mcheli.legacyZip points to {}, which does not exist", zip); + } + + Files.createDirectories(folder); + if (Files.isRegularFile(folder.resolve(ZIP_NAME))) { + return folder.resolve(ZIP_NAME); + } + // Browsers rename repeated downloads, so take any zip, the same way every start. + try (Stream files = Files.list(folder)) { + return files.filter(p -> p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) + .filter(Files::isRegularFile) + .sorted() + .findFirst() + .orElse(null); + } + } + + /** What the current import was made from. Comparing size and time is enough to notice a new zip. */ + private static Properties describe(Path zip) throws IOException { + Properties p = new Properties(); + p.setProperty("format", Integer.toString(LegacyImporter.FORMAT)); + p.setProperty("target", TARGET.name()); + p.setProperty("source", zip.toAbsolutePath().normalize().toString()); + p.setProperty("size", Long.toString(Files.size(zip))); + p.setProperty("modified", Long.toString(Files.getLastModifiedTime(zip).toMillis())); + return p; + } + + private Properties readMarker() throws IOException { + Properties p = new Properties(); + Path marker = packRoot.resolve(MARKER); + if (Files.isRegularFile(marker)) { + try (InputStream in = Files.newInputStream(marker)) { + p.load(in); + } + } + return p; + } + + private static void deleteTree(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + try (Stream paths = Files.walk(root)) { + for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) { + Files.delete(p); + } + } + } +} From b28da43b2dac53ac2fcaec6221ba358863345a27 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:08 +0800 Subject: [PATCH 0908/1461] Add MCHeli.java to the 1.21.5 shared sources --- .../java/com/jnhprojects/mcheli/MCHeli.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/MCHeli.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/MCHeli.java new file mode 100644 index 00000000..1a890651 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/MCHeli.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.registry.ModParticles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The mod, minus the loader. Each loader module's entry point installs its {@link Platform}, calls + * {@link #init()}, registers what {@code Registration} collected and wires its events to + * {@link CommonHooks} (and, on a client, {@code ClientHooks}). + */ +public final class MCHeli { + public static final String MOD_ID = "mcheli"; + public static final Logger LOGGER = LoggerFactory.getLogger(MCHeli.class); + + private static LegacyContent legacyContent; + private static ContentPacks packs; + private static ContentRegistry content; + + private MCHeli() { + } + + /** + * Imports the original content if needed, loads the definitions and declares the registry + * entries. The vehicle list must be known here: 1.0.0 had an item per vehicle, and those items + * are still registered so old worlds keep them. + */ + public static void init() { + Platform platform = Platform.get(); + LOGGER.info("MCHeli Rework {} on {}", platform.modVersion(), platform.loaderName()); + legacyContent = LegacyContent.prepare(platform.gameDir()); + packs = new ContentPacks(platform.gameDir(), legacyContent); + content = packs.load(false); + ModBlocks.init(); + ModBlockEntities.init(); + ModDataComponents.init(); + ModEntities.init(); + ModParticles.init(); + ModMenus.init(); + ModItems.init(content); + ModCreativeTabs.init(); + } + + /** + * Vehicle and weapon definitions, from the imported original content and the content packs. + * Available from mod construction onwards. Always the same object; a reload changes what it + * holds. + */ + public static ContentRegistry content() { + return content; + } + + /** Where the original vehicle content came from, and why it is missing if it is. */ + public static LegacyContent legacyContent() { + return legacyContent; + } + + /** The content packs in {@code mcheli/packs} and the ones in use. */ + public static ContentPacks packs() { + return packs; + } +} From 213e8bd5ecda9dfb2d568d75815ec08326c0f94d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:08 +0800 Subject: [PATCH 0909/1461] Add McheliCommand.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/McheliCommand.java | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/McheliCommand.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/McheliCommand.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/McheliCommand.java new file mode 100644 index 00000000..1384fcc4 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/McheliCommand.java @@ -0,0 +1,262 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.Definition; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.network.PackSubmissions; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * {@code /mcheli}: what content is loaded, and operator tools to hand out vehicles and reload the + * definitions and content packs. + *

+ *   /mcheli                              content source and counts
+ *   /mcheli list [helicopter|plane|...]  loaded vehicles
+ *   /mcheli packs                        content packs in use, and their problems
+ *   /mcheli give <players> <vehicle> [count]   (operators)
+ *   /mcheli reload                       re-read the definitions and packs (operators)
+ *   /mcheli pending                      packs players have sent, waiting (operators)
+ *   /mcheli approve <pack>               put a waiting pack in use (operators)
+ *   /mcheli reject <pack>                throw a waiting pack away (operators)
+ * 
+ */ +public final class McheliCommand { + private static final List KINDS = List.of("helicopter", "plane", "tank", "vehicle"); + private static final DynamicCommandExceptionType UNKNOWN_VEHICLE = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.unknown_vehicle", name)); + private static final DynamicCommandExceptionType UNKNOWN_PENDING = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.pending.unknown", name)); + + private McheliCommand() { + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal(MCHeli.MOD_ID) + .executes(McheliCommand::status) + .then(Commands.literal("list") + .executes(ctx -> list(ctx, null)) + .then(Commands.argument("kind", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest(KINDS, builder)) + .executes(ctx -> list(ctx, StringArgumentType.getString(ctx, "kind"))))) + .then(Commands.literal("packs") + .executes(McheliCommand::packs)) + .then(Commands.literal("give") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("targets", EntityArgument.players()) + .then(Commands.argument("vehicle", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest( + MCHeli.content().all().stream().map(AircraftDefinition::name), builder)) + .executes(ctx -> give(ctx, 1)) + .then(Commands.argument("count", IntegerArgumentType.integer(1, 64)) + .executes(ctx -> give(ctx, IntegerArgumentType.getInteger(ctx, "count"))))))) + .then(Commands.literal("reload") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::reload)) + .then(Commands.literal("pending") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::pending)) + .then(Commands.literal("approve") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::approve))) + .then(Commands.literal("reject") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::reject)))); + } + + private static CompletableFuture suggestPending(CommandContext ctx, SuggestionsBuilder builder) { + try { + return SharedSuggestionProvider.suggest(MCHeli.packs().pending().list().stream().map(PendingPacks.Submission::id), builder); + } catch (IOException e) { + return builder.buildFuture(); + } + } + + private static int pending(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List waiting; + try { + waiting = MCHeli.packs().pending().list(); + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + if (waiting.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.none"), false); + } + for (PendingPacks.Submission submission : waiting) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.entry", submission.id(), submission.manifest().version(), + submission.manifest().author(), submission.submitter(), "%.1f".formatted(submission.size() / 1048576.0), + String.join(", ", submission.definitions())), false); + List replaced = replaced(submission); + if (!replaced.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.replaces", String.join(", ", replaced)), false); + } + } + return waiting.size(); + } + + /** + * What going live would take the place of: a pack with the same id, and vehicles or weapons of + * another pack with the same file name. That is allowed, so it is the operator's call to make, + * but it should not come as a surprise. + */ + private static List replaced(PendingPacks.Submission submission) { + List names = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.id().equals(submission.id())) { + names.add("pack " + pack.id()); + } + } + ContentRegistry content = MCHeli.content(); + for (String path : submission.definitions()) { + String name = path.substring(path.lastIndexOf('/') + 1, path.length() - ".txt".length()); + Definition existing = path.startsWith("weapons/") ? content.weapon(name) : content.any(name); + if (existing != null && !existing.pack().equals(submission.id())) { + names.add(name + " (" + existing.pack() + ")"); + } + } + return names; + } + + private static PendingPacks.Submission waiting(CommandContext ctx) throws CommandSyntaxException { + String id = StringArgumentType.getString(ctx, "pack"); + PendingPacks.Submission submission; + try { + submission = MCHeli.packs().pending().find(id); + } catch (IOException e) { + submission = null; + } + if (submission == null) { + throw UNKNOWN_PENDING.create(id); + } + return submission; + } + + private static int approve(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + if (PackSubmissions.install(source.getServer(), submission)) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.approve.success", submission.id(), submission.submitter()), true); + return 1; + } + source.sendFailure(Component.translatable("commands.mcheli.approve.left_out", submission.id())); + MCHeli.packs().reportProblems(source::sendFailure); + } catch (IOException | PackException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.getMessage())); + } + return 0; + } + + private static int reject(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + PackSubmissions.reject(source.getServer(), submission); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reject.success", submission.id(), submission.submitter()), true); + return 1; + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + } + + private static int status(CommandContext ctx) { + ContentRegistry content = MCHeli.content(); + CommandSourceStack source = ctx.getSource(); + MCHeli.legacyContent().problem().ifPresent(source::sendFailure); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status", + content.helicopters().size(), content.planes().size(), content.tanks().size(), content.vehicles().size(), + content.weapons().size(), MCHeli.legacyContent().assets().toString()), false); + List packs = MCHeli.packs().active(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status.packs", packs.size(), + packs.stream().map(ContentPack::id).collect(Collectors.joining(", "))), false); + return content.all().size(); + } + + private static int packs(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List packs = MCHeli.packs().active(); + if (packs.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.none", MCHeli.packs().folder().toString()), false); + } else { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.header", packs.size(), MCHeli.packs().folder().toString()), false); + } + ContentRegistry content = MCHeli.content(); + for (ContentPack pack : packs) { + long vehicles = content.all().stream().filter(def -> def.pack().equals(pack.id())).count(); + long weapons = content.weapons().stream().filter(def -> def.pack().equals(pack.id())).count(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.entry", pack.id(), pack.manifest().version(), + pack.manifest().name(), vehicles, weapons, ContentPack.shortHash(pack.hash())), false); + if (!pack.manifest().author().isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.author", pack.manifest().author()), false); + } + } + MCHeli.packs().reportProblems(source::sendFailure); + return packs.size(); + } + + private static int list(CommandContext ctx, String kind) { + List defs = MCHeli.content().all().stream() + .filter(def -> kind == null || def.kind().equals(kind)) + .toList(); + String names = defs.stream().map(AircraftDefinition::name).collect(Collectors.joining(", ")); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.list", defs.size(), names), false); + return defs.size(); + } + + private static int give(CommandContext ctx, int count) throws CommandSyntaxException { + String name = StringArgumentType.getString(ctx, "vehicle"); + AircraftDefinition def = MCHeli.content().any(name); + if (def == null) { + throw UNKNOWN_VEHICLE.create(name); + } + Collection targets = EntityArgument.getPlayers(ctx, "targets"); + for (ServerPlayer player : targets) { + // Vehicle items do not stack, so each one is a separate stack. + for (int i = 0; i < count; i++) { + player.getInventory().placeItemBackInInventory(AircraftItem.stackOf(def)); + } + } + Component vehicle = AircraftItem.stackOf(def).getHoverName(); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.give", count, vehicle, targets.size()), true); + return targets.size(); + } + + private static int reload(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + ContentRegistry fresh = ContentReload.reload(source.getServer()); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reload.success", + fresh.all().size(), fresh.weapons().size(), MCHeli.packs().active().size()), true); + MCHeli.packs().reportProblems(source::sendFailure); + return fresh.all().size(); + } +} From d47b3b3d153799447dbdf6f02afd284533115d66 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:09 +0800 Subject: [PATCH 0910/1461] Add ModSounds.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/ModSounds.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ModSounds.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ModSounds.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ModSounds.java new file mode 100644 index 00000000..b67d71ab --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ModSounds.java @@ -0,0 +1,64 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.core.Holder; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Sound events from {@code assets/mcheli/sounds.json}. + * + *

The events are not put in the registry. Content files name their sounds freely and a content + * pack may bring its own, so they are created on demand as direct holders, which the sound packet + * and the sound manager both accept. + * + *

A sound a pack brought is in no {@code sounds.json}, so the sound manager would drop it as + * unknown. For those the server sends our own packet, and the client streams the file. + */ +public final class ModSounds { + /** How far a sound carries at a volume of one, and per volume above that, as in vanilla. */ + private static final float RANGE = 16.0F; + + private static final Map EVENTS = new ConcurrentHashMap<>(); + + private ModSounds() { + } + + public static SoundEvent get(String name) { + return EVENTS.computeIfAbsent(name, n -> SoundEvent.createVariableRangeEvent(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, n))); + } + + /** Plays {@code name} at the entity for everyone nearby. Call on the server. */ + public static void playAt(Entity entity, String name, float volume, float pitch) { + playAt(entity.level(), entity.getX(), entity.getY(), entity.getZ(), name, volume, pitch); + } + + /** Plays {@code name} at a position for everyone nearby. Call on the server. */ + public static void playAt(Level level, double x, double y, double z, String name, float volume, float pitch) { + if (level instanceof ServerLevel server && MCHeli.packs().hasSound(name)) { + ModNetwork.sendNear(server, x, y, z, RANGE * Math.max(volume, 1.0F), new PackSoundPacket(name, x, y, z, volume, pitch)); + } else { + level.playSound(null, x, y, z, get(name), SoundSource.NEUTRAL, volume, pitch); + } + } + + /** Plays {@code name} for one player only, such as a hit confirmation. */ + public static void playTo(ServerPlayer player, String name, float volume, float pitch) { + if (MCHeli.packs().hasSound(name)) { + ModNetwork.sendToPlayer(player, new PackSoundPacket(name, player.getX(), player.getY(), player.getZ(), volume, pitch)); + } else { + player.connection.send(new ClientboundSoundPacket(Holder.direct(get(name)), SoundSource.PLAYERS, + player.getX(), player.getY(), player.getZ(), volume, pitch, player.getRandom().nextLong())); + } + } +} From c097016bd9414df950e1a69590301130e4cb485e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:09 +0800 Subject: [PATCH 0911/1461] Add ServerConfig.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/ServerConfig.java | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ServerConfig.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ServerConfig.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ServerConfig.java new file mode 100644 index 00000000..9db31738 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/ServerConfig.java @@ -0,0 +1,172 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Server rules, the part of the original {@code mcheli.cfg} that decides how the game plays. + * The file lives per world in {@code serverconfig/mcheli-server.toml}, where Forge kept it, and is + * never sent to clients (only the server applies these rules). It is read when the world starts + * and again on {@code /mcheli reload}. + * + *

The values only exist while a world is loaded. Before that, and on clients, the getters return + * the defaults, which match 1.0.0. + */ +public final class ServerConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue EXPLOSIONS_BREAK_BLOCKS; + private static final ConfigSpec.BooleanValue EXPLOSIONS_START_FIRES; + private static final ConfigSpec.DoubleValue VEHICLE_DAMAGE_FACTOR; + private static final ConfigSpec.BooleanValue INFINITE_AMMO; + private static final ConfigSpec.BooleanValue INFINITE_FUEL; + private static final ConfigSpec.StringListValue DISABLED_VEHICLES; + private static final ConfigSpec.StringListValue PACK_ALLOW_LIST; + private static final ConfigSpec.IntValue MAX_PACK_SIZE; + private static final ConfigSpec.IntValue MAX_TOTAL_SIZE; + private static final ConfigSpec.BooleanValue ALLOW_SUBMISSIONS; + private static final ConfigSpec.BooleanValue REQUIRE_APPROVAL; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("explosions"); + EXPLOSIONS_BREAK_BLOCKS = builder + .comment("Whether weapons and crashing vehicles break blocks. The mobGriefing game rule also applies.", + "Original: Explosion_DestroyBlock") + .define("breakBlocks", true); + EXPLOSIONS_START_FIRES = builder + .comment("Whether incendiary weapons and crashes set blocks on fire. Original: Explosion_FlamingBlock") + .define("startFires", true); + builder.pop(); + + builder.push("vehicles"); + VEHICLE_DAMAGE_FACTOR = builder + .comment("Multiplies the damage every vehicle takes, after armour. 0.5 makes them twice as tough.") + .defineInRange("damageFactor", 1.0, 0.0, 100.0); + DISABLED_VEHICLES = builder + .comment("Vehicles that cannot be placed, by definition name (the file name, such as \"ah-64\").", + "Vehicles already in the world stay.") + .defineList("disabled", List.of()); + INFINITE_FUEL = builder + .comment("Whether vehicles never run out of fuel. Original: InfinityFuel") + .define("infiniteFuel", false); + builder.pop(); + + builder.push("weapons"); + INFINITE_AMMO = builder + .comment("Whether vehicle weapons never run out of ammunition. Original: InfinityAmmo") + .define("infiniteAmmo", false); + builder.pop(); + + builder.comment("Content packs from /mcheli/packs. Players get the packs from the server when they join.", + "Changes apply when the world starts or after /mcheli reload.") + .push("packs"); + PACK_ALLOW_LIST = builder + .comment("Pack ids the server loads, such as \"rework-example\". Empty loads every pack in the folder.") + .defineList("allowList", List.of()); + MAX_PACK_SIZE = builder + .comment("Largest pack the server loads and sends to players, in megabytes.") + .defineInRange("maxPackSize", 8, 1, 512); + MAX_TOTAL_SIZE = builder + .comment("Most the server loads and sends in all, in megabytes. Packs are taken in id order until", + "the next one would go over.") + .defineInRange("maxTotalSize", 64, 1, 4096); + ALLOW_SUBMISSIONS = builder + .comment("Whether players may send this server packs they made in the workshop. They wait in mcheli/pending", + "until an operator runs /mcheli approve. Off, players can only get packs from the server's owner.") + .define("allowSubmissions", false); + REQUIRE_APPROVAL = builder + .comment("Whether a submitted pack waits for an operator. Off, any player can add vehicles and weapons for", + "everyone, and even replace existing ones, so only turn it off among people you trust. A pack taking", + "the id of a pack already in use always waits.") + .define("requireApproval", true); + builder.pop(); + + SPEC = builder.build(); + } + + private ServerConfig() { + } + + /** Reads the world's file, writing it with the defaults if it is new. */ + public static void load(Path worldDir) { + Path file = worldDir.resolve("serverconfig").resolve(MCHeli.MOD_ID + "-server.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + /** The world closed: back to the defaults. */ + public static void unload() { + SPEC.unload(); + } + + public static boolean explosionsBreakBlocks() { + return SPEC.isLoaded() ? EXPLOSIONS_BREAK_BLOCKS.get() : EXPLOSIONS_BREAK_BLOCKS.getDefault(); + } + + public static boolean explosionsStartFires() { + return SPEC.isLoaded() ? EXPLOSIONS_START_FIRES.get() : EXPLOSIONS_START_FIRES.getDefault(); + } + + public static float vehicleDamageFactor() { + double factor = SPEC.isLoaded() ? VEHICLE_DAMAGE_FACTOR.get() : VEHICLE_DAMAGE_FACTOR.getDefault(); + return (float) factor; + } + + public static boolean infiniteAmmo() { + return SPEC.isLoaded() ? INFINITE_AMMO.get() : INFINITE_AMMO.getDefault(); + } + + public static boolean infiniteFuel() { + return SPEC.isLoaded() ? INFINITE_FUEL.get() : INFINITE_FUEL.getDefault(); + } + + /** Whether the allow list lets the server load this pack. An empty list allows every pack. */ + public static boolean isPackAllowed(String id) { + if (!SPEC.isLoaded() || PACK_ALLOW_LIST.get().isEmpty()) { + return true; + } + for (String allowed : PACK_ALLOW_LIST.get()) { + if (allowed.trim().toLowerCase(Locale.ROOT).equals(id)) { + return true; + } + } + return false; + } + + public static long maxPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_PACK_SIZE.get() : MAX_PACK_SIZE.getDefault()) << 20; + } + + public static long maxTotalPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_TOTAL_SIZE.get() : MAX_TOTAL_SIZE.getDefault()) << 20; + } + + public static boolean allowSubmissions() { + return SPEC.isLoaded() ? ALLOW_SUBMISSIONS.get() : ALLOW_SUBMISSIONS.getDefault(); + } + + public static boolean requireApproval() { + return SPEC.isLoaded() ? REQUIRE_APPROVAL.get() : REQUIRE_APPROVAL.getDefault(); + } + + public static boolean isDisabled(String vehicle) { + if (!SPEC.isLoaded()) { + return false; + } + for (String name : DISABLED_VEHICLES.get()) { + if (name.trim().toLowerCase(Locale.ROOT).equals(vehicle)) { + return true; + } + } + return false; + } +} From 1365cf1273b3b252cbb73916a35a536cd2e09a78 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:10 +0800 Subject: [PATCH 0912/1461] Add block/DraftingTableBlock.java to the 1.21.5 shared sources --- .../mcheli/block/DraftingTableBlock.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java new file mode 100644 index 00000000..8545d2b5 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java @@ -0,0 +1,100 @@ +package com.jnhprojects.mcheli.block; + +import com.mojang.serialization.MapCodec; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +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.block.state.properties.IntegerProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jspecify.annotations.Nullable; + +/** + * The drafting table, {@code MCH_DraftingTableBlock}: right-click opens the recipe browser, and + * Shift and right-click switches its lamp. It turns in eighths of a circle to face whoever placed + * it, and its block entity draws the original model. + */ +public class DraftingTableBlock extends BaseEntityBlock { + private static final MapCodec CODEC = simpleCodec(DraftingTableBlock::new); + public static final IntegerProperty ROTATION = IntegerProperty.create("rotation", 0, 7); + public static final BooleanProperty LIT = BooleanProperty.create("lit"); + private static final VoxelShape SHAPE = Block.column(16.0, 0.0, 15.0); + private static final Component TITLE = Component.translatable("block.mcheli.drafting_table"); + + public DraftingTableBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(ROTATION, 0).setValue(LIT, false)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ROTATION, LIT); + } + + /** {@code onBlockPlacedBy}: the nearest eighth of the placer's heading. */ + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + float yaw = context.getPlayer() != null ? context.getPlayer().getYRot() : 0.0F; + int rotation = (int) ((Mth.positiveModulo(yaw, 360.0F) + 22.5F) / 45.0F) % 8; + return defaultBlockState().setValue(ROTATION, rotation); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive()) { + level.setBlock(pos, state.cycle(LIT), Block.UPDATE_ALL); + level.playSound(null, pos, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.BLOCKS, 0.3F, 0.5F); + } else { + player.openMenu(state.getMenuProvider(level, pos)); + } + return InteractionResult.SUCCESS; + } + + @Override + protected @Nullable MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { + return new SimpleMenuProvider((id, inventory, player) -> new DraftingTableMenu(id, inventory, ContainerLevelAccess.create(level, pos)), TITLE); + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + /** The block entity renderer draws the table. */ + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new DraftingTableBlockEntity(pos, state); + } +} From 9ab703443833275c2bc247319bf17e5a9569c59d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:10 +0800 Subject: [PATCH 0913/1461] Add block/DraftingTableBlockEntity.java to the 1.21.5 shared sources --- .../mcheli/block/DraftingTableBlockEntity.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java new file mode 100644 index 00000000..56238f3f --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.block; + +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** {@code MCH_DraftingTableTileEntity}: holds nothing; it is there so the table's model can be drawn. */ +public class DraftingTableBlockEntity extends BlockEntity { + public DraftingTableBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.DRAFTING_TABLE.get(), pos, state); + } +} From fa6476811c452522240215b180aa3639894c2539 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:11 +0800 Subject: [PATCH 0914/1461] Add client/CameraAngles.java to the 1.21.5 shared sources --- .../mcheli/client/CameraAngles.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java new file mode 100644 index 00000000..ab62e120 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.client; + +import net.minecraft.client.Camera; + +/** + * The camera's angles for this frame, after vanilla placed the camera and before it renders, as + * the loader module hands them to {@link ClientHooks#computeCameraAngles}. Forge and NeoForge + * back this with their viewport event; Fabric with a mixin at the same spot. + */ +public interface CameraAngles { + Camera camera(); + + float partialTick(); + + float yaw(); + + float pitch(); + + float roll(); + + void setYaw(float yaw); + + void setPitch(float pitch); + + void setRoll(float roll); +} From bb460303b28c6735de3990ca321ed1ed4a3a3f2d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:11 +0800 Subject: [PATCH 0915/1461] Add client/ClientContentReload.java to the 1.21.5 shared sources --- .../mcheli/client/ClientContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java new file mode 100644 index 00000000..70bcc23e --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.CreativeModeTab; + +/** + * Makes the client show the content it now holds: after the server's pack list was applied, which + * also happens after {@code /mcheli reload}. + */ +final class ClientContentReload { + private ClientContentReload() { + } + + /** Drops cached models and textures so edited and new files load, and rebuilds the creative tabs. */ + static void refresh() { + Minecraft mc = Minecraft.getInstance(); + MeshCache.INSTANCE.clear(); + RuntimeAssets.refreshTextures(); + if (mc.level != null && mc.player != null) { + boolean operator = mc.options.operatorItemsTab().get() && mc.player.canUseGameMasterBlocks(); + CreativeModeTab.ItemDisplayParameters params = + new CreativeModeTab.ItemDisplayParameters(mc.level.enabledFeatures(), operator, mc.level.registryAccess()); + ModCreativeTabs.all().forEach(tab -> tab.buildContents(params)); + } + } +} From cced747c80ce1f11d20d6d55fdc5c60245954b47 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:12 +0800 Subject: [PATCH 0916/1461] Add client/ClientHooks.java to the 1.21.5 shared sources --- .../mcheli/client/ClientHooks.java | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java new file mode 100644 index 00000000..96d32987 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -0,0 +1,330 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.client.hud.HudScripts; +import com.jnhprojects.mcheli.client.render.A10Renderer; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import com.jnhprojects.mcheli.client.render.ChainRenderer; +import com.jnhprojects.mcheli.client.render.DraftingTableRenderer; +import com.jnhprojects.mcheli.client.render.EquipmentRenderer; +import com.jnhprojects.mcheli.client.render.FlareRenderer; +import com.jnhprojects.mcheli.client.render.GltdRenderer; +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.render.ParachuteRenderer; +import com.jnhprojects.mcheli.client.render.ProjectileRenderer; +import com.jnhprojects.mcheli.client.render.RiderPose; +import com.jnhprojects.mcheli.client.render.ThrowableRenderer; +import com.jnhprojects.mcheli.client.render.UavStationRenderer; +import com.jnhprojects.mcheli.client.screen.DraftingTableScreen; +import com.jnhprojects.mcheli.client.screen.TeamManagerScreen; +import com.jnhprojects.mcheli.client.screen.UavStationScreen; +import com.jnhprojects.mcheli.client.screen.VehicleScreen; +import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; +import com.jnhprojects.mcheli.client.workshop.WorkshopClient; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.team.Markers; +import com.jnhprojects.mcheli.team.TeamManager; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.NoopRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import java.util.List; +import java.util.Map; + +/** + * Client side counterpart of {@code CommonHooks}: what the mod does on client events. The loader + * module calls these from its own events. Kept out of the shared entry point so dedicated servers + * never load these classes. + */ +public final class ClientHooks { + /** The weapon HUD's layer, drawn above the vanilla HUD. */ + public static final ResourceLocation HUD_LAYER = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "weapons"); + + private static boolean shaderProgramsAssigned; + + private ClientHooks() { + } + + /** Loads a post effect into the game renderer, whose own method is private on 1.21.5. */ + public interface PostEffects { + void load(ResourceLocation effect); + } + + private static PostEffects postEffects = effect -> { + }; + + static PostEffects postEffects() { + return postEffects; + } + + /** During mod construction, on a client only. */ + public static void init(PostEffects effects) { + postEffects = effects; + ClientConfig.load(Platform.get().configDir()); + ClientAccess.install(() -> Minecraft.getInstance().player, ClientPacks.INSTANCE); + } + + /** How a loader registers an entity renderer. */ + public interface EntityRenderers { + void register(EntityType type, EntityRendererProvider provider); + } + + public static void registerEntityRenderers(EntityRenderers renderers) { + renderers.register(ModEntities.HELICOPTER.get(), AircraftRenderer::new); + renderers.register(ModEntities.PLANE.get(), AircraftRenderer::new); + renderers.register(ModEntities.TANK.get(), AircraftRenderer::new); + renderers.register(ModEntities.VEHICLE.get(), AircraftRenderer::new); + renderers.register(ModEntities.BULLET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.BOMB.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AA_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AS_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AT_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TV_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TORPEDO.get(), ProjectileRenderer::new); + renderers.register(ModEntities.MARKER_ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.DISPENSED_ITEM.get(), ProjectileRenderer::new); + renderers.register(ModEntities.FLARE.get(), FlareRenderer::new); + renderers.register(ModEntities.A10.get(), A10Renderer::new); + renderers.register(ModEntities.PARACHUTE.get(), ParachuteRenderer::new); + renderers.register(ModEntities.THROWABLE.get(), ThrowableRenderer::new); + renderers.register(ModEntities.GLTD.get(), GltdRenderer::new); + renderers.register(ModEntities.CHAIN.get(), ChainRenderer::new); + renderers.register(ModEntities.CONTAINER.get(), context -> new EquipmentRenderer<>(context, "container", 0.2F, 0xFFBFBFBF, 1.0F)); + renderers.register(ModEntities.UAV_STATION.get(), UavStationRenderer::new); + renderers.register(ModEntities.RAPPEL.get(), NoopRenderer::new); + } + + /** How a loader registers a block entity renderer. */ + public interface BlockEntityRenderers { + void register(BlockEntityType type, BlockEntityRendererProvider provider); + } + + public static void registerBlockEntityRenderers(BlockEntityRenderers renderers) { + renderers.register(ModBlockEntities.DRAFTING_TABLE.get(), DraftingTableRenderer::new); + } + + /** How a loader registers the screen of a menu: vanilla's own method is not public on all of them. */ + public interface MenuScreenRegistrar { + > void register(MenuType type, + MenuScreens.ScreenConstructor screen); + } + + /** On the main thread during client setup, once our menu types are registered. */ + public static void registerMenuScreens(MenuScreenRegistrar screens) { + screens.register(ModMenus.VEHICLE.get(), VehicleScreen::new); + screens.register(ModMenus.UAV_STATION.get(), UavStationScreen::new); + screens.register(ModMenus.DRAFTING_TABLE.get(), DraftingTableScreen::new); + } + + public static List keyMappings() { + return KeyBindings.ALL; + } + + /** Client resource reload listeners, by the id Fabric wants for each. */ + public static Map reloadListeners() { + return Map.of(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "meshes"), MeshCache.INSTANCE, + ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "hud_scripts"), HudScripts.INSTANCE); + } + + public static void clientTickEnd() { + if (!shaderProgramsAssigned) { + // On the first tick rather than at construction, so Iris has surely loaded by now. + shaderProgramsAssigned = true; + Glow.assignShaderPrograms(); + } + CrewController.onClientTick(); + GltdController.onClientTick(); + RangefinderController.onClientTick(); + LauncherController.onClientTick(); + VehicleCamera.tick(); + WeaponController.onClientTick(); + HudRenderer.tick(); + EngineSoundPlayer.tick(); + TargetMarkers.tick(); + WorkshopClient.tick(); + while (KeyBindings.TEAM_MANAGER.consumeClick()) { + openTeamManager(); + } + } + + /** Operators get the team manager; anyone else is told it is theirs to open. */ + private static void openTeamManager() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.screen != null) { + return; + } + if (TeamManager.mayManage(mc.player)) { + mc.setScreen(new TeamManagerScreen()); + } else { + mc.player.displayClientMessage(Component.translatable("message.mcheli.team.operators_only"), true); + } + } + + /** Tilts the view with the hull and moves the eye into the cockpit or onto a TV missile. */ + public static void computeCameraAngles(CameraAngles angles) { + CrewController.onCameraAngles(angles); + GltdController.onCameraAngles(angles); + } + + /** Sets the camera's rotation, roll included, the way Forge's {@code Camera#setRotation} does. */ + public interface CameraRotation { + void set(Camera camera, float yaw, float pitch, float roll); + } + + /** + * For loaders without Forge's camera angle event: called where Forge fires it, right after + * vanilla placed the camera for this frame. Starts from the camera's own angles and no roll, + * and only touches the rotation when something changed it. + */ + public static void computeCameraAngles(Camera camera, float partialTick, CameraRotation rotation) { + Angles angles = new Angles(camera, partialTick, camera.getYRot(), camera.getXRot()); + computeCameraAngles(angles); + if (angles.yaw != camera.getYRot() || angles.pitch != camera.getXRot() || angles.roll != 0.0F) { + rotation.set(camera, angles.yaw, angles.pitch, angles.roll); + } + } + + private static final class Angles implements CameraAngles { + private final Camera camera; + private final float partialTick; + private float yaw; + private float pitch; + private float roll; + + Angles(Camera camera, float partialTick, float yaw, float pitch) { + this.camera = camera; + this.partialTick = partialTick; + this.yaw = yaw; + this.pitch = pitch; + } + + @Override + public Camera camera() { + return camera; + } + + @Override + public float partialTick() { + return partialTick; + } + + @Override + public float yaw() { + return yaw; + } + + @Override + public float pitch() { + return pitch; + } + + @Override + public float roll() { + return roll; + } + + @Override + public void setYaw(float yaw) { + this.yaw = yaw; + } + + @Override + public void setPitch(float pitch) { + this.pitch = pitch; + } + + @Override + public void setRoll(float roll) { + this.roll = roll; + } + } + + /** The field of view for this frame, narrowed while the player zooms through a sight. */ + public static float fov(float fov) { + return LauncherController.fov(RangefinderController.fov(VehicleCamera.fov(fov))); + } + + /** Whether to skip drawing the first person hand this frame. */ + public static boolean hidesHand() { + return CrewController.hidesHand() || GltdController.hidesHand() || RangefinderController.isScoped() || LauncherController.isRaised(); + } + + /** What happens to a press of the use key. */ + public enum UseKey { + /** Vanilla handles it. */ + PASS, + /** Taken, and the hand does not swing: it is a weapon trigger. */ + CONSUME, + /** Taken, and the hand swings as for a vanilla click: the player boards a vehicle. */ + CONSUME_AND_SWING + } + + public static UseKey onUseKey(InteractionHand hand) { + if (WeaponController.onUseItem() || GltdController.onUseItem()) { + return UseKey.CONSUME; + } + return VehiclePicker.onUseItem(hand) ? UseKey.CONSUME_AND_SWING : UseKey.PASS; + } + + /** The pick block key switches weapons in a seat that has any; true when it was taken. */ + public static boolean onPickBlockKey() { + return WeaponController.onPickBlock(); + } + + /** The partial tick the camera entity is drawn at, for the loaders whose event does not carry it. */ + public static float cameraPartialTicks() { + return Minecraft.getInstance().gameRenderer.getMainCamera().getPartialTickTime(); + } + + public static void beforeRenderLiving(LivingEntityRenderState state, PoseStack pose, float partialTicks) { + RiderPose.beforeRender(state, pose, partialTicks); + } + + public static void drawHud(GuiGraphics graphics, DeltaTracker delta) { + WeaponHud.draw(graphics, delta); + GltdController.drawHud(graphics, delta); + RangefinderController.drawHud(graphics, delta); + LauncherController.drawHud(graphics, delta); + TargetMarkers.draw(graphics, delta); + } + + /** Without the original zip there are no vehicles, so say so where the player will see it. */ + public static void loggedIn(LocalPlayer player) { + MCHeli.legacyContent().problem().ifPresent(message -> player.displayClientMessage(message, false)); + } + + public static void loggedOut() { + ClientPacks.INSTANCE.loggedOut(); + WorkshopClient.loggedOut(); + Markers.clear(); + } +} From e9b686e48fdc2b3de956a73e39f3eb6498b1f265 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:12 +0800 Subject: [PATCH 0917/1461] Add client/ClientPacks.java to the 1.21.5 shared sources --- .../mcheli/client/ClientPacks.java | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java new file mode 100644 index 00000000..7e7d61b1 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java @@ -0,0 +1,338 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackRequestPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import com.jnhprojects.mcheli.platform.Platform; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * The client half of the content pack sync (the server half is {@code PackSync}). + * + *

When the server sends its pack list, every pack this client has neither in its own + * {@code mcheli/packs} nor in {@code mcheli/cache} is requested. Each arrives in chunks, is checked + * against its hash and kept as {@code mcheli/cache/.zip}. Once all are there, the content is + * rebuilt from the client's own import plus exactly the server's packs, in the server's order, so + * the vehicle a player flies (which their client simulates) has the server's definition. + * + *

In single player, and for the host of a LAN game, client and server share one registry, so + * only the client's view of the server's packs is refreshed. + * + *

The cache is capped by {@code packs.cacheSize} in the client config: after a download, the + * packs used longest ago go first. The current server's packs are always kept. + */ +public final class ClientPacks implements ClientAccess.PackReceiver { + public static final ClientPacks INSTANCE = new ClientPacks(); + + private static final Pattern CACHED = Pattern.compile("[0-9a-f]{64}\\.zip"); + + private final Path cache = Platform.get().gameDir().resolve(MCHeli.MOD_ID).resolve("cache"); + private List wanted = List.of(); + private final Map downloads = new LinkedHashMap<>(); + /** Packs this class opened for the current server; closed when the next list replaces them. */ + private List mounted = List.of(); + private int downloaded; + + private ClientPacks() { + } + + @Override + public void onPackList(PackListPacket packet) { + if (Minecraft.getInstance().hasSingleplayerServer()) { + useRoots(MCHeli.packs().active()); + ClientContentReload.refresh(); + return; + } + + wanted = packet.packs(); + downloads.clear(); + downloaded = 0; + long cap = ClientConfig.packCacheBytes(); + List request = new ArrayList<>(); + long bytes = 0; + for (PackListPacket.Entry entry : wanted) { + if (find(entry) != null) { + continue; + } + if (entry.size() > cap || entry.size() <= 0) { + say(Component.translatable("message.mcheli.pack.too_large", entry.id(), mb(entry.size()), cap >> 20)); + continue; + } + downloads.put(entry.hash(), new Download(entry)); + request.add(entry.hash()); + bytes += entry.size(); + } + + if (request.isEmpty()) { + apply(); + } else { + say(Component.translatable("message.mcheli.pack.downloading", request.size(), mb(bytes))); + ModNetwork.sendToServer(new PackRequestPacket(request)); + } + } + + @Override + public void onPackData(PackDataPacket packet) { + Download download = downloads.get(packet.hash()); + if (download == null) { + return; + } + if (!download.accept(packet)) { + fail(download, "the server sent a chunk out of order"); + return; + } + if (!download.complete()) { + return; + } + downloads.remove(packet.hash()); + try { + store(download); + downloaded++; + } catch (IOException | PackException e) { + fail(download, e.getMessage()); + return; + } + if (downloads.isEmpty()) { + apply(); + } + } + + @Override + public void onPackSound(PackSoundPacket packet) { + Minecraft.getInstance().getSoundManager().play( + new OneShotSound(packet.sound(), packet.x(), packet.y(), packet.z(), packet.volume(), packet.pitch())); + } + + /** The packs the server sent or named, for the package browser. Empty when hosting the game. */ + public List serverPacks() { + return mounted; + } + + /** Leaving a server: its packs are no longer needed. */ + void loggedOut() { + downloads.clear(); + wanted = List.of(); + RuntimeAssets.setRoots(List.of()); + close(mounted); + mounted = List.of(); + } + + private void fail(Download download, String reason) { + downloads.remove(download.entry.hash()); + MCHeli.LOGGER.error("Content pack {} could not be downloaded: {}", download.entry.id(), reason); + say(Component.translatable("message.mcheli.pack.failed", download.entry.id(), reason)); + if (downloads.isEmpty()) { + apply(); + } + } + + /** Writes a finished download to the cache once its content matches the hash it was listed with. */ + private void store(Download download) throws IOException, PackException { + String hash = download.entry.hash(); + Files.createDirectories(cache); + Path part = cache.resolve(hash + ".download.zip"); + Files.write(part, download.data); + try (ContentPack pack = ContentPack.open(part)) { + if (!pack.hash().equals(hash)) { + throw new PackException("its files do not match the hash the server listed"); + } + } catch (IOException | PackException e) { + Files.deleteIfExists(part); + throw e; + } + Files.move(part, cache.resolve(hash + ".zip"), StandardCopyOption.REPLACE_EXISTING); + } + + /** Rebuilds the content from the import and the server's packs that are now on disk. */ + private void apply() { + List opened = new ArrayList<>(); + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, MCHeli.legacyContent().assets())); + for (PackListPacket.Entry entry : wanted) { + Path file = find(entry); + if (file == null) { + continue; + } + try { + ContentPack pack = ContentPack.open(file); + if (!pack.hash().equals(entry.hash())) { + pack.close(); + MCHeli.LOGGER.warn("Content pack {} changed on disk and no longer matches the server's", file); + continue; + } + opened.add(pack); + sources.add(pack.contentSource()); + if (file.startsWith(cache)) { + Files.setLastModifiedTime(file, FileTime.from(Instant.now())); + } + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not open content pack {}", file, e); + } + } + + ContentRegistry registry = ContentRegistry.load(sources); + MCHeli.content().replaceWith(registry); + List old = mounted; + mounted = List.copyOf(opened); + useRoots(mounted); + close(old); + ClientContentReload.refresh(); + trimCache(); + if (downloaded > 0) { + say(Component.translatable("message.mcheli.pack.ready", downloaded)); + downloaded = 0; + } + } + + /** The pack on disk with the entry's content: one of the player's own packs, or a cached download. */ + private @Nullable Path find(PackListPacket.Entry entry) { + for (ContentPack own : MCHeli.packs().active()) { + if (own.hash().equals(entry.hash())) { + return own.source(); + } + } + Path cached = cache.resolve(entry.hash() + ".zip"); + return Files.isRegularFile(cached) ? cached : null; + } + + /** Deletes the cached packs used longest ago until the cache fits its cap. */ + private void trimCache() { + Set keep = new HashSet<>(); + for (PackListPacket.Entry entry : wanted) { + keep.add(entry.hash() + ".zip"); + } + List files; + try (Stream list = Files.list(cache)) { + files = list.filter(p -> CACHED.matcher(p.getFileName().toString()).matches()) + .sorted(Comparator.comparing(ClientPacks::lastUsed)) + .toList(); + } catch (IOException e) { + return; + } + long total = 0; + for (Path file : files) { + total += size(file); + } + long cap = ClientConfig.packCacheBytes(); + for (Path file : files) { + if (total <= cap) { + break; + } + if (keep.contains(file.getFileName().toString())) { + continue; + } + try { + long size = size(file); + Files.delete(file); + total -= size; + MCHeli.LOGGER.info("Removed {} from the content pack cache", file.getFileName()); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not remove {} from the content pack cache", file, e); + } + } + } + + private static FileTime lastUsed(Path file) { + try { + return Files.getLastModifiedTime(file); + } catch (IOException e) { + return FileTime.fromMillis(0); + } + } + + private static long size(Path file) { + try { + return Files.size(file); + } catch (IOException e) { + return 0; + } + } + + /** The pack loaded last overrides the others, so it is searched first. */ + private static void useRoots(List packs) { + List roots = new ArrayList<>(); + for (ContentPack pack : packs) { + roots.addFirst(pack.assets()); + } + RuntimeAssets.setRoots(roots); + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } + + private static void say(Component message) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null) { + mc.player.displayClientMessage(message, false); + } else { + MCHeli.LOGGER.info(message.getString()); + } + } + + private static String mb(long bytes) { + return "%.1f".formatted(bytes / 1048576.0); + } + + /** One pack arriving from the server. */ + private static final class Download { + private final PackListPacket.Entry entry; + private byte @Nullable [] data; + private int received; + + Download(PackListPacket.Entry entry) { + this.entry = entry; + } + + boolean accept(PackDataPacket packet) { + if (packet.total() != entry.size() || packet.offset() != received + || received + packet.data().length > entry.size()) { + return false; + } + if (data == null) { + data = new byte[entry.size()]; + } + System.arraycopy(packet.data(), 0, data, received, packet.data().length); + received += packet.data().length; + return true; + } + + boolean complete() { + return data != null && received == data.length; + } + } +} From 89c6fc88c52b6298c44302d5e0ebe8c55505144d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:12 +0800 Subject: [PATCH 0918/1461] Add client/CrewController.java to the 1.21.5 shared sources --- .../mcheli/client/CrewController.java | 482 ++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/CrewController.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/CrewController.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/CrewController.java new file mode 100644 index 00000000..32af6770 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/CrewController.java @@ -0,0 +1,482 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.CameraPosition; +import com.jnhprojects.mcheli.content.aircraft.SeatDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PilotStatePacket; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Input; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +/** + * Turns local player input into vehicle control, for whichever seat the player occupies. + * + *

Keys are sampled once per client tick. The mouse is handled every frame, the way the + * original did it: for helicopters and planes, vanilla first turns the player's view, then the + * change is fed to the aircraft and the view is snapped back onto the aircraft's nose. Everyone + * else looks around freely, within the limits of their seat, and turrets follow their view. + * + *

Gunner seats and camera positions move the first person eye to the point the definition + * gives, which is how the original's camera entity behaved. Every other seat keeps its eye above + * the seat along the hull's up axis, so it tilts with the vehicle. + */ +final class CrewController { + /** Two presses of jump within this many ticks eject the player. */ + private static final int EJECT_WINDOW = 10; + + private static boolean jumpWasDown; + private static int lastJumpPress = -100; + private static int ticks; + private static boolean tracking; + private static float lastYaw; + private static float lastPitch; + + private static @Nullable AircraftEntity viewVehicle; + private static int viewSeat = -1; + private static int cameraIndex; + private static boolean gunnerSight; + + private static @Nullable MethodHandle setCameraPosition; + private static boolean cameraLookupFailed; + + private CrewController() { + } + + /** + * Like the original, the first person hand stays hidden in a vehicle: always for the pilot, + * and for anyone else who looks through a camera or has a weapon to aim. + */ + static boolean hidesHand() { + LocalPlayer player = Minecraft.getInstance().player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + return false; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return false; + } + VehicleWeapons weapons = vehicle.weapons(); + boolean armed = weapons != null && weapons.selected(seat) >= 0; + return seat == 0 || armed || tvCamera(vehicle, seat) != null || eyePosition(vehicle, def, seat) != null; + } + + private static @Nullable AircraftEntity vehicleOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof AircraftEntity vehicle && vehicle.definition() != null ? vehicle : null; + } + + static void onClientTick() { + ticks++; + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + jumpWasDown = false; + tracking = false; + viewVehicle = null; + viewSeat = -1; + // The GLTD's sight shares the zoom and camera mode; only leaving it resets them. + if (GltdController.gltdOf(player) == null) { + VehicleCamera.reset(); + } + return; + } + int seat = vehicle.seatOf(player); + if (vehicle != viewVehicle || seat != viewSeat) { + // New vehicle or seat: start from the default view. + viewVehicle = vehicle; + viewSeat = seat; + cameraIndex = 0; + gunnerSight = false; + tracking = false; + VehicleCamera.reset(); + } + + Input keys = player.input.keyPresses; + boolean jumpPressed = keys.jump() && !jumpWasDown; + jumpWasDown = keys.jump(); + boolean pilot = seat == 0 && vehicle.getControllingPassenger() == player; + + if (pilot) { + AircraftEntity.ControlInput input = vehicle.input(); + input.throttleUp = keys.forward(); + input.throttleDown = keys.backward(); + input.left = keys.left(); + input.right = keys.right(); + input.freeLook = isFreeLook(vehicle); + input.brake = vehicle instanceof TankEntity && keys.jump(); + if (input.brake) { + // The hand brake also stops pivot turns once the vehicle has come to rest. + input.throttleUp = false; + input.throttleDown = false; + double dx = vehicle.getX() - vehicle.xo; + double dz = vehicle.getZ() - vehicle.zo; + if (vehicle.visibleThrottle() <= 0.03F && dx * dx + dz * dz < 0.01) { + input.left = false; + input.right = false; + } + } + if (jumpPressed && vehicle instanceof HelicopterEntity heli) { + heli.toggleHovering(); + } + ModNetwork.sendToServer(new PilotStatePacket(vehicle.getId(), vehicle.visibleThrottle(), vehicle.reverse(), + vehicle.roll(), vehicle.isHoveringMode(), input.write())); + } + + // Eject with a double tap, except for helicopter pilots whose jump key toggles hover. + if (jumpPressed && vehicle.canEject(player) && !(pilot && vehicle instanceof HelicopterEntity)) { + if (ticks - lastJumpPress <= EJECT_WINDOW) { + send(vehicle, CrewActionPacket.Action.EJECT); + lastJumpPress = -100; + } else { + lastJumpPress = ticks; + } + } + + while (KeyBindings.SWITCH_SEAT.consumeClick()) { + send(vehicle, CrewActionPacket.Action.SWITCH_SEAT); + } + while (KeyBindings.DROP_CREW.consumeClick()) { + send(vehicle, pilot ? CrewActionPacket.Action.DROP_CREW : CrewActionPacket.Action.JUMP_OUT); + } + while (KeyBindings.SWITCH_CAMERA.consumeClick()) { + switchCamera(vehicle, seat); + } + while (KeyBindings.LANDING_GEAR.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.LANDING_GEAR); + } + } + while (KeyBindings.FOLD.consumeClick()) { + if (looksThroughSight(vehicle, seat)) { + VehicleCamera.onZoomKey(player, vehicle); + } else if (pilot) { + send(vehicle, CrewActionPacket.Action.FOLD); + } + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + // As in the original, the key works the search light in vehicles that have one. + if (vehicle.canSwitchSearchLight(player)) { + send(vehicle, CrewActionPacket.Action.SEARCH_LIGHT); + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 1.0F); + } else { + VehicleCamera.onCameraModeKey(player, vehicle); + } + } + while (KeyBindings.VEHICLE_SCREEN.consumeClick()) { + if (!vehicle.isDestroyed()) { + send(vehicle, CrewActionPacket.Action.VEHICLE_SCREEN); + } + } + while (KeyBindings.VTOL.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.VTOL); + } + } + while (KeyBindings.RACK_LOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_LOAD); + } + } + while (KeyBindings.RACK_UNLOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_UNLOAD); + } + } + } + + /** + * Whether this seat sees through a sight that can zoom: a camera position or gunner sight, the + * pilot in gunner mode, or the pilot of a UAV. The zoom key folds the wings everywhere else. + */ + private static boolean looksThroughSight(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount() || def.cameraZoom <= 1) { + return false; + } + return eyePosition(vehicle, def, seat) != null || vehicle.isGunnerMode() || def.uav; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + /** Pilot cycles the camera positions; gunners that may leave their sight toggle it. */ + private static void switchCamera(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount()) { + return; + } + SeatDefinition s = def.seats.get(seat); + if (seat == 0) { + switchPilotView(vehicle, def); + } else if (s.gunner() && s.switchGunner()) { + gunnerSight = !gunnerSight; + } + } + + /** + * The pilot's switch mode key, after {@code KeySwitchMode}: leave gunner mode, otherwise step + * through the extra camera positions, otherwise enter gunner mode where the vehicle allows it, + * otherwise go to the first extra camera. Unlike the original, leaving gunner mode never + * depends on the plane still meeting the conditions for entering it. + */ + private static void switchPilotView(AircraftEntity vehicle, AircraftDefinition def) { + int cameras = def.cameraPositions.size(); + if (vehicle.isGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + cameraIndex = 0; + } else if (cameraIndex > 0) { + cameraIndex = (cameraIndex + 1) % cameras; + } else if (vehicle.canSwitchGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + } else if (cameras >= 2) { + cameraIndex = 1; + } + } + + /** + * Free look for the pilot. Helicopters and planes look around while the key is held; tanks + * start from their {@code DefaultFreelook} setting and the key swaps it while held. + */ + private static boolean isFreeLook(AircraftEntity vehicle) { + boolean held = KeyBindings.FREE_LOOK.isDown(); + if (vehicle instanceof TankEntity) { + AircraftDefinition def = vehicle.definition(); + return def != null && def.defaultFreelook != held; + } + return held; + } + + static void onCameraAngles(CameraAngles event) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null || mc.isPaused()) { + return; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return; + } + float partialTicks = event.partialTick(); + boolean firstPerson = mc.options.getCameraType().isFirstPerson(); + + if (seat == 0 && vehicle.steersWithMouse() && !vehicle.isGunnerMode() && vehicle.getControllingPassenger() == player) { + steerWithMouse(vehicle, player, event, partialTicks, firstPerson); + } else { + tracking = false; + constrainView(vehicle, def, def.seats.get(seat), seat, player); + } + + TvMissileEntity missile = tvCamera(vehicle, seat); + if (!(firstPerson && missile != null)) { + // Every seat of every kind tilts with the hull, measured along the way the rider looks. + event.setRoll(AircraftRotation.cameraRoll(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), + vehicle.roll(partialTicks), event.yaw(), event.pitch())); + } + if (firstPerson && missile != null) { + placeCamera(event.camera(), missile.getPosition(partialTicks)); + } else if (firstPerson) { + Vector3 eye = eyePosition(vehicle, def, seat); + moveCamera(event.camera(), vehicle, def, eye != null ? eye : seatEye(vehicle, def, seat, player), seat, partialTicks); + } + } + + /** + * The normal eye, in hull space. Vanilla puts it straight above the rider in world space, + * which leaves the head outside the cockpit once the hull banks or pitches; measured along the + * hull's own up axis it stays where the rider model's head is drawn. + */ + private static Vector3 seatEye(AircraftEntity vehicle, AircraftDefinition def, int seat, LocalPlayer player) { + Vector3 pos = def.seats.get(seat).position(); + double up = player.getEyeHeight() - player.getVehicleAttachmentPoint(vehicle).y; + return new Vector3(pos.x(), pos.y() + up, pos.z()); + } + + /** The phase one pilot model: mouse movement becomes aircraft rotation, the view stays on the nose. */ + private static void steerWithMouse(AircraftEntity vehicle, LocalPlayer player, CameraAngles event, + float partialTicks, boolean firstPerson) { + boolean freeLook = KeyBindings.FREE_LOOK.isDown(); + boolean ownPitch = vehicle.isHovering(); + if (!freeLook) { + if (tracking) { + float dYaw = Mth.wrapDegrees(player.getYRot() - lastYaw); + float dPitch = player.getXRot() - lastPitch; + vehicle.applyPilotLook(dYaw, dPitch); + HudRenderer.onPilotLook(dYaw, dPitch); + } + lockView(player, vehicle.getYRot(), vehicle.yRotO, ownPitch ? null : vehicle.getXRot(), vehicle.xRotO); + lastYaw = player.getYRot(); + lastPitch = player.getXRot(); + tracking = true; + } else { + // Re-acquire the nose smoothly once free look is released. + tracking = false; + } + if (firstPerson && !freeLook) { + event.setYaw(vehicle.getYRot(partialTicks)); + if (!ownPitch) { + event.setPitch(vehicle.getXRot(partialTicks)); + } + } + } + + /** Seat limits for everyone who does not fly with the mouse. */ + private static void constrainView(AircraftEntity vehicle, AircraftDefinition def, SeatDefinition seat, int index, LocalPlayer player) { + boolean freeLookHeld = KeyBindings.FREE_LOOK.isDown(); + if (seat.fixedRotation() && !freeLookHeld) { + lockView(player, vehicle.getYRot() + seat.fixedYaw(), vehicle.yRotO + seat.fixedYaw(), + vehicle.getXRot() + seat.fixedPitch(), vehicle.xRotO + seat.fixedPitch()); + return; + } + boolean pilot = index == 0; + if (pilot && vehicle instanceof TankEntity && !isFreeLook(vehicle)) { + lockView(player, vehicle.getYRot(), vehicle.yRotO, vehicle.getXRot(), vehicle.xRotO); + return; + } + float min; + float max; + if (pilot && (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity)) { + min = def.minRotationPitch; + max = def.maxRotationPitch; + } else if (seat.gunner() || (pilot && vehicle.isGunnerMode())) { + min = seat.minPitch(); + max = seat.maxPitch(); + } else { + return; + } + // Limits are relative to the hull's tilt along the view direction. + double rel = Math.toRadians(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot())); + float tilt = (float) (vehicle.getXRot() * Math.cos(rel) + vehicle.roll() * Math.sin(rel)); + float pitch = Mth.clamp(player.getXRot(), tilt + min, tilt + max); + pitch = Mth.clamp(pitch, -90.0F, 90.0F); + if (pitch != player.getXRot()) { + player.setXRot(pitch); + player.xRotO = pitch; + } + } + + private static void lockView(LocalPlayer player, float yaw, float yawO, @Nullable Float pitch, float pitchO) { + player.setYRot(yaw); + player.yRotO = yawO; + player.setYHeadRot(yaw); + player.setYBodyRot(yaw); + if (pitch != null) { + player.setXRot(pitch); + player.xRotO = pitchO; + } + } + + /** + * Where the first person eye goes, in hull space, or {@code null} for the normal eye. Mirrors + * the original's choice between the pilot's camera positions and a gunner seat's sight. + */ + private static @Nullable Vector3 eyePosition(AircraftEntity vehicle, AircraftDefinition def, int seat) { + SeatDefinition s = def.seats.get(seat); + boolean sight = s.gunner() && (!s.switchGunner() || gunnerSight); + CameraPosition camera = null; + if (seat == 0) { + if (cameraIndex > 0 && cameraIndex < def.cameraPositions.size()) { + camera = def.cameraPositions.get(cameraIndex); + } else if (def.uav || def.alwaysCameraView || sight || vehicle.isGunnerMode() || vehicle instanceof GroundVehicleEntity) { + camera = def.cameraPositions.getFirst(); + } + } else if (sight) { + camera = s.camera() != null && s.camera() != CameraPosition.DEFAULT ? s.camera() : def.cameraPositions.getFirst(); + } + // A camera at the origin means the file never set one; the normal eye is better there. + if (camera == null || camera.position().equals(Vector3.ZERO)) { + return null; + } + return camera.position(); + } + + private static void moveCamera(Camera camera, AircraftEntity vehicle, AircraftDefinition def, Vector3 eye, int seat, float partialTicks) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + SeatDefinition s = def.seats.get(seat); + Vector3 local = s.rotatingSeat() ? vehicle.onTurret(def, eye, partialTicks) : eye; + Vector3f offset = AircraftRotation.toWorld(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), vehicle.roll(partialTicks), + local.x(), local.y(), local.z()); + placeCamera(camera, new Vec3(Mth.lerp(partialTicks, vehicle.xo, vehicle.getX()) + offset.x, + Mth.lerp(partialTicks, vehicle.yo, vehicle.getY()) + offset.y, + Mth.lerp(partialTicks, vehicle.zo, vehicle.getZ()) + offset.z)); + } + + /** + * The TV missile this seat is flying, whose nose camera replaces the eye. Like the original, + * only while the seat has a TV missile selected in its guided mode. + */ + static @Nullable TvMissileEntity tvCamera(AircraftEntity vehicle, int seat) { + TvMissileEntity missile = vehicle.tvMissile(); + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + if (missile == null || set == null || set.definition().type != WeaponType.TV_MISSILE || set.mode() != 0) { + return null; + } + return missile; + } + + static void placeCamera(Camera camera, Vec3 at) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + try { + setter.invoke(camera, at); + } catch (Throwable t) { + MCHeli.LOGGER.warn("Could not move the camera; the first person eye stays where vanilla puts it", t); + setCameraPosition = null; + cameraLookupFailed = true; + } + } + + private static Method accessible(Method method) { + method.setAccessible(true); + return method; + } + + /** {@code Camera.setPosition} is protected. Looked up once; on failure the feature turns itself off. */ + private static @Nullable MethodHandle cameraSetter() { + if (setCameraPosition == null && !cameraLookupFailed) { + try { + setCameraPosition = MethodHandles.lookup().unreflect( + accessible(Camera.class.getDeclaredMethod("setPosition", Vec3.class))); + } catch (Exception e) { + MCHeli.LOGGER.warn("Camera.setPosition is not accessible; the first person eye stays where vanilla puts it", e); + cameraLookupFailed = true; + } + } + return setCameraPosition; + } +} From 7c97db25893074b7060b203cba08ac3b7ce1df83 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:13 +0800 Subject: [PATCH 0919/1461] Add client/GltdController.java to the 1.21.5 shared sources --- .../mcheli/client/GltdController.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/GltdController.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/GltdController.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/GltdController.java new file mode 100644 index 00000000..a8db0832 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/GltdController.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.network.EquipmentActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +/** + * The local player at a GLTD, {@code MCH_ClientGLTDTickHandler} and {@code MCH_GuiGLTD}: the + * view moves to the sight, the zoom and camera mode keys work as in a vehicle's sight, the use + * key calls the A-10 on the spot under the cross, and a small readout says when it can be called + * again. + */ +final class GltdController { + /** The original zoomed smoothly up to ten; the vehicle sights double each press. */ + private static final int MAX_ZOOM = 8; + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int CROSS = 0xC0FF4040; + + /** Edge of the use key, so holding it calls one strike rather than one per tick. */ + private static boolean useWasDown; + + private GltdController() { + } + + static @Nullable GltdEntity gltdOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof GltdEntity gltd ? gltd : null; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + GltdEntity gltd = gltdOf(player); + if (gltd == null) { + useWasDown = false; + return; + } + while (KeyBindings.FOLD.consumeClick()) { + VehicleCamera.zoomStep(player, MAX_ZOOM); + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + VehicleCamera.nextMode(player, on -> send(gltd, + on ? EquipmentActionPacket.Action.NIGHT_VISION_ON : EquipmentActionPacket.Action.NIGHT_VISION_OFF)); + } + boolean useDown = mc.screen == null && mc.options.keyUse.isDown(); + if (useDown && !useWasDown) { + send(gltd, EquipmentActionPacket.Action.CALL_STRIKE); + } + useWasDown = useDown; + } + + /** The use key belongs to the sight while sitting at a GLTD. */ + static boolean onUseItem() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + /** First person looks out of the sight rather than from behind the tripod. */ + static void onCameraAngles(CameraAngles angles) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd != null && mc.options.getCameraType().isFirstPerson()) { + CrewController.placeCamera(angles.camera(), gltd.sightPosition(angles.partialTick())); + } + } + + static boolean hidesHand() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + static void drawHud(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd == null || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int cx = g.guiWidth() / 2; + int cy = g.guiHeight() / 2; + g.fill(cx - 12, cy, cx - 3, cy + 1, CROSS); + g.fill(cx + 4, cy, cx + 13, cy + 1, CROSS); + g.fill(cx, cy - 12, cx + 1, cy - 3, CROSS); + g.fill(cx, cy + 4, cx + 1, cy + 13, CROSS); + + Font font = mc.font; + int x = cx + 20; + int y = cy + 20; + int wait = gltd.waitTicks(); + if (wait > 0) { + g.drawString(font, Component.translatable("hud.mcheli.gltd.wait", (wait + 19) / 20), x, y, AMBER); + } else { + g.drawString(font, Component.translatable("hud.mcheli.gltd.ready"), x, y, WHITE); + } + g.drawString(font, Component.translatable("hud.mcheli.zoom", Math.round(VehicleCamera.zoom())), x, y + 10, WHITE); + } + + private static void send(GltdEntity gltd, EquipmentActionPacket.Action action) { + ModNetwork.sendToServer(new EquipmentActionPacket(gltd.getId(), action)); + } +} From f48d29b78408a0deb0a90613ee3503fec1d827b3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:13 +0800 Subject: [PATCH 0920/1461] Add client/KeyBindings.java to the 1.21.5 shared sources --- .../mcheli/client/KeyBindings.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java new file mode 100644 index 00000000..dc594e26 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.KeyMapping; + +import java.util.List; + +/** + * Mod key bindings. Throttle, steering, hover, brake and eject reuse the vanilla movement and jump + * keys (W/S, A/D and Space by default), which matches the original defaults. Firing uses the + * vanilla use key and the pick block button switches weapons, the right and middle mouse buttons + * of the original. The rest keep the original's letters where vanilla leaves them free. + */ +public final class KeyBindings { + public static final String CATEGORY = "key.category." + MCHeli.MOD_ID + ".main"; + + /** Hold to look around without steering. The original used Left Ctrl, which now clashes with sprint. */ + public static final KeyMapping FREE_LOOK = new KeyMapping("key.mcheli.free_look", InputConstants.KEY_LALT, CATEGORY); + /** Cycles the pilot's camera positions, or toggles the gunner sight in seats that allow it. */ + public static final KeyMapping SWITCH_CAMERA = new KeyMapping("key.mcheli.switch_camera", InputConstants.KEY_H, CATEGORY); + public static final KeyMapping LANDING_GEAR = new KeyMapping("key.mcheli.landing_gear", InputConstants.KEY_B, CATEGORY); + /** Zooms through a sight, as the original Z key did; otherwise folds hatches and wings, sweeps variable geometry wings. */ + public static final KeyMapping FOLD = new KeyMapping("key.mcheli.fold", InputConstants.KEY_Z, CATEGORY); + /** The original used F, which vanilla now uses to swap hands. */ + public static final KeyMapping VTOL = new KeyMapping("key.mcheli.vtol", InputConstants.KEY_V, CATEGORY); + public static final KeyMapping SWITCH_SEAT = new KeyMapping("key.mcheli.switch_seat", InputConstants.KEY_R, CATEGORY); + public static final KeyMapping RACK_LOAD = new KeyMapping("key.mcheli.rack_load", InputConstants.KEY_J, CATEGORY); + public static final KeyMapping RACK_UNLOAD = new KeyMapping("key.mcheli.rack_unload", InputConstants.KEY_U, CATEGORY); + /** The second weapon switch key of the original; the first is the vanilla pick block button. */ + public static final KeyMapping SWITCH_WEAPON = new KeyMapping("key.mcheli.switch_weapon", InputConstants.KEY_G, CATEGORY); + public static final KeyMapping WEAPON_MODE = new KeyMapping("key.mcheli.weapon_mode", InputConstants.KEY_X, CATEGORY); + /** The original used V, which VTOL took over. */ + public static final KeyMapping FLARE = new KeyMapping("key.mcheli.flare", InputConstants.KEY_C, CATEGORY); + /** Fuel and parachute slots, reloading and the cargo chest. The original used R, which switches seats here. */ + public static final KeyMapping VEHICLE_SCREEN = new KeyMapping("key.mcheli.vehicle_screen", InputConstants.KEY_I, CATEGORY); + /** Night vision, thermal view and back in vehicles with {@code EnableNightVision}. The original used C, now the flare key. */ + public static final KeyMapping CAMERA_MODE = new KeyMapping("key.mcheli.camera_mode", InputConstants.KEY_K, CATEGORY); + /** The pilot lets the troops down; anyone else aboard slides down a rope or jumps. The original used Y for the pilot's part. */ + public static final KeyMapping DROP_CREW = new KeyMapping("key.mcheli.drop_crew", InputConstants.KEY_Y, CATEGORY); + /** Operators open the team manager, as the original's multiplayer manager key M did. */ + public static final KeyMapping TEAM_MANAGER = new KeyMapping("key.mcheli.team_manager", InputConstants.KEY_M, CATEGORY); + + public static final List ALL = List.of(FREE_LOOK, SWITCH_CAMERA, LANDING_GEAR, FOLD, VTOL, SWITCH_SEAT, RACK_LOAD, RACK_UNLOAD, + SWITCH_WEAPON, WEAPON_MODE, FLARE, VEHICLE_SCREEN, CAMERA_MODE, DROP_CREW, TEAM_MANAGER); + + private KeyBindings() { + } +} From 254a322d65d9f985b9a0fe0499d6bd1d35b60b74 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:14 +0800 Subject: [PATCH 0921/1461] Add client/LauncherController.java to the 1.21.5 shared sources --- .../mcheli/client/LauncherController.java | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java new file mode 100644 index 00000000..71044604 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java @@ -0,0 +1,239 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.network.LauncherPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The Stinger and Javelin in the player's hands, {@code MCH_ClientLightWeaponTickHandler} and + * {@code MCH_GuiLightWeapon}. Raised, the launcher's sight covers the view and its guidance locks + * onto what the cross holds; the attack key fires once the lock is complete. Z steps the zoom, X + * the Javelin's direct and top attack modes. An empty launcher held down reloads itself from a + * missile in the inventory. + */ +final class LauncherController { + /** Ticks of raising before the sight and the lock start, as in the original. */ + private static final int RAISE_TICKS = 10; + private static final int RELOAD_TICKS = 40; + /** Top attack climbs first and needs this much room, measured flat. */ + private static final double TOP_ATTACK_MIN_DISTANCE = 40.0; + private static final int GREEN = 0xFF2AD848; + private static final int RED = 0xFFDF0808; + private static final int WHITE = 0xFFFFFFFF; + + private static @Nullable String weapon; + private static @Nullable Guidance guidance; + private static int zoomIndex; + private static int mode; + private static int reloadTicks; + private static int lockSoundTicks; + private static boolean attackWasDown; + + private LauncherController() { + } + + private static ItemStack launcher(@Nullable LocalPlayer player) { + if (player == null || !LauncherItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getItemInHand(InteractionHand.MAIN_HAND); + return stack.getItem() instanceof LauncherItem ? stack : ItemStack.EMPTY; + } + + /** Whether the player holds a launcher up to the eye. */ + static boolean isRaised() { + LocalPlayer player = Minecraft.getInstance().player; + ItemStack stack = launcher(player); + return !stack.isEmpty() && player.isUsingItem() && player.getUseItem() == stack && player.getTicksUsingItem() > RAISE_TICKS; + } + + private static @Nullable WeaponDefinition definition(ItemStack stack) { + return MCHeli.content().weapon(((LauncherItem) stack.getItem()).weapon()); + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = launcher(player); + WeaponDefinition def = stack.isEmpty() ? null : definition(stack); + if (def == null) { + weapon = null; + guidance = null; + reloadTicks = 0; + attackWasDown = false; + return; + } + String name = ((LauncherItem) stack.getItem()).weapon(); + if (!name.equals(weapon) || guidance == null) { + weapon = name; + boolean air = def.type != WeaponType.AT_MISSILE; + guidance = new Guidance(air, !air, def.ridableOnly, def.lockTime, e -> e != player && !e.isAlliedTo(player)); + zoomIndex = 0; + mode = 0; + } + boolean raised = isRaised(); + boolean attackDown = raised && mc.screen == null && mc.options.keyAttack.isDown(); + boolean attackPressed = attackDown && !attackWasDown; + attackWasDown = attackDown; + + if (LauncherItem.isLoaded(stack)) { + reloadTicks = 0; + if (raised) { + if (guidance.lock(player, player.getEyePosition())) { + lockSoundTicks = 0; + } + playLockSound(player); + } else { + guidance.clear(); + } + } else { + guidance.clear(); + reload(player, stack); + } + guidance.tick(); + + if (!raised) { + return; + } + while (KeyBindings.FOLD.consumeClick()) { + if (def.zoom.length > 1) { + zoomIndex = (zoomIndex + 1) % def.zoom.length; + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + if (def.modeNum > 1) { + mode = (mode + 1) % def.modeNum; + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 0.9F); + } + } + if (attackPressed) { + Entity target = guidance.lastLocked(); + if (LauncherItem.isLoaded(stack) && guidance.isLocked() && target != null && canFire(player, target)) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.FIRE, target.getId(), mode)); + guidance.clear(); + } else { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + } + + /** Top attack needs room to climb and dive. */ + private static boolean canFire(LocalPlayer player, @Nullable Entity target) { + if (mode == 0 || target == null) { + return true; + } + double dx = target.getX() - player.getX(); + double dz = target.getZ() - player.getZ(); + return Math.sqrt(dx * dx + dz * dz) >= TOP_ATTACK_MIN_DISTANCE; + } + + /** The tone quickens as the lock builds. */ + private static void playLockSound(LocalPlayer player) { + if (guidance == null || guidance.lockCount() <= 0) { + lockSoundTicks = 0; + return; + } + if (lockSoundTicks > 0) { + lockSoundTicks--; + return; + } + lockSoundTicks = Math.max(2, (int) (7 * (1.0 - (double) guidance.lockCount() / guidance.lockCountMax()))); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + + /** Held down and empty, with a missile to hand: two seconds to load it. */ + private static void reload(LocalPlayer player, ItemStack stack) { + LauncherItem item = (LauncherItem) stack.getItem(); + boolean hasMissile = player.getAbilities().instabuild || player.getInventory().findSlotMatchingItem(new ItemStack(item.missile())) >= 0; + if (!hasMissile || player.isUsingItem()) { + reloadTicks = 0; + return; + } + reloadTicks++; + if (reloadTicks == 10) { + player.level().playLocalSound(player, ModSounds.get("fim92_reload"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (reloadTicks == RELOAD_TICKS) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.RELOAD, 0, 0)); + } + } + + static float fov(float fov) { + if (!isRaised()) { + return fov; + } + WeaponDefinition def = definition(launcher(Minecraft.getInstance().player)); + if (def == null || def.zoom.length == 0) { + return fov; + } + float zoom = def.zoom[Math.min(zoomIndex, def.zoom.length - 1)]; + return zoom > 1.0F ? fov / zoom : fov; + } + + static void drawHud(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (!mc.options.getCameraType().isFirstPerson()) { + return; + } + ItemStack stack = launcher(player); + if (stack.isEmpty()) { + return; + } + Font font = mc.font; + if (!isRaised()) { + if (!LauncherItem.isLoaded(stack) && reloadTicks > 0) { + g.drawString(font, Component.translatable("hud.mcheli.launcher.reloading"), g.guiWidth() / 2 - 30, g.guiHeight() / 2 + 20, WHITE); + } + return; + } + WeaponDefinition def = definition(stack); + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + String texture = def != null && def.type == WeaponType.AT_MISSILE ? "javelin" : "stinger"; + g.blit(RenderType::guiTextured, ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + texture + ".png"), + (w - size) / 2, (h - size) / 2, 0.0F, 0.0F, size, size, 256, 256, 256, 256); + + int x = w / 2 + 20; + int y = h / 2 + 20; + if (!LauncherItem.isLoaded(stack)) { + g.drawString(font, Component.translatable("hud.mcheli.launcher.empty"), x, y, RED); + } else if (guidance != null && guidance.isLocked()) { + Entity target = guidance.lastLocked(); + g.drawString(font, Component.translatable("hud.mcheli.locked"), x, y, canFire(player, target) ? GREEN : RED); + if (target != null) { + g.drawString(font, String.format("[%.0f m]", target.distanceTo(player)), x, y + 10, GREEN); + } + } else if (guidance != null && guidance.lockCount() > 0) { + g.drawString(font, Component.translatable("hud.mcheli.locking", guidance.lockCount() * 100 / Math.max(guidance.lockCountMax(), 1)), x, y, WHITE); + } + if (def != null && def.modeNum > 1) { + g.drawString(font, Component.translatable(mode == 0 ? "hud.mcheli.launcher.direct" : "hud.mcheli.launcher.top_attack"), x, y + 20, WHITE); + } + if (def != null && def.zoom.length > 0) { + g.drawString(font, Component.translatable("hud.mcheli.zoom", String.format("%.1f", def.zoom[Math.min(zoomIndex, def.zoom.length - 1)])), + x, y + 30, WHITE); + } + } +} From 0872c8bab5d41c8f684950a343ce5aa1232c5484 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:14 +0800 Subject: [PATCH 0922/1461] Add client/RangefinderController.java to the 1.21.5 shared sources --- .../mcheli/client/RangefinderController.java | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java new file mode 100644 index 00000000..9ec9ed76 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java @@ -0,0 +1,156 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.SpotPacket; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.Locale; + +/** + * Looking through the rangefinder, {@code MCH_ClientToolTickHandler} and {@code MCH_GuiRangeFinder}. + * Once it has been held up for a moment the scope covers the view and reads the range; the zoom + * key and the weapon mode key zoom in and out while held, the flare key picks what to spot, and + * the attack key spots. + */ +final class RangefinderController { + private static final ResourceLocation SCOPE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/rangefinder.png"); + /** Ticks of holding it up before the scope shows, as in the original. */ + private static final int RAISE_TICKS = 8; + private static final int COOLDOWN = 60; + private static final float MIN_ZOOM = 1.2F; + private static final float MAX_ZOOM = 10.0F; + private static final double RANGE = 300.0; + private static final int GREEN = 0xFF10FF20; + private static final int RED = 0xFFFF1010; + private static final int WHITE = 0xFFFFFFFF; + private static final int GREY = 0xFF404040; + + private static float zoom = 2.0F; + private static SpotPacket.Mode mode = SpotPacket.Mode.PLAYERS_AND_VEHICLES; + private static int cooldown; + + private RangefinderController() { + } + + /** The rangefinder the player is looking through, or empty. */ + private static ItemStack scope(LocalPlayer player) { + if (player == null || !player.isUsingItem() || player.getTicksUsingItem() <= RAISE_TICKS || !RangefinderItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getUseItem(); + return stack.getItem() instanceof RangefinderItem ? stack : ItemStack.EMPTY; + } + + static boolean isScoped() { + return !scope(Minecraft.getInstance().player).isEmpty(); + } + + static void onClientTick() { + if (cooldown > 0) { + cooldown--; + } + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty()) { + zoom = 2.0F; + return; + } + if (KeyBindings.FOLD.isDown() && zoom < MAX_ZOOM) { + zoom = Math.min(zoom + zoom / 10.0F, MAX_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 1.0F); + } + if (KeyBindings.WEAPON_MODE.isDown() && zoom > MIN_ZOOM) { + zoom = Math.max(zoom - zoom / 10.0F, MIN_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 0.9F); + } + while (KeyBindings.FLARE.consumeClick()) { + mode = SpotPacket.Mode.values()[(mode.ordinal() + 1) % SpotPacket.Mode.values().length]; + // Alone in a world there is no one else to spot, as the original reasoned. + boolean alone = mc.hasSingleplayerServer() && !mc.getSingleplayerServer().isPublished(); + if (alone && mode == SpotPacket.Mode.PLAYERS_AND_VEHICLES) { + mode = SpotPacket.Mode.MOBS; + } + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 0.9F); + } + if (mc.options.keyAttack.isDown() && cooldown == 0) { + if (mode != SpotPacket.Mode.MARK_POINT && RangefinderItem.charges(stack) <= 0) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 1.0F, 1.0F); + } else { + cooldown = COOLDOWN; + ModNetwork.sendToServer(new SpotPacket(mode)); + } + } + } + + static float fov(float fov) { + return isScoped() ? fov / zoom : fov; + } + + static void drawHud(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty() || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + g.blit(RenderType::guiTextured, SCOPE, (w - size) / 2, (h - size) / 2, 0.0F, 0.0F, size, size, 256, 256, 256, 256); + + // The readouts sit on the scope texture's own labels: lay them out in its 512 wide space, + // centred, as the original did, and scale that with the scope. + float k = size / 512.0F; + g.pose().pushPose(); + g.pose().translate(w / 2.0F, h / 2.0F, 0.0F); + g.pose().scale(k, k, 1.0F); + drawReadouts(g, mc.font, player, stack); + g.pose().popPose(); + } + + /** In scope space: the centre of the view is at 0, 0. */ + private static void drawReadouts(GuiGraphics g, Font font, LocalPlayer player, ItemStack stack) { + int charge = RangefinderItem.charges(stack) * 100 / RangefinderItem.CHARGES; + // Over BATT. + g.drawString(font, String.format("%3d%%", charge), -100, 55, charge > 0 ? GREEN : RED); + if (charge <= 0) { + g.drawString(font, Component.translatable("hud.mcheli.rangefinder.empty"), -40, 55, RED); + } else { + // Over RANGE METERS. + Vec3 eye = player.getEyePosition(); + HitResult hit = player.level().clip(new ClipContext(eye, eye.add(player.getLookAngle().scale(RANGE)), + ClipContext.Block.OUTLINE, ClipContext.Fluid.ANY, player)); + String range = hit.getType() == HitResult.Type.MISS ? "----" : String.format("%4d", Math.round(hit.getLocation().distanceTo(eye))); + g.drawString(font, range, 40, 55, hit.getType() == HitResult.Type.MISS ? RED : GREEN); + } + // The spot cooldown bar, and the zoom above it. + g.fill(-100, 51, -70, 53, GREEN); + g.fill(-100, 51, -100 + cooldown / 2, 53, RED); + g.drawString(font, String.format("x%.1f", zoom), -100, 31, WHITE); + int y = 21; + for (SpotPacket.Mode m : SpotPacket.Mode.values()) { + if (m == mode) { + g.drawString(font, ">", 30, y, WHITE); + } + g.drawString(font, Component.translatable("hud.mcheli.rangefinder." + m.name().toLowerCase(Locale.ROOT)), 40, y, m == mode ? WHITE : GREY); + y += 10; + } + } +} From b57ebc9a00a7479c41afd1d7525733093f863c3b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:15 +0800 Subject: [PATCH 0923/1461] Add client/RuntimeAssets.java to the 1.21.5 shared sources --- .../mcheli/client/RuntimeAssets.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java new file mode 100644 index 00000000..d21a189a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java @@ -0,0 +1,165 @@ +package com.jnhprojects.mcheli.client; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Vehicle textures, models and sounds that can change while the game runs, without a resource + * reload (F3 + T), which would stall the game for every pack loaded and is not available on a + * server at all. + * + *

Lookups try the resource manager first, so resource packs keep the last word, then the + * runtime roots: folders laid out like {@code assets/mcheli} that content can add at any time. + * Content packs and, later, the workshop register their folders here. + *

    + *
  • Textures found only in a runtime root are uploaded as a {@link DynamicTexture} the first + * time a vehicle draws them. {@link #refreshTextures} uploads every vehicle texture again + * from disk, which is how a content reload shows edited textures.
  • + *
  • Models are read through {@link #open} by the mesh cache, which a reload clears.
  • + *
  • Sounds found only in a runtime root are streamed from the file + * ({@link #soundFile}); see {@code RuntimeSoundInstance}.
  • + *
+ */ +public final class RuntimeAssets { + private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeAssets.class); + + /** Searched in order, so the pack loaded last comes first. Replaced as a whole. */ + private static volatile List roots = List.of(); + /** The pack open in the workshop, searched first whether the game has loaded it or not. */ + private static volatile @Nullable Path workshopRoot; + /** Every vehicle texture asked for, and whether we uploaded it ourselves. */ + private static final Map TEXTURES = new ConcurrentHashMap<>(); + + private RuntimeAssets() { + } + + /** + * Sets the folders laid out like {@code assets/mcheli} that are searched after the resource + * packs, the first one first. Content packs set theirs whenever the packs in use change. + */ + public static void setRoots(List folders) { + roots = List.copyOf(folders); + } + + /** Sets the {@code assets/mcheli} folder of the pack open in the workshop, or {@code null} for none. */ + public static void setWorkshopRoot(@Nullable Path folder) { + workshopRoot = folder; + } + + /** Opens an {@code mcheli} asset from the resource packs, or else from a runtime root. */ + public static @Nullable InputStream open(ResourceLocation id) throws IOException { + Optional resource = Minecraft.getInstance().getResourceManager().getResource(id); + if (resource.isPresent()) { + return resource.get().open(); + } + Path file = find(id.getPath()); + return file != null ? Files.newInputStream(file) : null; + } + + /** + * A texture ready to bind. The first request for a texture no resource pack has, but a runtime + * root does, uploads it. Cheap after the first call, so renderers can call it every frame. + */ + public static ResourceLocation texture(ResourceLocation id) { + TEXTURES.computeIfAbsent(id, key -> { + if (Minecraft.getInstance().getResourceManager().getResource(key).isPresent()) { + return false; + } + return upload(key); + }); + return id; + } + + /** + * Uploads every texture vehicles have drawn so far again, read fresh from disk. Resource pack + * textures are replaced too, until the next resource reload loads them the normal way. One that + * no longer exists anywhere, because its pack is gone, is released. + */ + public static void refreshTextures() { + for (Map.Entry entry : TEXTURES.entrySet()) { + boolean ours = upload(entry.getKey()); + if (!ours && entry.getValue()) { + Minecraft.getInstance().getTextureManager().release(entry.getKey()); + } + entry.setValue(ours); + } + } + + /** Uploads one texture again, read fresh from disk: the workshop just replaced its file. */ + public static void refreshTexture(ResourceLocation id) { + TEXTURES.put(id, upload(id)); + } + + private static boolean upload(ResourceLocation id) { + try (InputStream in = open(id)) { + if (in == null) { + return false; + } + NativeImage image = NativeImage.read(in); + Minecraft.getInstance().getTextureManager().register(id, new DynamicTexture(id::toString, image)); + return true; + } catch (IOException e) { + LOGGER.warn("Could not load texture {}", id, e); + return false; + } + } + + /** + * Drops the textures we uploaded, so the texture manager loads them again the normal way. + * Called on resource reload. + */ + public static void clearTextures() { + TEXTURES.forEach((id, ours) -> { + if (ours) { + Minecraft.getInstance().getTextureManager().release(id); + } + }); + TEXTURES.clear(); + } + + /** + * The OGG file for a sound event of ours that no resource pack defines, such as {@code heli} + * from {@code sounds/heli.ogg} in a runtime root. Numbered variants ({@code heli1.ogg}) count. + */ + public static @Nullable Path soundFile(String event) { + Path file = find("sounds/" + event + ".ogg"); + return file != null ? file : find("sounds/" + event + "1.ogg"); + } + + private static @Nullable Path find(String path) { + Path workshop = workshopRoot; + if (workshop != null) { + Path file = inRoot(workshop, path); + if (file != null) { + return file; + } + } + for (Path root : roots) { + Path file = inRoot(root, path); + if (file != null) { + return file; + } + } + return null; + } + + private static @Nullable Path inRoot(Path root, String path) { + Path file = root.resolve(path).normalize(); + return file.startsWith(root) && Files.isRegularFile(file) ? file : null; + } +} From 56131d92616846c8a4f1d70a61f5a1d47c5d6ad1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:15 +0800 Subject: [PATCH 0924/1461] Add client/TargetMarkers.java to the 1.21.5 shared sources --- .../mcheli/client/TargetMarkers.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java new file mode 100644 index 00000000..a7d8f86a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.joml.Vector4f; +import org.jspecify.annotations.Nullable; + +import java.util.Map; + +/** + * {@code MCH_GuiTargetMarker}: the team's spotted targets and marked point on screen. A target + * gets a triangle over its head with its distance; one off screen gets a diamond at the edge on + * its side. The marked point gets a diamond with its distance and coordinates. + */ +final class TargetMarkers { + private static final int TARGET = 0xFFFF4040; + private static final int POINT = 0xFFFFD000; + private static final int EDGE = 12; + + private TargetMarkers() { + } + + static void tick() { + Markers.tick(); + } + + static void draw(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || mc.player == null) { + return; + } + float partialTicks = delta.getGameTimeDeltaPartialTick(false); + for (Map.Entry spotted : Markers.spotted().entrySet()) { + Entity entity = mc.level.getEntity(spotted.getKey()); + if (entity == null || entity == mc.player || !entity.isAlive()) { + continue; + } + Vec3 top = entity.getPosition(partialTicks).add(0.0, entity.getBbHeight() + 0.5, 0.0); + mark(g, mc, top, "â–¼", TARGET, "%.0fm".formatted(mc.player.distanceTo(entity)), null); + } + BlockPos point = Markers.point(); + if (point != null) { + Vec3 at = Vec3.atCenterOf(point).add(0.0, 1.0, 0.0); + String coordinates = "%d %d %d".formatted(point.getX(), point.getY(), point.getZ()); + mark(g, mc, at, "â—†", POINT, "%.0fm".formatted(Math.sqrt(mc.player.distanceToSqr(at))), coordinates); + } + } + + private static void mark(GuiGraphics g, Minecraft mc, Vec3 at, String symbol, int color, String distance, @Nullable String extra) { + Camera camera = mc.gameRenderer.getMainCamera(); + Vector3fc forward = camera.getLookVector(); + Vec3 offset = at.subtract(camera.getPosition()); + boolean ahead = offset.x * forward.x() + offset.y * forward.y() + offset.z * forward.z() > 0.0; + // 26.x has projectPointToScreen for this; the view bobbing and the sprint widening are left out. + Vector3f view = new Vector3f((float) offset.x, (float) offset.y, (float) offset.z).rotate(camera.rotation().conjugate(new Quaternionf())); + Vector4f clip = mc.gameRenderer.getProjectionMatrix(ClientHooks.fov(mc.options.fov().get())) + .transform(new Vector4f(view, 1.0F)); + Vec3 ndc = new Vec3(clip.x() / clip.w(), clip.y() / clip.w(), clip.z() / clip.w()); + int w = g.guiWidth(); + int h = g.guiHeight(); + double x = (ndc.x + 1.0) / 2.0 * w; + double y = (1.0 - ndc.y) / 2.0 * h; + Font font = mc.font; + if (ahead && x >= 0 && x <= w && y >= 0 && y <= h) { + g.drawCenteredString(font, symbol, (int) x, (int) y - 4, color); + g.drawCenteredString(font, distance, (int) x, (int) y + 6, color); + if (extra != null) { + g.drawCenteredString(font, extra, (int) x, (int) y + 16, color); + } + return; + } + // Behind or off to the side: point the way along the screen edge. + double dx = ahead ? x - w / 2.0 : w / 2.0 - x; + double dy = ahead ? y - h / 2.0 : h / 2.0 - y; + double scale = Math.max(Math.abs(dx) / (w / 2.0 - EDGE), Math.abs(dy) / (h / 2.0 - EDGE)); + if (scale <= 0.0) { + return; + } + int ex = Mth.clamp((int) (w / 2.0 + dx / scale), EDGE, w - EDGE); + int ey = Mth.clamp((int) (h / 2.0 + dy / scale), EDGE, h - EDGE); + g.drawCenteredString(font, "â—†", ex, ey - 4, color); + } +} From 6feb0b2049344f6f776a6e47da4f6be267bc2037 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:16 +0800 Subject: [PATCH 0925/1461] Add client/VehicleCamera.java to the 1.21.5 shared sources --- .../mcheli/client/VehicleCamera.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java new file mode 100644 index 00000000..1130e270 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java @@ -0,0 +1,127 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; + +import java.util.function.Consumer; + +/** + * The local crew member's sight, {@code MCH_Camera}: zoom for vehicles with a {@code CameraZoom} + * above one, and night vision and thermal view for those with {@code EnableNightVision}. Both + * reset whenever the player changes seat or leaves the vehicle. + */ +public final class VehicleCamera { + /** The original's camera modes; the HUD reads the ordinal as {@code cam_mode}. */ + public enum Mode { + NORMAL, + NIGHT_VISION, + /** Night vision seen through a white hot filter, the original's "pencil" shader mode. */ + THERMAL + } + + /** The post effect of the thermal view, {@code assets/mcheli/post_effect/thermal.json}. */ + private static final ResourceLocation THERMAL_EFFECT = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "thermal"); + + private static float zoom = 1.0F; + private static Mode mode = Mode.NORMAL; + + private VehicleCamera() { + } + + public static float zoom() { + return zoom; + } + + public static Mode mode() { + return mode; + } + + /** Back to the plain view, on a seat change or after leaving the vehicle. */ + static void reset() { + zoom = 1.0F; + mode = Mode.NORMAL; + } + + /** + * Keeps the thermal filter in the player's post effects while the thermal view is on. The list + * is the one a server's post effect packet fills, so ours is put back if that replaces it. + */ + static void tick() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + return; + } + boolean active = THERMAL_EFFECT.equals(mc.gameRenderer.currentPostEffect()); + if (mode == Mode.THERMAL && !active) { + ClientHooks.postEffects().load(THERMAL_EFFECT); + } else if (mode != Mode.THERMAL && active) { + mc.gameRenderer.clearPostEffect(); + } + } + + /** + * {@code zoomCamera}: each press doubles the zoom up to the vehicle's {@code CameraZoom}, and + * the press after that goes back to one. The caller checks that the seat looks through a sight. + */ + static void onZoomKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + zoomStep(player, def != null && !vehicle.isDestroyed() ? def.cameraZoom : 1); + } + + /** One press of the zoom key on a sight that zooms up to {@code max}. */ + static void zoomStep(LocalPlayer player, int max) { + if (max <= 1) { + refuse(player); + return; + } + zoom = zoom >= max - 0.01F ? 1.0F : Math.min(zoom * 2.0F, max); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + + /** + * {@code switchCameraMode}: steps from the plain view to night vision, to thermal view and + * back. The server applies night vision as an effect in both of the last two; the thermal + * filter is drawn here. + */ + static void onCameraModeKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + if (def == null || !def.nightVision || vehicle.isDestroyed()) { + refuse(player); + return; + } + nextMode(player, on -> ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), + on ? CrewActionPacket.Action.NIGHT_VISION_ON : CrewActionPacket.Action.NIGHT_VISION_OFF))); + } + + /** + * Steps to the next camera mode on a sight that has night vision. {@code nightVision} asks + * the server to switch the effect on or off, only when that changes. + */ + static void nextMode(LocalPlayer player, Consumer nightVision) { + Mode next = Mode.values()[(mode.ordinal() + 1) % Mode.values().length]; + if (mode == Mode.NORMAL || next == Mode.NORMAL) { + nightVision.accept(next != Mode.NORMAL); + } + mode = next; + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + + /** The view's field of view, narrowed by the zoom. */ + public static float fov(float fov) { + return zoom > 1.0F ? fov / zoom : fov; + } + + /** {@code playSoundNG}: the key does nothing in this seat. */ + private static void refuse(LocalPlayer player) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } +} From 249803e45347638e5c0d9fad86471aed6a431a32 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:16 +0800 Subject: [PATCH 0926/1461] Add client/VehiclePicker.java to the 1.21.5 shared sources --- .../mcheli/client/VehiclePicker.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java new file mode 100644 index 00000000..9507412f --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.BoardVehiclePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Right-clicking anywhere on a vehicle's hull boards it. + * + *

Vanilla only picks an entity by its main hit box, which for most MCHeli vehicles is the + * default two by 0.7 block box at their feet; the hull's real shape lives in its extra + * {@code BoundingBox} lines. In the original those boxes were clickable too. Inflating the main box + * instead would change how the vehicle collides with terrain, so the use key looks for them here. + */ +final class VehiclePicker { + private VehiclePicker() { + } + + static boolean onUseItem(InteractionHand hand) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || hand != InteractionHand.MAIN_HAND || player.getVehicle() instanceof AircraftEntity) { + return false; + } + // Vanilla already handles a click on the main box. + if (mc.hitResult instanceof EntityHitResult hit && hit.getEntity() instanceof AircraftEntity) { + return false; + } + Hit hit = pick(player, mc.hitResult); + if (hit == null) { + return false; + } + ModNetwork.sendToServer(new BoardVehiclePacket(hit.vehicle.getId(), hit.location)); + return true; + } + + private record Hit(AircraftEntity vehicle, Vec3 location) { + } + + /** The nearest vehicle hull along the view, closer than the block or entity vanilla picked. */ + private static @Nullable Hit pick(Player player, @Nullable HitResult vanilla) { + double range = player.entityInteractionRange(); + Vec3 eye = player.getEyePosition(); + Vec3 to = eye.add(player.getViewVector(1.0F).scale(range)); + double limit = vanilla != null && vanilla.getType() != HitResult.Type.MISS ? eye.distanceToSqr(vanilla.getLocation()) : range * range; + Hit best = null; + for (AircraftEntity vehicle : player.level().getEntitiesOfClass(AircraftEntity.class, new AABB(eye, to).inflate(16.0))) { + Optional at = vehicle.clipHitBoxes(eye, to, 0.2); + vehicle.consumeHitBoxFactor(); + if (at.isPresent() && eye.distanceToSqr(at.get()) < limit) { + limit = eye.distanceToSqr(at.get()); + best = new Hit(vehicle, at.get()); + } + } + return best; + } +} From 1b13ff14792abb0785815c7f1cf9fe263ae7986f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:16 +0800 Subject: [PATCH 0927/1461] Add client/WeaponController.java to the 1.21.5 shared sources --- .../mcheli/client/WeaponController.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java new file mode 100644 index 00000000..d46804e6 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.FireWeaponPacket; +import com.jnhprojects.mcheli.network.LockWarningPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The crew member's trigger: fires, switches weapons and modes, drops flares and resupplies. + * + *

Like the original, the client fires first. Its copy of the weapon decides whether the shot + * can happen (ready, loaded, locked on, a target in sight) and works out the option values; only + * then is the server asked, which fires the real projectile. + */ +final class WeaponController { + private WeaponController() { + } + + private record Seat(AircraftEntity vehicle, VehicleWeapons weapons, int seat) { + } + + private static @Nullable Seat seat(@Nullable LocalPlayer player) { + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle) || vehicle.definition() == null) { + return null; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (weapons == null || seat < 0 || seat >= vehicle.definition().seatCount()) { + return null; + } + return new Seat(vehicle, weapons, seat); + } + + /** The use key is the trigger while the seat has a weapon, so it must not also use items. */ + static boolean onUseItem() { + Seat s = seat(Minecraft.getInstance().player); + return s != null && s.weapons.selected(s.seat) >= 0; + } + + static boolean onPickBlock() { + Seat s = seat(Minecraft.getInstance().player); + if (s == null || s.weapons.sets().isEmpty()) { + return false; + } + send(s.vehicle, Minecraft.getInstance().player != null && Minecraft.getInstance().player.isShiftKeyDown() + ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + return true; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + Seat s = seat(player); + if (s == null) { + return; + } + while (KeyBindings.SWITCH_WEAPON.consumeClick()) { + send(s.vehicle, player.isShiftKeyDown() ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + WeaponSet set = s.weapons.current(s.seat); + if (set != null && set.switchMode()) { + send(s.vehicle, CrewActionPacket.Action.WEAPON_MODE); + click(); + } + } + while (KeyBindings.FLARE.consumeClick()) { + if (s.seat <= 1 && s.weapons.flares.use()) { + send(s.vehicle, CrewActionPacket.Action.FLARE); + click(); + } + } + + if (mc.screen == null && mc.options.keyUse.isDown() && !s.vehicle.isDestroyed()) { + fire(s, player); + } + } + + private static void fire(Seat s, LocalPlayer player) { + int id = s.weapons.selected(s.seat); + if (id < 0 || s.weapons.operatorSeat(id) != s.seat) { + return; + } + Shot shot = new Shot(s.vehicle, player); + boolean fired = s.weapons.use(player, s.seat, shot, (set, mount, sh) -> clientShot(s, id, set, sh, player)); + if (fired) { + ModNetwork.sendToServer(new FireWeaponPacket(s.vehicle.getId(), id, shot.option1, shot.option2)); + } + } + + /** The client half of each weapon's {@code shot}: can it fire, and with which options. */ + private static boolean clientShot(Seat s, int id, WeaponSet set, Shot shot, LocalPlayer player) { + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_2, ROCKET, MARKER_ROCKET -> { + shot.option1 = set.mode(); + yield true; + } + case AA_MISSILE, AT_MISSILE -> { + Guidance g = s.weapons.guidance(id); + if (g == null) { + yield false; + } + boolean locked = g.lock(player, viewPoint(player)); + if (g.shouldNotifyTarget() && g.target() != null) { + ModNetwork.sendToServer(new LockWarningPacket(g.target().getId())); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (!locked || g.lastLocked() == null) { + yield false; + } + shot.option1 = g.lastLocked().getId(); + shot.option2 = set.mode(); + yield true; + } + case TV_MISSILE -> { + shot.option1 = set.mode(); + shot.option2 = 0; + yield true; + } + case AS_MISSILE -> landAhead(s.vehicle, player, 1.62, 200.0, false) != null; + case CAS -> landAhead(s.vehicle, player, 2.0, 80.0, false) != null; + case TORPEDO -> !w.guidedTorpedo || landAhead(player, player, 0.0, 100.0, true) != null; + case DUMMY, SMOKE -> false; + case null -> false; + default -> true; + }; + } + + /** + * Where the player sees from: in first person the camera, which CrewController moves to gunner + * sights and launcher cameras; otherwise the player's eyes. + */ + private static Vec3 viewPoint(LocalPlayer player) { + Minecraft mc = Minecraft.getInstance(); + return mc.options.getCameraType().isFirstPerson() ? mc.gameRenderer.getMainCamera().getPosition() : player.getEyePosition(); + } + + /** A block along the player's view: dry ground for strike weapons, water for guided torpedoes. */ + private static @Nullable BlockHitResult landAhead(Entity origin, LocalPlayer player, double eyeHeight, double range, boolean water) { + Vec3 from = origin.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(Vec3.directionFromRotation(player.getXRot(), player.getYRot()).scale(range)); + BlockHitResult hit = player.level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, player)); + if (hit.getType() != HitResult.Type.BLOCK) { + return null; + } + boolean wet = false; + BlockPos pos = hit.getBlockPos(); + for (Direction d : Direction.values()) { + wet |= player.level().getFluidState(pos.relative(d)).is(FluidTags.WATER); + } + return wet == water ? hit : null; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + private static void click() { + LocalPlayer player = Minecraft.getInstance().player; + if (player != null) { + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + } +} From d1cb9cd3ecbaa47a0b5976d82bdf98ea1d1bac4e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:17 +0800 Subject: [PATCH 0928/1461] Add client/WeaponHud.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/client/WeaponHud.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java new file mode 100644 index 00000000..347ac6f1 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java @@ -0,0 +1,123 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; + +/** + * The vehicle HUD layer. In first person it runs the seat's HUD script (the {@code HUD} line of + * the definition, or the kind's default), or {@code tv_missile} while flying a TV missile, as the + * original did; a seat whose HUD is {@code none} gets nothing. Where no script draws (third + * person, a destroyed vehicle, a script that does not exist) it shows a plain text readout of hit + * points, ammunition, reload, heat, lock and flares. + */ +final class WeaponHud { + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int RED = 0xFFFF5050; + private static final int GREEN = 0xFF60FF60; + private static final String NO_HUD = "none"; + + private WeaponHud() { + } + + /** A pilot in gunner mode gets the gunner seat's HUD, as in the original. */ + private static int hudSeat(AircraftEntity vehicle, int seat) { + return seat == 0 && vehicle.isGunnerMode() ? 1 : seat; + } + + static void draw(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + AircraftDefinition def = vehicle.definition(); + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (def == null || weapons == null || seat < 0 || seat >= def.seatCount()) { + return; + } + if (mc.options.getCameraType().isFirstPerson() && !vehicle.isDestroyed()) { + String hud = CrewController.tvCamera(vehicle, seat) != null ? "tv_missile" + : hudSeat(vehicle, seat) < def.huds.size() ? def.huds.get(hudSeat(vehicle, seat)) : null; + if (NO_HUD.equals(hud)) { + // The original's way of saying a seat has no HUD at all. + return; + } + if (hud != null && HudRenderer.draw(g, vehicle, def, player, seat, hud, delta.getGameTimeDeltaPartialTick(false))) { + return; + } + } + Font font = mc.font; + int x = g.guiWidth() - 160; + int y = g.guiHeight() - 92; + + float hp = (float) vehicle.hp() / def.maxHp; + int hpColor = vehicle.isDestroyed() ? RED : hp < 0.5F ? AMBER : GREEN; + g.drawString(font, Component.translatable("hud.mcheli.hp", vehicle.hp(), def.maxHp), x, y, hpColor); + y += 11; + + WeaponSet set = weapons.current(seat); + if (set != null) { + g.drawString(font, set.displayName(), x, y, WHITE); + y += 11; + Component ammo = vehicle.hasInfiniteAmmo(player) && set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo_infinite", set.ammo()) + : set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo", set.ammo(), set.restAmmo()) + : Component.translatable("hud.mcheli.ammo_none"); + g.drawString(font, ammo, x, y, set.ammo() == 0 && set.magazineSize() > 0 ? RED : WHITE); + y += 11; + Component status = null; + int color = AMBER; + if (set.isReloading()) { + status = Component.translatable("hud.mcheli.reloading"); + } else if (set.isOverheated()) { + status = Component.translatable("hud.mcheli.overheat"); + color = RED; + } else if (set.isInPreparation()) { + status = Component.translatable("hud.mcheli.preparing"); + } else if (set.magazineSize() > 0 && set.ammo() == 0 && set.restAmmo() == 0) { + status = Component.translatable("hud.mcheli.resupply", KeyBindings.VEHICLE_SCREEN.getTranslatedKeyMessage()); + color = RED; + } + Guidance lock = weapons.guidance(weapons.selected(seat)); + if (status == null && lock != null && lock.lockCount() > 0) { + status = lock.isLocked() + ? Component.translatable("hud.mcheli.locked") + : Component.translatable("hud.mcheli.locking", lock.lockCount() * 100 / Math.max(lock.lockCountMax(), 1)); + color = lock.isLocked() ? RED : AMBER; + } + if (status != null) { + g.drawString(font, status, x, y, color); + y += 11; + } + if (set.definition().maxHeatCount > 0) { + int width = 100; + int filled = Math.min(width, set.currentHeat * width / set.definition().maxHeatCount); + g.fill(x, y + 2, x + width, y + 5, 0x80000000); + g.fill(x, y + 2, x + filled, y + 5, set.isOverheated() ? RED : AMBER); + y += 8; + } + } else if (!weapons.sets().isEmpty()) { + g.drawString(font, Component.translatable("hud.mcheli.no_weapon"), x, y, WHITE); + y += 11; + } + if (seat <= 1 && weapons.flares.isFitted()) { + Component flare = vehicle.isFlareActive() ? Component.translatable("hud.mcheli.flare_active") + : weapons.flares.isPreparing() ? Component.translatable("hud.mcheli.flare_wait") + : Component.translatable("hud.mcheli.flare_ready", KeyBindings.FLARE.getTranslatedKeyMessage()); + g.drawString(font, flare, x, y, vehicle.isFlareActive() ? GREEN : WHITE); + } + } +} From b9a14ca643363b0fc8bd223c81c6d59af9db0109 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:17 +0800 Subject: [PATCH 0929/1461] Port HudCanvas to the pose stack and blit of 1.21.5 --- .../mcheli/client/hud/HudCanvas.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java new file mode 100644 index 00000000..05be6d57 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java @@ -0,0 +1,165 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; + +/** + * The drawing primitives of {@code MCH_HudItem}, on the 1.21.5 GUI. + * + *

The original drew GL lines, points and quads at fractional GUI coordinates. The GUI now only + * takes whole numbers, but every element keeps a copy of the pose, so each primitive is a unit + * square stretched and turned into place. Lines are one GUI unit thick, which is what + * {@code glLineWidth(scaleFactor)} gave; blips are two units square. + */ +final class HudCanvas { + private static final float LINE_WIDTH = 1.0F; + private static final float POINT_SIZE = 2.0F; + + private final GuiGraphics g; + private final Font font; + + HudCanvas(GuiGraphics g, Font font) { + this.g = g; + this.font = font; + } + + PoseStack pose() { + return g.pose(); + } + + /** Turns everything drawn until the matching {@code pop} by {@code degrees} about a point. */ + void pushRotation(double x, double y, float degrees) { + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) x, (float) y, 0.0F); + pose.mulPose(Axis.ZP.rotation((float) Math.toRadians(degrees))); + pose.translate((float) -x, (float) -y, 0.0F); + } + + void pop() { + g.pose().popPose(); + } + + void line(double x0, double y0, double x1, double y1, int color) { + double dx = x1 - x0; + double dy = y1 - y0; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + return; + } + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) x0, (float) y0, 0.0F); + pose.mulPose(Axis.ZP.rotation((float) Math.atan2(dy, dx))); + pose.translate(0.0F, -LINE_WIDTH / 2.0F, 0.0F); + pose.scale((float) length, LINE_WIDTH, 1.0F); + g.fill(0, 0, 1, 1, color); + pose.popPose(); + } + + /** {@code GL_LINES}: x, y pairs taken two points at a time. */ + void lines(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 4) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_STRIP}. */ + void strip(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 2) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_LOOP}. */ + void loop(double[] p, int color) { + strip(p, color); + if (p.length >= 4) { + line(p[p.length - 2], p[p.length - 1], p[0], p[1], color); + } + } + + /** + * {@code GL_LINES} with {@code glLineStipple(factor, pattern)}: each segment starts the 16 bit + * pattern afresh, lowest bit first, every bit covering {@code factor} GUI units. + */ + void stippled(double[] p, int color, int factor, int pattern) { + double step = Math.max(factor, 1); + for (int i = 0; i + 3 < p.length; i += 4) { + double dx = p[i + 2] - p[i]; + double dy = p[i + 3] - p[i + 1]; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + continue; + } + double ux = dx / length; + double uy = dy / length; + double runStart = -1.0; + int bit = 0; + for (double t = 0.0; t < length; t += step, bit = (bit + 1) & 15) { + boolean on = (pattern >> bit & 1) != 0; + if (on && runStart < 0.0) { + runStart = t; + } else if (!on && runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i] + ux * t, p[i + 1] + uy * t, color); + runStart = -1.0; + } + } + if (runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i + 2], p[i + 3], color); + } + } + } + + void rect(double x0, double y0, double x1, double y1, int color) { + double left = Math.min(x0, x1); + double top = Math.min(y0, y1); + double width = Math.abs(x1 - x0); + double height = Math.abs(y1 - y0); + if (width <= 0.0 || height <= 0.0) { + return; + } + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) left, (float) top, 0.0F); + pose.scale((float) width, (float) height, 1.0F); + g.fill(0, 0, 1, 1, color); + pose.popPose(); + } + + void point(double x, double y, int color) { + rect(x - POINT_SIZE / 2.0, y - POINT_SIZE / 2.0, x + POINT_SIZE / 2.0, y + POINT_SIZE / 2.0, color); + } + + /** + * {@code drawTexture}: texels {@code u, v, uWidth, vHeight} of a {@code textureWidth} by + * {@code textureHeight} image, drawn into a box and turned about its centre. Drawn untinted, + * as the original reset the colour to white first. + */ + void texture(ResourceLocation texture, double left, double top, double width, double height, double u, double v, + double uWidth, double vHeight, float rotation, int textureWidth, int textureHeight) { + if (width == 0.0 || height == 0.0) { + return; + } + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) (left + width / 2.0), (float) (top + height / 2.0), 0.0F); + pose.mulPose(Axis.ZP.rotation((float) Math.toRadians(rotation))); + pose.translate((float) (-width / 2.0), (float) (-height / 2.0), 0.0F); + pose.scale((float) width, (float) height, 1.0F); + g.blit(RenderType::guiTextured, texture, 0, 0, (float) u, (float) v, 1, 1, (int) Math.round(uWidth), (int) Math.round(vHeight), textureWidth, textureHeight); + pose.popPose(); + } + + void text(String text, int x, int y, int color, boolean centered) { + if (centered) { + g.drawCenteredString(font, text, x, y, color); + } else { + g.drawString(font, text, x, y, color); + } + } +} From ca0cf21bee1964a89eb2614b8842de2f4f0f53bc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:18 +0800 Subject: [PATCH 0930/1461] Add client/hud/HudRenderer.java to the 1.21.5 shared sources --- .../mcheli/client/hud/HudRenderer.java | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java new file mode 100644 index 00000000..458ee88e --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java @@ -0,0 +1,354 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.hud.Expression; +import com.jnhprojects.mcheli.hud.HudCommand; +import com.jnhprojects.mcheli.hud.HudScript; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.IllegalFormatException; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Runs HUD scripts, the drawing half of {@code MCH_Hud} and the {@code MCH_HudItem} subclasses. + * + *

An {@code If} hides every line up to its {@code EndIf}; {@code Exit} ends the current script + * only, so a {@code Call}er carries on; the colour carries into called scripts and over to the next + * frame. A script cannot call itself, directly or through another, the same guard the original had. + * A line that fails to format is skipped and reported once, where the original crashed. + */ +public final class HudRenderer { + private static final Logger LOGGER = LoggerFactory.getLogger(HudRenderer.class); + private static final int AZIMUTH_RED = 0xFFFF0000; + private static final String[] AZIMUTH_8 = {"S", "SW", "W", "NW", "N", "NE", "E", "SE"}; + /** {@code glLineStipple} pattern of the original's dashed lines. */ + private static final int DASHES = 0xCCCC; + + private static final HudState STATE = new HudState(); + private static final Set REPORTED = new HashSet<>(); + + private final HudCanvas canvas; + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final LocalPlayer player; + private final double centerX; + private final double centerY; + private final Set running = new HashSet<>(); + + private HudRenderer(HudCanvas canvas, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, double width, double height) { + this.canvas = canvas; + this.vehicle = vehicle; + this.def = def; + this.player = player; + this.centerX = width / 2.0; + this.centerY = height / 2.0; + } + + public static void tick() { + LocalPlayer player = Minecraft.getInstance().player; + STATE.tick(player != null && player.getVehicle() instanceof AircraftEntity vehicle ? vehicle : null); + } + + /** Fed by the pilot's mouse steering; moves the control stick indicator. */ + public static void onPilotLook(float dYaw, float dPitch) { + STATE.onPilotLook(dYaw, dPitch); + } + + /** + * Draws a HUD script for the local player's seat. + * + * @return false if no script by that name is loaded, so the caller can draw something else + */ + public static boolean draw(GuiGraphics g, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, + int seat, String hud, float partialTicks) { + HudScript script = HudScripts.INSTANCE.get(hud); + if (script == null) { + return false; + } + double width = g.guiWidth(); + double height = g.guiHeight(); + STATE.update(vehicle, def, player, seat, width, height, partialTicks); + HudRenderer renderer = new HudRenderer(new HudCanvas(g, Minecraft.getInstance().font), vehicle, def, player, width, height); + renderer.run(script); + return true; + } + + private double eval(Expression e) { + return e.eval(STATE.vars); + } + + private double x(Expression e) { + return centerX + eval(e); + } + + private double y(Expression e) { + return centerY + eval(e); + } + + private double[] points(List coordinates) { + double[] out = new double[coordinates.size()]; + for (int i = 0; i + 1 < out.length; i += 2) { + out[i] = x(coordinates.get(i)); + out[i + 1] = y(coordinates.get(i + 1)); + } + return out; + } + + private void run(HudScript script) { + if (!running.add(script.name())) { + return; + } + try { + boolean hidden = false; + for (HudCommand command : script.commands()) { + if (command instanceof HudCommand.If c) { + hidden = eval(c.condition()) == 0.0; + continue; + } + if (command instanceof HudCommand.EndIf) { + hidden = false; + continue; + } + if (hidden) { + continue; + } + if (command instanceof HudCommand.Exit) { + return; + } + execute(script, command); + } + } finally { + running.remove(script.name()); + } + } + + private void execute(HudScript script, HudCommand command) { + int color = STATE.color; + switch (command) { + case HudCommand.Color c -> { + STATE.color = (int) (long) eval(c.argb()); + STATE.vars.set(HudVariable.COLOR, STATE.color & 0xFFFFFFFFL); + } + case HudCommand.Call c -> { + HudScript called = HudScripts.INSTANCE.get(c.hud()); + if (called != null) { + run(called); + } + } + case HudCommand.Text c -> text(script, c, color); + case HudCommand.Texture c -> { + ResourceLocation texture = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + c.name() + ".png"); + int[] size = HudScripts.INSTANCE.textureSize(Minecraft.getInstance().getResourceManager(), texture); + canvas.texture(texture, x(c.left()), y(c.top()), eval(c.width()), eval(c.height()), + eval(c.u()), eval(c.v()), eval(c.uWidth()), eval(c.vHeight()), (float) eval(c.rotation()), size[0], size[1]); + } + case HudCommand.Rect c -> { + // The original truncated the size, not the position. + double left = x(c.left()); + double top = y(c.top()); + canvas.rect(left, top, left + (int) eval(c.width()), top + (int) eval(c.height()), color); + } + case HudCommand.Line c -> canvas.strip(points(c.points()), color); + case HudCommand.LineStipple c -> canvas.stippled(points(c.points()), color, (int) eval(c.factor()), (int) eval(c.pattern())); + case HudCommand.Radar c -> radar(c, color); + case HudCommand.Graduation c -> graduation(c, color); + case HudCommand.CameraRot c -> cameraRot(x(c.x()), y(c.y()), color); + case HudCommand.If c -> { + } + case HudCommand.EndIf c -> { + } + case HudCommand.Exit c -> { + } + } + } + + /** {@code MCH_HudItemString}: skipped while a weapon value is shown and no weapon (or magazine) applies. */ + private void text(HudScript script, HudCommand.Text c, int color) { + Object[] args = new Object[c.args().size()]; + for (int i = 0; i < args.length; i++) { + TextArg arg = c.args().get(i); + if ((arg.needsWeapon() && !STATE.hasWeapon()) || (arg.needsMagazine() && !STATE.hasMagazine())) { + return; + } + args[i] = STATE.text(arg, vehicle, def, player); + } + String text; + try { + text = String.format(Locale.ROOT, c.format(), args); + } catch (IllegalFormatException e) { + String key = script.name() + ':' + c.line(); + if (REPORTED.add(key)) { + LOGGER.warn("HUD {}.txt line {}: cannot format \"{}\": {}", script.name(), c.line(), c.format(), e.getMessage()); + } + return; + } + canvas.text(text, (int) x(c.x()), (int) y(c.y()), color, c.centered()); + } + + /** {@code MCH_HudItemRadar}: blips at half scale for a 64 unit box, turned with the view. */ + private void radar(HudCommand.Radar c, int color) { + List blips = c.enemies() ? STATE.enemyBlips : STATE.entityBlips; + if (blips.isEmpty()) { + return; + } + double rotation = Math.toRadians(eval(c.rotation())); + double left = x(c.left()); + double top = y(c.top()); + double w = eval(c.width()); + double h = eval(c.height()); + double cos = Math.cos(rotation); + double sin = Math.sin(rotation); + for (double[] blip : blips) { + double px = blip[0] / 2.0 * w / 64.0; + double py = blip[1] / 2.0 * h / 64.0; + double rx = px * cos - py * sin; + double ry = px * sin + py * cos; + if (rx > -w / 2.0 && rx < w / 2.0 && ry > -h / 2.0 && ry < h / 2.0) { + canvas.point(rx + left + w / 2.0, ry + top + h / 2.0, color); + } + } + } + + private void graduation(HudCommand.Graduation c, int color) { + int x = (int) x(c.x()); + int y = (int) y(c.y()); + double angle = eval(c.angle()); + canvas.pushRotation(x, y, (float) eval(c.roll())); + switch (c.kind()) { + case YAW -> headingTape(angle, color, x, y); + case PITCH_1 -> pitchScale(angle, color, x, y); + case PITCH_2 -> pitchLadder(angle, color, x, y, true); + case PITCH_3 -> pitchLadder(angle, color, x, y, false); + } + canvas.pop(); + } + + /** {@code drawCommonGraduationYaw}: a tick every degree, labels every three, compass points every 45. */ + private void headingTape(double playerYaw, int color, int posX, int posY) { + double yaw = HudState.rotate360(playerYaw); + posX -= 90; + double[] line = new double[76]; + int offset = (int) (yaw * 10.0) % 10; + int degree = (int) yaw - 9; + for (int i = 0; i < line.length / 4; i++, degree++) { + int azimuthX = posX + i * 10 - offset; + line[i * 4] = azimuthX; + line[i * 4 + 1] = posY; + line[i * 4 + 2] = azimuthX; + line[i * 4 + 3] = posY + (degree % 45 == 0 ? 15 : degree % 3 == 0 ? 10 : 5); + if (degree % 45 == 0) { + canvas.text(AZIMUTH_8[Math.floorMod(degree, 360) / 45], azimuthX, posY - 10, AZIMUTH_RED, true); + } else if (degree % 3 == 0) { + int heading = degree + 180; + if (heading < 0) { + heading += 360; + } + if (heading > 360) { + heading -= 360; + } + canvas.text(Integer.toString(heading), azimuthX, posY - 10, color, true); + } + } + canvas.lines(line, color); + } + + /** {@code drawCommonGraduationPitch1}: a scale of ticks on both sides that slides with the pitch. */ + private void pitchScale(double playerPitch, int color, int posX, int posY) { + int pitch = (int) playerPitch % 360; + int y = (int) (playerPitch * 10.0 % 10.0); + if (y < 0) { + y += 10; + } + int left = posX - 100; + int right = posX + 100; + posY -= 80; + double[] line = new double[144]; + int p = playerPitch < 0.0 && y != 0 ? pitch - 9 : pitch - 8; + for (int i = 0; i < line.length / 8; i++, p++) { + int outer = p % 3 == 0 ? 15 : 5; + double rowY = posY + i * 10 - y; + line[i * 8] = left - outer; + line[i * 8 + 1] = rowY; + line[i * 8 + 2] = left; + line[i * 8 + 3] = rowY; + line[i * 8 + 4] = right + outer; + line[i * 8 + 5] = rowY; + line[i * 8 + 6] = right; + line[i * 8 + 7] = rowY; + } + canvas.lines(line, color); + canvas.strip(new double[]{left - 25, posY - 90, left, posY - 90, left, posY + 90, left - 25, posY + 90}, color); + canvas.strip(new double[]{right + 25, posY - 90, right, posY - 90, right, posY + 90, right + 25, posY + 90}, color); + } + + /** + * {@code drawCommonGraduationPitch2}: a ladder with a rung every five degrees, dashed below + * the horizon. {@code wide} is {@code Pitch2} with five rungs; {@code Pitch3} has three, twice as far apart. + */ + private void pitchLadder(double playerPitch, int color, int posX, int posY, boolean wide) { + playerPitch = -playerPitch; + int pitchStep = (int) playerPitch / 5 * 5; + int start = wide ? 0 : 1; + int end = wide ? 5 : 4; + int spacing = wide ? 1 : 2; + double rest = playerPitch % 5.0; + double[] line = new double[8]; + for (int i = start; i < end; i++) { + int pitch = -(-pitchStep - 10 + i * 5); + int halfWidth = pitch != 0 ? 50 : 100; + int y = posY + (int) (-60 * spacing + rest * 6.0 * spacing + i * 30 * spacing); + line[0] = posX - halfWidth; + line[1] = y + (pitch == 0 ? 0 : pitch > 0 ? 2 : -2); + line[2] = posX - 50; + line[3] = y; + line[4] = posX + halfWidth; + line[5] = line[1]; + line[6] = posX + 50; + line[7] = y; + canvas.lines(line, color); + line[0] = posX - 50; + line[1] = y; + line[2] = posX - 30; + line[3] = y; + line[4] = posX + 50; + line[5] = y; + line[6] = posX + 30; + line[7] = y; + if (pitch >= 0) { + canvas.lines(line, color); + } else { + canvas.stippled(line, color, 1, DASHES); + } + if (pitch != 0) { + canvas.text(Integer.toString(pitch), posX - 60, y - 4, color, true); + canvas.text(Integer.toString(pitch), posX + 60, y - 4, color, true); + } + } + } + + /** {@code MCH_HudItemCameraRot}: a frame with a box showing where the crew's view points against the hull. */ + private void cameraRot(double posX, double posY, int color) { + canvas.loop(new double[]{posX - 21, posY - 11, posX + 21, posY - 11, posX + 21, posY + 11, posX - 21, posY + 11}, color); + canvas.stippled(new double[]{posX - 21, posY, posX, posY, posX + 21, posY, posX, posY, + posX, posY - 11, posX, posY, posX, posY + 11, posX, posY}, color, 1, DASHES); + float pitch = Mth.clamp(player.getXRot(), -30.0F, 70.0F); + pitch = (float) ((pitch - 20.0F) * 0.16); + float yaw = Mth.clamp(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot()) * 2.0F, -50.0F, 50.0F); + yaw = (float) (yaw * 0.34); + canvas.loop(new double[]{posX + yaw - 3, posY + pitch - 2, posX + yaw + 3, posY + pitch - 2, + posX + yaw + 3, posY + pitch + 2, posX + yaw - 3, posY + pitch + 2}, color); + } +} From 7731542ed28d46817b944725b53713d44cb349e1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:18 +0800 Subject: [PATCH 0931/1461] Add client/hud/HudScripts.java to the 1.21.5 shared sources --- .../mcheli/client/hud/HudScripts.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java new file mode 100644 index 00000000..5a670dbd --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java @@ -0,0 +1,86 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.hud.HudFormatException; +import com.jnhprojects.mcheli.hud.HudScript; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +/** + * The HUD scripts in {@code assets/mcheli/hud}, read through the resource manager so resource + * packs can replace or add them, and reloaded with F3 + T. A script that fails to parse is logged + * and left out; the vehicles using it fall back to the plain readout. + */ +public final class HudScripts implements ResourceManagerReloadListener { + public static final HudScripts INSTANCE = new HudScripts(); + private static final Logger LOGGER = LoggerFactory.getLogger(HudScripts.class); + private static final String FOLDER = "hud"; + private static final int DEFAULT_TEXTURE_SIZE = 256; + + private Map scripts = Map.of(); + private final Map textureSizes = new HashMap<>(); + + private HudScripts() { + } + + public @Nullable HudScript get(String name) { + return scripts.get(name.toLowerCase(Locale.ROOT)); + } + + @Override + public void onResourceManagerReload(ResourceManager resources) { + Map loaded = new HashMap<>(); + resources.listResources(FOLDER, id -> id.getNamespace().equals(MCHeli.MOD_ID) && id.getPath().endsWith(".txt")) + .forEach((id, resource) -> { + String path = id.getPath(); + String name = path.substring(FOLDER.length() + 1, path.length() - 4).toLowerCase(Locale.ROOT); + try (Reader in = resource.openAsReader()) { + loaded.put(name, HudScript.parse(name, in)); + } catch (IOException | HudFormatException e) { + LOGGER.error("Skipping HUD {}: {}", id, e.getMessage()); + } + }); + scripts = Map.copyOf(loaded); + textureSizes.clear(); + LOGGER.info("Loaded {} HUD scripts", scripts.size()); + } + + /** + * Width and height of a GUI texture in pixels, read from the PNG header once. The original + * fell back to 256 when it could not tell, and so does this. + */ + int[] textureSize(ResourceManager resources, ResourceLocation texture) { + return textureSizes.computeIfAbsent(texture, id -> readSize(resources.getResource(id))); + } + + private static int[] readSize(Optional resource) { + if (resource.isPresent()) { + try (InputStream in = resource.get().open(); DataInputStream data = new DataInputStream(in)) { + // 8 byte signature, IHDR length and type, then width and height as big endian ints. + data.skipNBytes(16); + int width = data.readInt(); + int height = data.readInt(); + if (width > 0 && height > 0) { + return new int[]{width, height}; + } + } catch (IOException e) { + LOGGER.warn("Could not read the size of a HUD texture", e); + } + } + return new int[]{DEFAULT_TEXTURE_SIZE, DEFAULT_TEXTURE_SIZE}; + } +} From 1a8eaa57cf2425c53fdcde22365b9087e6e04b07 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:19 +0800 Subject: [PATCH 0932/1461] Add client/hud/HudState.java to the 1.21.5 shared sources --- .../mcheli/client/hud/HudState.java | 373 ++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java new file mode 100644 index 00000000..46cabfc7 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java @@ -0,0 +1,373 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.client.VehicleCamera; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * What the HUD scripts read, the static state of {@code MCH_HudItem}. Ticked with the client, so + * the radar sweep, altitude and stick behave as in the original; {@link #update} then fills in the + * per frame values before a script runs. + * + *

Systems that arrive later read as their idle state for now: the vehicle inventory is empty + * and the mortar distance is unknown. + */ +final class HudState { + private static final int RADAR_RANGE = 64; + private static final int ALTITUDE_INTERVAL = 10; + private static final double STICK_MAX = 40.0; + /** Roughly the mouse units of the original per degree of pilot look. */ + private static final double STICK_PER_DEGREE = 6.0; + private static final int STICK_SAMPLES = 4; + + final HudVariable.Values vars = new HudVariable.Values(); + final List entityBlips = new ArrayList<>(); + final List enemyBlips = new ArrayList<>(); + + private @Nullable AircraftEntity vehicle; + /** Where the selected mortar would land, or -1. */ + private float mortarDistance = -1.0F; + private int radarRotate; + private int radarRotateO; + private int altitude; + private int altitudeCountdown; + private double stickX; + private double stickY; + private final double[] stickXSamples = new double[STICK_SAMPLES]; + private final double[] stickYSamples = new double[STICK_SAMPLES]; + private int stickSample; + private int fuelWarnCount; + + /** Colour set by the last {@code Color} line. Like the original it carries over between frames. */ + int color = 0xFF000000; + + // Per frame, for the text arguments. + private @Nullable WeaponSet weapon; + private String weaponName = ""; + private String weaponAmmo = ""; + private String weaponRestAmmo = ""; + private float reloadPercent; + private float reloadSeconds; + + /** Mouse look of the pilot this frame, in degrees; drives the control stick indicator. */ + void onPilotLook(float dYaw, float dPitch) { + stickX += dYaw * STICK_PER_DEGREE; + stickY += dPitch * STICK_PER_DEGREE; + double length = Math.sqrt(stickX * stickX + stickY * stickY); + if (length > STICK_MAX) { + stickX *= STICK_MAX / length; + stickY *= STICK_MAX / length; + } + } + + void tick(@Nullable AircraftEntity riding) { + if (riding != vehicle) { + vehicle = riding; + radarRotate = 0; + radarRotateO = 0; + altitudeCountdown = 0; + entityBlips.clear(); + enemyBlips.clear(); + } + stickX *= 0.85; + stickY *= 0.85; + if (riding == null || riding.definition() == null) { + return; + } + if (riding.definition().entityRadar) { + radarRotateO = radarRotate; + radarRotate += radarSpeed(riding); + if (radarRotate >= 360) { + radarRotate = 0; + radarRotateO = 0; + sweepRadar(riding); + } + } + if (--altitudeCountdown <= 0) { + altitude = measureAltitude(riding); + altitudeCountdown = ALTITUDE_INTERVAL; + } + } + + /** Degrees per tick, from each kind's client tick handler. */ + private static int radarSpeed(AircraftEntity vehicle) { + return vehicle instanceof PlaneEntity || vehicle instanceof TankEntity ? 10 : 5; + } + + /** {@code MCH_Radar.updateXZ}: mobs within 64 blocks that are not buried under five or more blocks. */ + private void sweepRadar(AircraftEntity center) { + entityBlips.clear(); + enemyBlips.clear(); + Level level = center.level(); + for (Mob mob : level.getEntitiesOfClass(Mob.class, center.getBoundingBox().inflate(RADAR_RANGE))) { + double x = mob.getX() - center.getX(); + double z = mob.getZ() - center.getZ(); + if (x * x + z * z >= RADAR_RANGE * RADAR_RANGE || isBuried(level, mob)) { + continue; + } + (mob instanceof Enemy ? enemyBlips : entityBlips).add(new double[]{x, z}); + } + } + + private static boolean isBuried(Level level, Mob mob) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(mob.getBlockX(), Math.max(mob.getBlockY() + 1, level.getMinY()), mob.getBlockZ()); + int blocks = 0; + for (; pos.getY() < level.getMaxY(); pos.move(0, 1, 0)) { + if (!level.getBlockState(pos).isAir() && ++blocks >= 5) { + return true; + } + } + return false; + } + + /** {@code updateAltitude}: blocks of air below the vehicle, looking at most 256 down. */ + private static int measureAltitude(AircraftEntity vehicle) { + Level level = vehicle.level(); + BlockPos.MutableBlockPos pos = vehicle.blockPosition().mutable(); + int top = pos.getY(); + for (int i = 0; i < 256 && top - i > level.getMinY(); i++) { + pos.setY(top - i); + if (!level.getBlockState(pos).isAir()) { + return i; + } + } + return 0; + } + + float radarRotation(float partialTicks) { + float rot = radarRotate; + float prev = radarRotateO; + if (rot < prev) { + rot += 360.0F; + } + return prev + (rot - prev) * partialTicks; + } + + void update(AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, int seat, double width, double height, + float partialTicks) { + HudVariable.Values v = vars; + v.set(HudVariable.COLOR, color & 0xFFFFFFFFL); + v.set(HudVariable.CENTER_X, width / 2.0); + v.set(HudVariable.CENTER_Y, height / 2.0); + v.set(HudVariable.WIDTH, width); + v.set(HudVariable.HEIGHT, height); + v.set(HudVariable.TIME, Math.floorMod(vehicle.level().getDayTime(), 24000L)); + v.set(HudVariable.TEST_MODE, 0.0); + v.set(HudVariable.PLYR_YAW, Mth.wrapDegrees(player.getViewYRot(partialTicks))); + v.set(HudVariable.PLYR_PITCH, player.getViewXRot(partialTicks)); + v.set(HudVariable.YAW, Mth.wrapDegrees(vehicle.getYRot(partialTicks))); + v.set(HudVariable.PITCH, vehicle.getXRot(partialTicks)); + v.set(HudVariable.ROLL, Mth.wrapDegrees(vehicle.roll(partialTicks))); + v.set(HudVariable.ALTITUDE, altitude); + v.set(HudVariable.SEA_ALT, Math.max(vehicle.getY() - vehicle.level().getSeaLevel(), 0.0)); + v.set(HudVariable.HAVE_RADAR, def.entityRadar); + v.set(HudVariable.RADAR_ROT, radarRotation(partialTicks)); + v.set(HudVariable.HP, vehicle.hp()); + v.set(HudVariable.MAX_HP, def.maxHp); + v.set(HudVariable.HP_RTO, def.maxHp > 0 ? (double) vehicle.hp() / def.maxHp : 0.0); + v.set(HudVariable.THROTTLE, vehicle.visibleThrottle()); + v.set(HudVariable.POS_X, vehicle.getX()); + v.set(HudVariable.POS_Y, vehicle.getY()); + v.set(HudVariable.POS_Z, vehicle.getZ()); + // Only the pilot's client simulates the hull, so read motion from the position instead. + double mx = vehicle.getX() - vehicle.xo; + double my = vehicle.getY() - vehicle.yo; + double mz = vehicle.getZ() - vehicle.zo; + v.set(HudVariable.MOTION_X, mx); + v.set(HudVariable.MOTION_Y, my); + v.set(HudVariable.MOTION_Z, mz); + v.set(HudVariable.SPEED, Math.sqrt(mx * mx + my * my + mz * mz)); + v.set(HudVariable.FUEL, vehicle.fuelRatio()); + v.set(HudVariable.LOW_FUEL, lowFuel(vehicle)); + updateStick(seat == 0 && vehicle.steersWithMouse()); + updateWeapon(vehicle, seat); + v.set(HudVariable.VTOL_STAT, vehicle.vtolMode()); + v.set(HudVariable.FREE_LOOK, seat == 0 && vehicle.input().freeLook); + v.set(HudVariable.GUNNER_MODE, vehicle.isGunnerMode(player)); + v.set(HudVariable.CAM_MODE, VehicleCamera.mode().ordinal()); + v.set(HudVariable.CAM_ZOOM, VehicleCamera.zoom()); + v.set(HudVariable.AUTO_PILOT, vehicle instanceof PlaneEntity && vehicle.isGunnerMode(player)); + VehicleWeapons weapons = vehicle.weapons(); + boolean flares = weapons != null && weapons.flares.isFitted(); + v.set(HudVariable.HAVE_FLARE, flares); + v.set(HudVariable.CAN_FLARE, flares && !weapons.flares.isPreparing() && !weapons.flares.isActive()); + v.set(HudVariable.INVENTORY, 0.0); + v.set(HudVariable.HOVERING, vehicle instanceof HelicopterEntity && vehicle.isHoveringMode()); + v.set(HudVariable.IS_UAV, def.uav); + double uavDistance = vehicle.stationDistance(); + // {@code getUAV_Fs}: signal strength, full at the station and gone 120 blocks out. + v.set(HudVariable.UAV_FS, uavDistance < 0.0 ? 0.0 : 1.0 - Math.min(uavDistance, 120.0) / 120.0); + } + + /** + * {@code isLowFuel}: below 10% the warning blinks, on for eight seconds out of every fourteen. + * Ticks with each update, as the original counted per frame. + */ + private boolean lowFuel(AircraftEntity vehicle) { + if (fuelWarnCount <= 0) { + fuelWarnCount = 280; + } + fuelWarnCount--; + return fuelWarnCount < 160 && vehicle.maxFuel() > 0 && vehicle.fuelRatio() < 0.1F; + } + + /** The original averaged the last four stick positions, scaled to -1..1, with up positive. */ + private void updateStick(boolean pilot) { + stickXSamples[stickSample] = pilot ? stickX / STICK_MAX : 0.0; + stickYSamples[stickSample] = pilot ? -stickY / STICK_MAX : 0.0; + stickSample = (stickSample + 1) % STICK_SAMPLES; + double x = 0.0; + double y = 0.0; + for (int i = 0; i < STICK_SAMPLES; i++) { + x += stickXSamples[i]; + y += stickYSamples[i]; + } + vars.set(HudVariable.STICK_X, x / STICK_SAMPLES); + vars.set(HudVariable.STICK_Y, y / STICK_SAMPLES); + } + + /** {@code updateWeapon} and {@code updateVarMap_Weapon}. */ + private void updateWeapon(AircraftEntity vehicle, int seat) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + weapon = set; + double reloadTime = 0.0; + double heat = 0.0; + double lock = 0.0; + int sightType = 0; + if (set != null) { + WeaponDefinition def = set.definition(); + weaponName = set.displayName(); + weaponAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.ammo()) : ""; + weaponRestAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.restAmmo()) : ""; + float wait; + if (def.delay > def.reloadTime) { + reloadSeconds = Math.abs(set.countWait); + wait = Math.min(Math.abs((float) set.countWait / Math.max(def.delay, 1)), 1.0F); + } else { + reloadSeconds = set.countReloadWait; + wait = (float) set.countReloadWait / Math.max(def.reloadTime, 1); + } + reloadTime = wait; + reloadSeconds /= 20.0F; + reloadPercent = (1.0F - wait) * 100.0F; + if (def.maxHeatCount > 0) { + heat = Math.min((double) set.currentHeat / def.maxHeatCount, 1.0); + } + Guidance guidance = weapons.guidance(weapons.selected(seat)); + if (def.sight == WeaponDefinition.Sight.LOCK && guidance != null && guidance.lockCountMax() > 0) { + lock = (double) guidance.lockCount() / guidance.lockCountMax(); + sightType = 2; + } else if (def.sight == WeaponDefinition.Sight.ROCKET) { + sightType = 1; + } + vars.set(HudVariable.IS_HEAT_WPN, def.maxHeatCount > 0); + vars.set(HudVariable.RELOADING, set.isInPreparation()); + vars.set(HudVariable.DSP_MT_DIST, def.displayMortarDistance); + // The original worked the arc out every fifth tick; it is cheap enough to follow every update. + mortarDistance = def.displayMortarDistance ? (float) set.landingDistance(vehicle) : -1.0F; + } else { + vars.set(HudVariable.IS_HEAT_WPN, 0.0); + vars.set(HudVariable.RELOADING, 0.0); + vars.set(HudVariable.DSP_MT_DIST, 0.0); + mortarDistance = -1.0F; + } + vars.set(HudVariable.RELOAD_TIME, reloadTime); + vars.set(HudVariable.WPN_HEAT, heat); + vars.set(HudVariable.SIGHT_TYPE, sightType); + vars.set(HudVariable.LOCK, lock); + vars.set(HudVariable.MT_DIST, mortarDistance); + } + + boolean hasWeapon() { + return weapon != null; + } + + boolean hasMagazine() { + return weapon != null && weapon.magazineSize() > 0; + } + + /** A {@code DrawString} argument, typed the way the original passed it to {@code String.format}. */ + @Nullable Object text(TextArg arg, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player) { + int worldTime = (int) Math.floorMod(vehicle.level().getDayTime() + 6000L, 24000L); + return switch (arg) { + case NONE -> null; + // The vehicle inventory screen and its key come later in phase 4. + case KEY_GUI -> ""; + case NAME -> def.displayName; + case ALTITUDE -> altitude; + case DATE -> new Date(); + case MC_THOR -> worldTime / 1000; + case MC_TMIN -> worldTime % 1000 * 36 / 10 / 60; + case MC_TSEC -> worldTime % 1000 * 36 / 10 % 60; + case MAX_HP -> def.maxHp; + case HP -> vehicle.hp(); + case HP_PER -> def.maxHp > 0 ? 100.0 * vehicle.hp() / def.maxHp : 0.0; + case POS_X -> vehicle.getX(); + case POS_Y -> vehicle.getY(); + case POS_Z -> vehicle.getZ(); + case MOTION_X -> vars.get(HudVariable.MOTION_X); + case MOTION_Y -> vars.get(HudVariable.MOTION_Y); + case MOTION_Z -> vars.get(HudVariable.MOTION_Z); + case INVENTORY -> 0; + case WPN_NAME -> weaponName; + case WPN_AMMO -> weaponAmmo; + case WPN_RM_AMMO -> weaponRestAmmo; + case RELOAD_PER -> reloadPercent; + case RELOAD_SEC -> reloadSeconds; + case MORTAR_DIST -> mortarDistance; + case MC_VER -> "1.21.5"; + case MOD_VER -> Platform.get().modVersion(); + case MOD_NAME -> "MCHeli Rework"; + case YAW -> rotate360(vehicle.getYRot() + 180.0); + case PITCH -> -vehicle.getXRot(); + case ROLL -> Mth.wrapDegrees(vehicle.roll()); + case PLYR_YAW -> rotate360(player.getYRot() + 180.0); + case PLYR_PITCH -> -player.getXRot(); + case TVM_POS_X, TVM_POS_Y, TVM_POS_Z, TVM_DIFF -> tvMissile(arg, vehicle); + case CAM_ZOOM -> VehicleCamera.zoom(); + case UAV_DIST -> Math.max(vehicle.stationDistance(), 0.0); + case THROTTLE -> vehicle.visibleThrottle() * 100.0; + }; + } + + private static double tvMissile(TextArg arg, AircraftEntity vehicle) { + TvMissileEntity missile = vehicle.tvMissile(); + if (missile == null) { + return 0.0; + } + return switch (arg) { + case TVM_POS_X -> missile.getX(); + case TVM_POS_Y -> missile.getY(); + case TVM_POS_Z -> missile.getZ(); + default -> missile.position().distanceTo(vehicle.position()); + }; + } + + static double rotate360(double degrees) { + double r = degrees % 360.0; + return r >= 0.0 ? r : r + 360.0; + } +} From 4894365d8ff790eef07f2caa2268d9f7aaf53b25 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:19 +0800 Subject: [PATCH 0933/1461] Add client/particle/GlowParticle.java to the 1.21.5 shared sources --- .../mcheli/client/particle/GlowParticle.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java new file mode 100644 index 00000000..8d314c9d --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.ParticleRenderType; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.particle.TextureSheetParticle; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import org.jspecify.annotations.Nullable; + +/** + * {@code mcheli:glow}: a ball of light added on top of the scene. A flash swells quickly and fades + * out; a fire holds its brightness, wavering, and only fades towards the end. How bright it gets + * depends on the light where it starts, so the same explosion is a flicker at noon and lights up + * the sky at night. + */ +public class GlowParticle extends TextureSheetParticle { + private final float size; + private final float peak; + private final boolean flicker; + private float waver = 1.0F; + + GlowParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, GlowParticleOptions options, + TextureAtlasSprite sprite, float strength) { + super(level, x, y, z); + setSprite(sprite); + setParticleSpeed(xd, yd, zd); + this.size = options.size(); + this.peak = strength; + this.flicker = options.flicker(); + this.quadSize = size; + this.lifetime = options.lifetime(); + this.friction = 0.9F; + this.gravity = 0.0F; + this.hasPhysics = false; + int color = options.color(); + setColor(ARGB.red(color) / 255.0F, ARGB.green(color) / 255.0F, ARGB.blue(color) / 255.0F); + this.alpha = 0.0F; + } + + @Override + public void tick() { + super.tick(); + if (flicker) { + waver = 0.7F + random.nextFloat() * 0.3F; + } + } + + @Override + public void render(VertexConsumer buffer, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + if (flicker) { + // Burning: hold, then die down over the last third. + alpha = peak * waver * Math.min(1.0F, (1.0F - t) * 3.0F); + } else { + // A flash: brightest at once, gone quickly. + float left = 1.0F - t; + alpha = peak * left * left; + } + super.render(buffer, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return flicker ? size * (0.85F + 0.15F * waver) : size * (0.6F + 0.4F * Math.min(1.0F, t * 4.0F)); + } + + @Override + protected int getLightColor(float partialTicks) { + return LightTexture.FULL_BRIGHT; + } + + @Override + public ParticleRenderType getRenderType() { + return Glow.PARTICLE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public @Nullable Particle createParticle(GlowParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd) { + float strength = Glow.strength(level, BlockPos.containing(x, y, z)); + if (strength <= 0.0F) { + return null; + } + return new GlowParticle(level, x, y, z, xd, yd, zd, options, sprites.get(level.random), strength); + } + } +} From 5f0d14f75760269e5483960debe4dabbcdd5a8a6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:20 +0800 Subject: [PATCH 0934/1461] Add client/particle/SmokeParticle.java to the 1.21.5 shared sources --- .../mcheli/client/particle/SmokeParticle.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java new file mode 100644 index 00000000..4d1763d1 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java @@ -0,0 +1,85 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.ParticleRenderType; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.particle.TextureSheetParticle; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; + +/** + * {@code mcheli:smoke}: a puff of coloured smoke, {@code MCH_EntityParticleSmoke}. It swells to its + * full size over the first part of its life, its colour drifts towards white, it rises slowly and + * fades out at the end. The sprites are vanilla's campfire smoke. + */ +public class SmokeParticle extends TextureSheetParticle { + private final float size; + + SmokeParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, SmokeParticleOptions options, + SpriteSet sprites, RandomSource random) { + super(level, x, y, z); + setSprite(sprites.get(random)); + setParticleSpeed(xd, yd, zd); + // The original drew a puff 0.1 x its size out from the middle, so size 10 is two blocks across. + this.size = options.size() * 0.1F * (0.8F + random.nextFloat() * 0.4F); + this.quadSize = size * 0.4F; + this.lifetime = options.lifetime() > 0 ? options.lifetime() : 60 + random.nextInt(40); + this.friction = 0.96F; + this.gravity = -0.002F; + this.hasPhysics = true; + setColor((ARGB.red(options.color()) / 255.0F), (ARGB.green(options.color()) / 255.0F), (ARGB.blue(options.color()) / 255.0F)); + this.alpha = 0.9F; + } + + @Override + public void tick() { + super.tick(); + // toWhite: pale towards the brightest channel while the colour is still strong. + float max = Math.max(rCol, Math.max(gCol, bCol)); + float min = Math.min(rCol, Math.min(gCol, bCol)); + if (max - min > 0.2F) { + rCol += (max - rCol) * 0.016F; + gCol += (max - gCol) * 0.016F; + bCol += (max - bCol) * 0.016F; + } + } + + @Override + public void render(VertexConsumer buffer, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + alpha = 0.9F * Math.min(1.0F, (1.0F - t) * 4.0F); + super.render(buffer, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return size * (0.4F + 0.6F * Math.min(1.0F, t * 3.0F)); + } + + @Override + public ParticleRenderType getRenderType() { + return Glow.SMOKE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public Particle createParticle(SmokeParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd) { + return new SmokeParticle(level, x, y, z, xd, yd, zd, options, sprites, level.random); + } + } +} From 5a6a1daad33c08a5769327e02916b66d6faa181c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:20 +0800 Subject: [PATCH 0935/1461] Add client/render/A10Renderer.java to the 1.21.5 shared sources --- .../mcheli/client/render/A10Renderer.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java new file mode 100644 index 00000000..52dc21b8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +/** The close air support A-10, {@code MCH_RenderA10}. */ +public class A10Renderer extends EntityRenderer { + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/a10.png"); + + public A10Renderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 10.5F; + } + + public static class State extends EntityRenderState { + float yaw; + float pitch; + boolean visible; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(A10Entity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.visible = entity.isVisible(); + } + + @Override + public void render(State state, PoseStack pose, MultiBufferSource buffers, int packedLight) { + Mesh mesh = state.visible ? MeshCache.INSTANCE.get("", "a-10") : null; + if (mesh != null) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(-state.pitch)); + int light = packedLight; + Geometry.submit(buffers, pose, RenderType.entityCutout(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.render(state, pose, buffers, packedLight); + } +} From 15f5a1472b35cdcafe6f617a22f07cbe20f32e9d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:21 +0800 Subject: [PATCH 0936/1461] Carry the hull boxes to the 1.21.5 render step --- .../client/render/AircraftRenderState.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java new file mode 100644 index 00000000..e5e78ade --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java @@ -0,0 +1,84 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.world.phys.AABB; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Snapshot of a vehicle for one frame, filled in by {@link AircraftRenderer#extractRenderState}. + * Every angle is already interpolated for the frame's partial tick. + */ +public class AircraftRenderState extends EntityRenderState { + /** The hull boxes to outline while F3+B is on, relative to the entity. */ + public final List hull = new ArrayList<>(); + public int hullListed; + /** Definitions are immutable after loading, so the snapshot can share them. */ + public @Nullable AircraftDefinition definition; + public String texture = ""; + public float yaw; + public float pitch; + public float roll; + /** Helicopter main rotor or plane propeller angle. */ + public float rotorAngle; + /** Each helicopter blade's own angle, per rotor, so folding blades can leave the rotor's spin. */ + public float[][] bladeAngles = new float[0][]; + public float throttle; + + // Part states, 0 to 90 degrees + public float canopy; + public float landingGear; + public float hatch; + /** Rope length below the hooks while troops go down, 0 with the ropes in. */ + public float ropeLength; + /** The beams of the search lights, or false with them off. */ + public boolean searchLight; + /** Where the crew aim a turning search light, in world degrees. */ + public float searchLightYaw; + public float searchLightPitch; + public float nozzle; + public float wing; + + public float wheelRoll; + public float steer; + public final float[] roller = new float[2]; + /** Track run per side from 0 to 1, in link spacings. */ + public final float[] track = new float[2]; + public float[] rotatingParts = new float[0]; + + /** Per seat: occupant view relative to the hull, and whether anyone sits there. */ + public float[] seatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public boolean[] seatOccupied = new boolean[0]; + /** Seat of the player at this client, or -1, and whether they look through their own eyes. */ + public int localSeat = -1; + public boolean localFirstPerson; + /** Shade for the unlit glass, from the light at the vehicle. */ + public int glassColor = 0xFFFFFFFF; + /** Drawn inside a screen, as the workshop does, rather than in the world. */ + public boolean inGui; + + /** Per {@code AddPartWeapon} line, the state of the weapon it belongs to; see {@link WeaponPartState}. */ + public WeaponPartState[] weaponParts = new WeaponPartState[0]; + /** Per weapon bay, 0 to 90 degrees. */ + public float[] bays = new float[0]; + + /** How one weapon part is drawn this frame. */ + public static final class WeaponPartState { + /** Whether the part's weapon exists; without one the part follows its operator's view. */ + public boolean hasWeapon; + /** Aim relative to the weapon's default direction. */ + public float yaw; + public float pitch; + public float defaultYaw; + /** How far a turret weapon lags behind the pilot's view. */ + public float turretLag; + public float recoil; + public float barrel; + /** A missile part whose round is on its way and not reloaded yet. */ + public boolean hidden; + } +} From c2f85fb0ca96352e7e8fd3a1f6c4549e87650204 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:21 +0800 Subject: [PATCH 0937/1461] Port AircraftRenderer to the 1.21.5 render step, with the hull boxes carried in the state --- .../client/render/AircraftRenderer.java | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java new file mode 100644 index 00000000..028e1e66 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java @@ -0,0 +1,347 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RepellingHook; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.SearchLight; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.AnimationState; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PartState; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.ShapeRenderer; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * Draws any MCHeli vehicle from its MQO/OBJ model, following {@code MCH_RenderAircraft}: the hull, + * the kind's own moving parts (rotors, wings, gun mounts) and then the parts every kind can have. + */ +public class AircraftRenderer extends EntityRenderer { + private static final float[] HULL_COLOR = {0.0F, 0.88F, 1.0F}; + private static final float[] GENERATED_COLOR = {1.0F, 0.75F, 0.0F}; + /** Ropes take the colour of brown wool. */ + private static final ResourceLocation ROPE = ResourceLocation.withDefaultNamespace("textures/block/brown_wool.png"); + private static final float ROPE_WIDTH = 0.03F; + + public AircraftRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 2.0F; + } + + /** + * With F3+B on, outlines the hull boxes next to vanilla's main box: what projectiles hit and + * what other vehicles collide with. The definition's own boxes are cyan, the ones generated + * from the model amber. + */ + private static void showHull(AircraftRenderState state, PoseStack pose, MultiBufferSource buffers) { + if (state.hull.isEmpty()) { + return; + } + VertexConsumer lines = buffers.getBuffer(RenderType.lines()); + for (int i = 0; i < state.hull.size(); i++) { + float[] c = i < state.hullListed ? HULL_COLOR : GENERATED_COLOR; + ShapeRenderer.renderLineBox(pose, lines, state.hull.get(i), c[0], c[1], c[2], 1.0F); + } + } + + @Override + public AircraftRenderState createRenderState() { + return new AircraftRenderState(); + } + + @Override + public void extractRenderState(T entity, AircraftRenderState state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + AircraftDefinition def = entity.definition(); + state.definition = def; + state.texture = entity.texture(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.roll = entity.roll(partialTicks); + if (def == null) { + return; + } + state.hull.clear(); + if (entityRenderDispatcher.shouldRenderHitBoxes()) { + // The first is the main box, which vanilla draws itself; then the listed boxes, then the generated ones. + List hull = entity.hullBoxes(); + Vec3 offset = entity.getPosition(partialTicks); + for (int i = 1; i < hull.size(); i++) { + state.hull.add(hull.get(i).move(-offset.x, -offset.y, -offset.z).move(offset.subtract(entity.position()))); + } + state.hullListed = def.boundingBoxes.size(); + } + + state.rotorAngle = switch (entity) { + case HelicopterEntity heli -> heli.rotorAngle(partialTicks); + case PlaneEntity plane -> plane.rotorAngle(partialTicks); + default -> 0.0F; + }; + state.canopy = angle(entity.canopy(), partialTicks); + state.landingGear = angle(entity.landingGear(), partialTicks); + state.hatch = angle(entity.hatch(), partialTicks); + if (entity instanceof HelicopterEntity heli && def instanceof HelicopterDefinition heliDef) { + bladeAngles(heli, heliDef, state, partialTicks); + } + state.ropeLength = entity.isRepelling() ? entity.ropeLength() : 0.0F; + state.searchLight = entity.isSearchLightOn(); + if (state.searchLight) { + Entity aimer = entity.searchLightAimer(); + state.searchLightYaw = aimer != null ? aimer.getYRot(partialTicks) : state.yaw; + state.searchLightPitch = aimer != null ? aimer.getXRot(partialTicks) : state.pitch; + } + if (entity instanceof PlaneEntity plane) { + state.nozzle = angle(plane.nozzle(), partialTicks); + state.wing = angle(plane.wing(), partialTicks); + } else { + state.nozzle = 0.0F; + state.wing = 0.0F; + } + + AnimationState anim = entity.animation; + state.throttle = Mth.lerp(partialTicks, anim.prevThrottle, anim.throttle); + state.wheelRoll = Mth.lerp(partialTicks, anim.prevWheelRoll, anim.wheelRoll); + state.steer = Mth.lerp(partialTicks, anim.prevSteer, anim.steer); + for (int i = 0; i < 2; i++) { + state.roller[i] = Mth.lerp(partialTicks, anim.prevRoller[i], anim.roller[i]); + state.track[i] = lerpWrapped(anim.prevTrack[i], anim.track[i], partialTicks); + } + if (state.rotatingParts.length != anim.rotatingParts.length) { + state.rotatingParts = new float[anim.rotatingParts.length]; + } + for (int i = 0; i < anim.rotatingParts.length; i++) { + state.rotatingParts[i] = Mth.lerp(partialTicks, anim.prevRotatingParts[i], anim.rotatingParts[i]); + } + + int seats = def.seatAndRackCount(); + if (state.seatYaw.length != seats) { + state.seatYaw = new float[seats]; + state.seatPitch = new float[seats]; + state.seatOccupied = new boolean[seats]; + } + for (int i = 0; i < seats; i++) { + state.seatYaw[i] = anim.seatYaw(i, partialTicks); + state.seatPitch[i] = anim.seatPitch(i, partialTicks); + state.seatOccupied[i] = entity.occupant(i) != null; + } + Minecraft mc = Minecraft.getInstance(); + state.localSeat = mc.player != null ? entity.seatOf(mc.player) : -1; + state.localFirstPerson = mc.options.getCameraType().isFirstPerson(); + state.glassColor = glassColor(entity, getPackedLightCoords(entity, partialTicks)); + extractWeapons(entity, def, state, partialTicks); + } + + /** + * Glass is drawn without the light map, so shade it by hand: as bright as the brighter of block + * light and the sky light left after night or rain darkens it. + */ + private static int glassColor(AircraftEntity entity, int lightCoords) { + int sky = Math.max(LightTexture.sky(lightCoords) - entity.level().getSkyDarken(), 0); + int light = Math.max(LightTexture.block(lightCoords), sky); + int shade = (int) ((0.3F + 0.7F * light / 15.0F) * 255.0F); + return 0xFF000000 | shade << 16 | shade << 8 | shade; + } + + /** {@code renderWeapon}'s inputs: each part's weapon aim, recoil, barrel spin and reload state. */ + private static void extractWeapons(AircraftEntity entity, AircraftDefinition def, AircraftRenderState state, float partialTicks) { + VehicleWeapons weapons = entity.weapons(); + int parts = def.weaponParts.size(); + if (state.weaponParts.length != parts) { + state.weaponParts = new AircraftRenderState.WeaponPartState[parts]; + for (int i = 0; i < parts; i++) { + state.weaponParts[i] = new AircraftRenderState.WeaponPartState(); + } + } + WeaponSet previous = null; + int mount = 0; + for (int i = 0; i < parts; i++) { + WeaponPart part = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState out = state.weaponParts[i]; + WeaponSet set = weapons != null && !part.weapons().isEmpty() ? weapons.byName(part.weapons().getFirst()) : null; + if (set != previous) { + mount = 0; + previous = set; + } + out.hasWeapon = set != null; + if (set == null) { + continue; + } + float yaw = set.rotationYaw; + float prevYaw = set.prevRotationYaw; + if (yaw - prevYaw > 180.0F) { + prevYaw += 360.0F; + } else if (yaw - prevYaw < -180.0F) { + prevYaw -= 360.0F; + } + out.yaw = Mth.lerp(partialTicks, prevYaw, yaw) - set.defaultRotationYaw; + out.pitch = Mth.lerp(partialTicks, set.prevRotationPitch, set.rotationPitch); + out.defaultYaw = set.defaultRotationYaw; + out.turretLag = set.rotationTurretYaw; + out.barrel = Mth.lerp(partialTicks, set.prevBarrel, set.barrel); + // A part shared by several weapons kicks back with whichever fired hardest. + float recoil = set.recoil(0, partialTicks); + for (String name : part.weapons()) { + WeaponSet other = weapons.byName(name); + if (other != null) { + recoil = Math.max(recoil, other.recoil(0, partialTicks)); + } + } + out.recoil = recoil; + out.hidden = part.missile() && weapons.isMountUsed(weapons.indexOf(set.name()), mount); + mount++; + } + if (state.bays.length != def.weaponBays.size()) { + state.bays = new float[def.weaponBays.size()]; + } + for (int i = 0; i < state.bays.length; i++) { + state.bays[i] = weapons != null ? weapons.bayAngle(i, partialTicks) : 0.0F; + } + } + + private static float angle(@Nullable PartState part, float partialTicks) { + return part != null ? part.angle(partialTicks) : 0.0F; + } + + /** Interpolates a value that wraps from 1 back to 0. */ + private static float lerpWrapped(float prev, float current, float partialTicks) { + float d = current - prev; + if (d > 0.5F) { + d -= 1.0F; + } else if (d < -0.5F) { + d += 1.0F; + } + float v = prev + d * partialTicks; + return v - Mth.floor(v); + } + + @Override + protected AABB getBoundingBoxForCulling(T entity) { + // The hit box is far smaller than most models, so cull on a generous box instead. + AircraftDefinition def = entity.definition(); + double reach = def != null ? Math.max(8.0, def.markerWidth * 4.0) : 8.0; + return entity.getBoundingBox().inflate(reach, reach / 2.0, reach); + } + + @Override + public void render(AircraftRenderState state, PoseStack poseStack, MultiBufferSource buffers, int packedLight) { + draw(state, poseStack, buffers, packedLight); + super.render(state, poseStack, buffers, packedLight); + } + + /** Draws the vehicle, without the name tag and the like a world entity also gets. The workshop preview uses this. */ + public static void draw(AircraftRenderState state, PoseStack poseStack, MultiBufferSource buffers, int packedLight) { + AircraftDefinition def = state.definition; + showHull(state, poseStack, buffers); + Mesh mesh = def != null ? MeshCache.INSTANCE.get(def.folder(), def.name()) : null; + if (mesh != null) { + // Canopies and windshields are semi-transparent texels; PartRenderer draws those + // triangles blended, as the original did, and everything else as cutout. + poseStack.pushPose(); + poseStack.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + poseStack.mulPose(Axis.XP.rotationDegrees(state.pitch)); + poseStack.mulPose(Axis.ZP.rotationDegrees(state.roll)); + new PartRenderer(def, mesh, state, poseStack, buffers, texture(def.folder(), state.texture), packedLight, + OverlayTexture.NO_OVERLAY, state.glassColor).submitAll(); + if (state.ropeLength < 0.0F) { + ropes(def, state, poseStack, buffers, packedLight); + } + if (state.searchLight) { + searchLights(def, state, poseStack, buffers); + } + poseStack.popPose(); + } + } + + /** + * {@code renderLight}: each lamp's cone. A turning light follows the crew's aim; a fixed one + * points along the vehicle, and a steering one also turns with the front wheels. + */ + private static void searchLights(AircraftDefinition def, AircraftRenderState state, PoseStack pose, MultiBufferSource buffers) { + for (SearchLight light : def.searchLights) { + pose.pushPose(); + pose.translate(light.position().x(), light.position().y(), light.position().z()); + if (!light.fixed()) { + pose.mulPose(Axis.YN.rotationDegrees(state.searchLightYaw - state.yaw + light.yaw())); + pose.mulPose(Axis.XP.rotationDegrees(state.searchLightPitch + 90.0F - state.pitch + light.pitch())); + } else { + float steer = light.steering() ? -state.steer * light.steeringRotation() : 0.0F; + pose.mulPose(Axis.YN.rotationDegrees(light.yaw() + steer)); + pose.mulPose(Axis.XP.rotationDegrees(90.0F + light.pitch())); + } + Glow.cone(pose, buffers, light.height(), light.width() / 2.0F, light.colorStart(), light.colorEnd()); + pose.popPose(); + } + } + + private static void bladeAngles(HelicopterEntity heli, HelicopterDefinition def, AircraftRenderState state, float partialTicks) { + if (state.bladeAngles.length != def.rotors.size()) { + state.bladeAngles = new float[def.rotors.size()][]; + } + for (int r = 0; r < def.rotors.size(); r++) { + RotorDefinition rotor = def.rotors.get(r); + int blades = Math.max(rotor.blades(), 1); + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + if (state.bladeAngles[r] == null || state.bladeAngles[r].length != blades) { + state.bladeAngles[r] = new float[blades]; + } + for (int b = 0; b < blades; b++) { + state.bladeAngles[r][b] = heli.bladeAngle(r, b, step, partialTicks); + } + } + } + + /** {@code renderRope}: a dark line straight down from each hook, in the vehicle's own frame. */ + private static void ropes(AircraftDefinition def, AircraftRenderState state, PoseStack pose, MultiBufferSource buffers, int light) { + float length = state.ropeLength; + Geometry.submit(buffers, pose, RenderType.entityCutoutNoCull(ROPE), (p, buffer) -> { + for (RepellingHook hook : def.repellingHooks) { + float x = (float) hook.position().x(); + float y = (float) hook.position().y(); + float z = (float) hook.position().z(); + // Two thin crossed strips, so the rope shows from every side. + ropeStrip(p, buffer, x - ROPE_WIDTH, z, x + ROPE_WIDTH, z, y, length, light); + ropeStrip(p, buffer, x, z - ROPE_WIDTH, x, z + ROPE_WIDTH, y, length, light); + } + }); + } + + private static void ropeStrip(PoseStack.Pose p, VertexConsumer buffer, float x0, float z0, float x1, float z1, float y, float length, int light) { + ropeVertex(p, buffer, x0, y, z0, 0.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y, z1, 1.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y + length, z1, 1.0F, 1.0F, light); + ropeVertex(p, buffer, x0, y + length, z0, 0.0F, 1.0F, light); + } + + private static void ropeVertex(PoseStack.Pose p, VertexConsumer buffer, float x, float y, float z, float u, float v, int light) { + buffer.addVertex(p, x, y, z).setColor(0xFFFFFFFF).setUv(u, v).setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(p, 0.0F, 1.0F, 0.0F); + } + + private static ResourceLocation texture(String folder, String name) { + return RuntimeAssets.texture(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + folder + "/" + name + ".png")); + } +} From 3f01ed9a2c20fbad277a48c257d043efdfe6fcb4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:21 +0800 Subject: [PATCH 0938/1461] Add client/render/ChainRenderer.java to the 1.21.5 shared sources --- .../mcheli/client/render/ChainRenderer.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java new file mode 100644 index 00000000..98a9b608 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java @@ -0,0 +1,93 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +/** + * {@code MCH_RenderChain}: a line of links from the towed end to the end that pulls, one link + * every 0.95 blocks, each turned along the chain. + */ +public class ChainRenderer extends EntityRenderer { + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/chain.png"); + private static final double LINK = 0.95; + private static final int GREY = 0xFF808080; + + public ChainRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + /** Towed end, relative to the chain entity. */ + Vec3 from = Vec3.ZERO; + /** From the towed end to the end that pulls. */ + Vec3 span = Vec3.ZERO; + } + + /** The chain hangs over the end that pulls but reaches to the other end; keep both in view. */ + @Override + protected AABB getBoundingBoxForCulling(ChainEntity chain) { + AABB box = super.getBoundingBoxForCulling(chain); + Entity towed = chain.towed(); + return towed != null ? box.minmax(towed.getBoundingBox()) : box; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ChainEntity chain, State state, float partialTicks) { + super.extractRenderState(chain, state, partialTicks); + Entity tow = chain.tow(); + Entity towed = chain.towed(); + if (tow == null || towed == null) { + state.span = Vec3.ZERO; + return; + } + Vec3 here = chain.getPosition(partialTicks); + Vec3 start = towed.getPosition(partialTicks); + state.from = start.subtract(here); + state.span = tow.getPosition(partialTicks).subtract(start); + } + + @Override + public void render(State state, PoseStack pose, MultiBufferSource buffers, int packedLight) { + Mesh mesh = MeshCache.INSTANCE.get("", "chain"); + double length = state.span.length(); + if (mesh != null && length > LINK) { + Vec3 step = state.span.scale(LINK / length); + float yaw = (float) Math.toDegrees(Math.atan2(step.z, step.x)); + float pitch = (float) Math.toDegrees(Math.atan2(step.y, Math.sqrt(step.x * step.x + step.z * step.z))); + int light = packedLight; + pose.pushPose(); + pose.translate(state.from); + for (double left = length; left > LINK; left -= LINK) { + pose.translate(step); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-yaw)); + pose.mulPose(Axis.ZP.rotationDegrees(pitch)); + Geometry.submit(buffers, pose, RenderType.entityCutoutNoCull(TEXTURE), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, GREY)); + pose.popPose(); + } + pose.popPose(); + } + super.render(state, pose, buffers, packedLight); + } +} From 076e2624ee3cc3aa93a67ff397ccfcddcba7522e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:22 +0800 Subject: [PATCH 0939/1461] Add client/render/DraftingTableRenderer.java to the 1.21.5 shared sources --- .../client/render/DraftingTableRenderer.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java new file mode 100644 index 00000000..69812a0e --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java @@ -0,0 +1,38 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.Vec3; + +/** {@code MCH_DraftingTableRenderer}: the original model, turned to the block's eighth of a circle. */ +public class DraftingTableRenderer implements BlockEntityRenderer { + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/block/drafting_table.png"); + /** The original greyed the table a little. */ + private static final int TINT = 0xFFBFBFBF; + + public DraftingTableRenderer(BlockEntityRendererProvider.Context context) { + } + + @Override + public void render(DraftingTableBlockEntity table, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light, int overlay, Vec3 camera) { + Mesh mesh = MeshCache.INSTANCE.get("blocks", "drafting_table"); + if (mesh == null) { + return; + } + pose.pushPose(); + pose.translate(0.5F, 0.0F, 0.5F); + pose.mulPose(Axis.YP.rotationDegrees(-table.getBlockState().getValue(DraftingTableBlock.ROTATION) * 45.0F + 180.0F)); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(TEXTURE)), light, OverlayTexture.NO_OVERLAY, TINT); + pose.popPose(); + } +} From a84e14dae90a311da1ce6e38f56194e15bbdd258 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:22 +0800 Subject: [PATCH 0940/1461] Add client/render/EquipmentRenderer.java to the 1.21.5 shared sources --- .../client/render/EquipmentRenderer.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java new file mode 100644 index 00000000..331e4d31 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; + +/** + * Equipment drawn as one model from the models root with the texture of the same name, turned to + * the entity's heading: the container, {@code MCH_RenderContainer}. + */ +public class EquipmentRenderer extends EntityRenderer { + private final String model; + private final ResourceLocation texture; + private final float yOffset; + private final int tint; + + /** + * @param model the file name under {@code models/}, without extension + * @param yOffset how far the model sits below the entity's position + * @param tint ARGB the model is multiplied by; the original greyed the container a little + */ + public EquipmentRenderer(EntityRendererProvider.Context context, String model, float yOffset, int tint, float shadow) { + super(context); + this.model = model; + this.texture = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + model + ".png"); + this.yOffset = yOffset; + this.tint = tint; + this.shadowRadius = shadow; + } + + public static class State extends EntityRenderState { + float yaw; + float pitch; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(T entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + } + + @Override + public void render(State state, PoseStack pose, MultiBufferSource buffers, int packedLight) { + Mesh mesh = MeshCache.INSTANCE.get("", model); + if (mesh != null) { + pose.pushPose(); + pose.translate(0.0F, -yOffset, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + int light = packedLight; + Geometry.submit(buffers, pose, RenderType.entityCutoutNoCull(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY, tint)); + pose.popPose(); + } + super.render(state, pose, buffers, packedLight); + } +} From 95569903968bd96693775b97b6614427062ee329 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:23 +0800 Subject: [PATCH 0941/1461] Add client/render/FlareRenderer.java to the 1.21.5 shared sources --- .../mcheli/client/render/FlareRenderer.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java new file mode 100644 index 00000000..2ca11c16 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java @@ -0,0 +1,52 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and the burning debris of a crash were only their particle trails. Not in the original: + * a burning flare now glows, which is what makes a flare salvo light up the night sky. Smoke + * grenades stay invisible. + */ +public class FlareRenderer extends EntityRenderer { + private static final float RADIUS = 1.4F; + + public FlareRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + float glow; + float flicker = 1.0F; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(FlareEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.glow = entity.isSmoke() || entity.isInWater() ? 0.0F : Glow.strength(entity.level(), entity.blockPosition()); + // A cheap waver keyed to the entity, so neighbouring flares do not pulse in step. + state.flicker = 0.8F + 0.2F * (float) Math.sin((entity.tickCount + partialTicks) * 1.7F + entity.getId()); + } + + @Override + public void render(State state, PoseStack pose, MultiBufferSource buffers, int packedLight) { + Camera camera = Minecraft.getInstance().gameRenderer.getMainCamera(); + Glow.halo(pose, buffers, camera, Vec3.ZERO, RADIUS * state.flicker, GlowParticleOptions.FIRE, state.glow); + Glow.halo(pose, buffers, camera, Vec3.ZERO, RADIUS * 0.35F, GlowParticleOptions.FLASH, state.glow); + super.render(state, pose, buffers, packedLight); + } +} From 6a109bc5a52ac271f0be9f476200c7e7dec7f49b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:23 +0800 Subject: [PATCH 0942/1461] Draw into the frame's buffers on 1.21.5, which has no submit step --- .../mcheli/client/render/Geometry.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/Geometry.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/Geometry.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/Geometry.java new file mode 100644 index 00000000..24181799 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/Geometry.java @@ -0,0 +1,24 @@ +package com.jnhprojects.mcheli.client.render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; + +/** + * Draws straight into the frame's buffers. Later versions queue geometry for a submit step instead, + * so this keeps the renderers reading the same on every version. + */ +final class Geometry { + private Geometry() { + } + + /** Fills a buffer of {@code type} with what {@code renderer} emits, at the pose as it is now. */ + static void submit(MultiBufferSource buffers, PoseStack pose, RenderType type, Renderer renderer) { + renderer.emit(pose.last(), buffers.getBuffer(type)); + } + + interface Renderer { + void emit(PoseStack.Pose pose, VertexConsumer buffer); + } +} From 62a50464082d99afe8cbdbb1472203965aa73727 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:24 +0800 Subject: [PATCH 0943/1461] Add client/render/GlassSplit.java to the 1.21.5 shared sources --- .../mcheli/client/render/GlassSplit.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java new file mode 100644 index 00000000..f21bbd2c --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Sorts a model's triangles into opaque ones and see-through glass, by the alpha of the texture + * under each triangle. + * + *

Drawing a whole vehicle translucent made its glass write depth before the game drew water, + * so water behind a canopy vanished. Opaque triangles now draw as cutout, like any entity, and + * only the glass draws translucent, through a type that leaves the depth buffer alone. + */ +final class GlassSplit { + static final GlassSplit INSTANCE = new GlassSplit(); + private static final Logger LOGGER = LoggerFactory.getLogger(GlassSplit.class); + /** Texels between these alphas are glass; fully clear ones are cut out, solid ones opaque. */ + private static final int MIN_GLASS_ALPHA = 10; + private static final int MAX_GLASS_ALPHA = 245; + + record Split(List opaque, List glass) { + static final Split EMPTY = new Split(List.of(), List.of()); + } + + private record Alpha(int width, int height, byte[] values) { + int at(float u, float v) { + int x = Math.floorMod((int) Math.floor(u * width), width); + int y = Math.floorMod((int) Math.floor(v * height), height); + return values[y * width + x] & 0xFF; + } + } + + private final Map> textures = new HashMap<>(); + private final Map, Map> splits = new IdentityHashMap<>(); + private final Map, Map, List>> buckets = new IdentityHashMap<>(); + + private GlassSplit() { + } + + /** Called from the render thread only. */ + Split split(List groups, ResourceLocation texture) { + if (groups.isEmpty()) { + return Split.EMPTY; + } + return splits.computeIfAbsent(groups, g -> new HashMap<>()).computeIfAbsent(texture, t -> compute(groups, t)); + } + + /** + * A part's objects by the texture each draws with, for a vehicle with {@code PartTexture} lines. + * The lists stay the same objects from frame to frame, so {@link #split} can remember them too. + * Called from the render thread only. + */ + List buckets(List groups, Map overrides) { + return buckets.computeIfAbsent(groups, g -> new HashMap<>()) + .computeIfAbsent(Map.copyOf(overrides), o -> PartTextures.assign(groups, o)); + } + + void clear() { + textures.clear(); + splits.clear(); + buckets.clear(); + } + + private Split compute(List groups, ResourceLocation texture) { + Alpha alpha = textures.computeIfAbsent(texture, GlassSplit::load).orElse(null); + if (alpha == null) { + return new Split(groups, List.of()); + } + List opaque = new ArrayList<>(); + List glass = new ArrayList<>(); + for (MeshGroup group : groups) { + split(group, alpha, opaque, glass); + } + return new Split(List.copyOf(opaque), List.copyOf(glass)); + } + + private static void split(MeshGroup group, Alpha alpha, List opaque, List glass) { + float[] uv = group.uvs(); + int triangles = group.vertexCount() / 3; + boolean[] isGlass = new boolean[triangles]; + int glassCount = 0; + for (int t = 0; t < triangles; t++) { + int i = t * 6; + float u = (uv[i] + uv[i + 2] + uv[i + 4]) / 3.0F; + float v = (uv[i + 1] + uv[i + 3] + uv[i + 5]) / 3.0F; + int a = alpha.at(u, v); + if (a > MIN_GLASS_ALPHA && a < MAX_GLASS_ALPHA) { + isGlass[t] = true; + glassCount++; + } + } + if (glassCount == 0) { + opaque.add(group); + } else if (glassCount == triangles) { + glass.add(group); + } else { + opaque.add(pick(group, isGlass, false, triangles - glassCount)); + glass.add(pick(group, isGlass, true, glassCount)); + } + } + + private static MeshGroup pick(MeshGroup group, boolean[] isGlass, boolean wanted, int count) { + float[] p = new float[count * 9]; + float[] n = new float[count * 9]; + float[] uv = new float[count * 6]; + int out = 0; + for (int t = 0; t < isGlass.length; t++) { + if (isGlass[t] != wanted) { + continue; + } + System.arraycopy(group.positions(), t * 9, p, out * 9, 9); + System.arraycopy(group.normals(), t * 9, n, out * 9, 9); + System.arraycopy(group.uvs(), t * 6, uv, out * 6, 6); + out++; + } + return new MeshGroup(group.name(), p, n, uv); + } + + private static Optional load(ResourceLocation texture) { + try (InputStream in = RuntimeAssets.open(texture)) { + if (in == null) { + return Optional.empty(); + } + return Optional.of(read(in)); + } catch (IOException | RuntimeException e) { + LOGGER.warn("Could not read {} to find its glass; drawing it opaque", texture, e); + return Optional.empty(); + } + } + + private static Alpha read(InputStream in) throws IOException { + try (NativeImage image = NativeImage.read(in)) { + int w = image.getWidth(); + int h = image.getHeight(); + byte[] values = new byte[w * h]; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + values[y * w + x] = (byte) (image.getPixel(x, y) >>> 24); + } + } + return new Alpha(w, h, values); + } + } +} From d4afaa1bdcf8278a972e9794ed3b03fa08cbcbc9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:24 +0800 Subject: [PATCH 0944/1461] Build the glow render types from a nested subclass on 1.21.5 --- .../mcheli/client/render/Glow.java | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java new file mode 100644 index 00000000..d517866a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java @@ -0,0 +1,224 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.effect.NightGlow; +import com.mojang.blaze3d.pipeline.BlendFunction; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.particle.ParticleRenderType; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import net.minecraft.util.TriState; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.phys.Vec3; + +/** + * Not in the original: light that adds to the scene instead of covering it, for tracers, rocket + * motors, flares and fireballs. Everything here blends additively and leaves the depth buffer + * alone, so overlapping glows brighten each other and never cut holes in the smoke behind them. + * With order independent transparency on, the vanilla translucent pipelines stand in. + */ +public final class Glow { + /** A white disc fading to nothing at the rim; also the {@code mcheli:glow} particle sprite. */ + public static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/particle/glow.png"); + + private static final RenderPipeline PARTICLE_PIPELINE = RenderPipeline.builder(RenderPipelines.PARTICLE_SNIPPET) + .withLocation(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/glow_particle")) + .withBlend(BlendFunction.LIGHTNING) + .withDepthWrite(false) + .build(); + + /** + * Vanilla's translucent particles with depth writes off. A puff's clear edge would otherwise + * hide the puffs drawn after it, and a smoke cloud is mostly overlapping puffs. + */ + private static final RenderPipeline SMOKE_PIPELINE = RenderPipeline.builder(RenderPipelines.PARTICLE_SNIPPET) + .withLocation(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/smoke_particle")) + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .build(); + + private static final RenderPipeline ENTITY_PIPELINE = RenderPipeline.builder(RenderPipelines.MATRICES_COLOR_FOG_SNIPPET) + .withLocation(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "pipeline/glow")) + .withVertexShader("core/entity") + .withFragmentShader("core/entity") + .withShaderDefine("EMISSIVE") + .withShaderDefine("NO_OVERLAY") + .withShaderDefine("NO_CARDINAL_LIGHTING") + .withSampler("Sampler0") + .withVertexFormat(DefaultVertexFormat.NEW_ENTITY, VertexFormat.Mode.QUADS) + .withBlend(BlendFunction.LIGHTNING) + .withDepthWrite(false) + .build(); + + /** The particle render type for {@code mcheli:glow}. */ + public static final ParticleRenderType PARTICLE_LAYER = new ParticleRenderType("MCHELI_GLOW", Types.particle("glow_particle", PARTICLE_PIPELINE)); + + /** The particle render type for {@code mcheli:smoke}. */ + public static final ParticleRenderType SMOKE_LAYER = new ParticleRenderType("MCHELI_SMOKE", Types.particle("smoke_particle", SMOKE_PIPELINE)); + + private static final RenderType HALO = Types.halo(); + + private static final int FULL_BRIGHT = LightTexture.FULL_BRIGHT; + + private Glow() { + } + + + /** Only a render type sees the protected shards these are built from. */ + private abstract static class Types extends RenderType { + private Types() { + super("", 0, false, false, () -> { + }, () -> { + }); + } + + static RenderType particle(String name, RenderPipeline pipeline) { + return create(MCHeli.MOD_ID + "_" + name, TRANSIENT_BUFFER_SIZE, false, false, pipeline, CompositeState.builder() + .setTextureState(new TextureStateShard(TextureAtlas.LOCATION_PARTICLES, TriState.FALSE, false)) + .setOutputState(PARTICLES_TARGET) + .setLightmapState(LIGHTMAP) + .createCompositeState(false)); + } + + static RenderType halo() { + return create(MCHeli.MOD_ID + "_glow", TRANSIENT_BUFFER_SIZE, false, true, ENTITY_PIPELINE, CompositeState.builder() + .setTextureState(new TextureStateShard(TEXTURE, TriState.FALSE, false)) + .createCompositeState(false)); + } + } + + /** + * Hands our pipelines to Iris, so a shader pack draws them with a matching program: the halos + * and tracers as emissive entities, the glow and smoke particles as translucent particles. + * Called once the mods are loaded; without Iris it does nothing. + */ + public static void assignShaderPrograms() { + IrisCompat.assign(ENTITY_PIPELINE, "EMISSIVE_ENTITIES"); + IrisCompat.assign(PARTICLE_PIPELINE, "PARTICLES_TRANSLUCENT"); + IrisCompat.assign(SMOKE_PIPELINE, "PARTICLES_TRANSLUCENT"); + } + + /** + * How strongly something at {@code pos} glows: faint by day, full at night, in caves and + * under water. 0 when the player turned the glow off. + */ + public static float strength(Level level, BlockPos pos) { + if (!ClientConfig.nightGlow()) { + return 0.0F; + } + return NightGlow.strength(level.getBrightness(LightLayer.BLOCK, pos), level.getBrightness(LightLayer.SKY, pos), level.getSkyDarken()); + } + + /** + * A disc of light facing the camera, centred {@code offset} away from the current pose. + * + * @param color RGB; {@code strength} becomes the alpha, which scales the light added + */ + public static void halo(PoseStack pose, MultiBufferSource buffers, Camera camera, Vec3 offset, float radius, int color, + float strength) { + if (strength <= 0.0F || radius <= 0.0F) { + return; + } + int argb = ARGB.color(alpha(strength), color); + pose.pushPose(); + pose.translate(offset); + pose.mulPose(camera.rotation()); + Geometry.submit(buffers, pose, HALO, (p, buffer) -> { + // Both windings, so culling never hides it whichever way the quad ends up facing. + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + }); + pose.popPose(); + } + + /** + * A tracer: a ribbon of light from the current pose back along {@code tail}, turned to face + * the camera, bright at the head and fading out towards the end. + * + * @param toCamera from the camera to the head, so the ribbon can be turned side on to the view + */ + public static void streak(PoseStack pose, MultiBufferSource buffers, Vec3 toCamera, Vec3 tail, float width, int color, float strength) { + if (strength <= 0.0F || tail.lengthSqr() < 1.0E-4) { + return; + } + Vec3 side = tail.cross(toCamera); + if (side.lengthSqr() < 1.0E-8) { + // Seen exactly end on; the halo at the head covers this. + return; + } + side = side.normalize().scale(width); + int head = ARGB.color(alpha(strength), color); + int end = ARGB.color(0, color); + float sx = (float) side.x; + float sy = (float) side.y; + float sz = (float) side.z; + float tx = (float) tail.x; + float ty = (float) tail.y; + float tz = (float) tail.z; + Geometry.submit(buffers, pose, HALO, (p, buffer) -> { + // The middle row of the disc texture gives the ribbon soft edges. + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + }); + } + + /** + * A search light's beam, {@code renderLight}: a cone {@code height} long along +Y to a disc + * {@code radius} across, fading from {@code start} at the lamp to {@code end} at the rim (ARGB). + */ + public static void cone(PoseStack pose, MultiBufferSource buffers, float height, float radius, int start, int end) { + Geometry.submit(buffers, pose, HALO, (p, buffer) -> { + int segments = 24; + for (int i = 0; i < segments; i++) { + float a0 = i * Mth.TWO_PI / segments; + float a1 = (i + 1) * Mth.TWO_PI / segments; + // A quad with the apex twice is the fan's triangle; the texture's bright middle colours it. + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, Mth.sin(a0) * radius, height, Mth.cos(a0) * radius, 0.5F, 0.5F, end); + vertex(buffer, p, Mth.sin(a1) * radius, height, Mth.cos(a1) * radius, 0.5F, 0.5F, end); + } + }); + } + + private static int alpha(float strength) { + return Math.clamp(Math.round(strength * 255.0F), 0, 255); + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float x, float y, float z, float u, float v, int argb) { + buffer.addVertex(pose, x, y, z) + .setColor(argb) + .setUv(u, v) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(FULL_BRIGHT) + .setNormal(pose, 0.0F, 1.0F, 0.0F); + } +} From 418411463c55711c974e1071079788b044f72b00 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:25 +0800 Subject: [PATCH 0945/1461] Add client/render/GltdRenderer.java to the 1.21.5 shared sources --- .../mcheli/client/render/GltdRenderer.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java new file mode 100644 index 00000000..91619d88 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java @@ -0,0 +1,106 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * {@code MCH_RenderGLTD}: the tripod, its head turned to where the viewer aims, and a faint red + * laser out of the head while someone uses it. The viewer's own head part is left out in first + * person, where it would fill the view. + */ +public class GltdRenderer extends EntityRenderer { + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gltd.png"); + private static final float HEAD_PIVOT = 0.45F; + private static final double LASER_LENGTH = 100.0; + private static final int LASER = 0xFF2020; + + public GltdRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + public static class State extends EntityRenderState { + float yaw; + float headYaw; + float headPitch; + boolean drawHead = true; + boolean laser; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(GltdEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + Entity viewer = entity.getFirstPassenger(); + Minecraft mc = Minecraft.getInstance(); + state.laser = viewer != null; + state.drawHead = viewer == null || viewer != mc.player || !mc.options.getCameraType().isFirstPerson(); + state.headYaw = viewer != null ? viewer.getYRot(partialTicks) : state.yaw; + state.headPitch = viewer != null ? viewer.getXRot(partialTicks) : 0.0F; + } + + @Override + public void render(State state, PoseStack pose, MultiBufferSource buffers, int packedLight) { + Camera camera = Minecraft.getInstance().gameRenderer.getMainCamera(); + Mesh mesh = MeshCache.INSTANCE.get("", "gltd"); + if (mesh != null) { + int light = packedLight; + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + draw(mesh.part("$body"), pose, buffers, light); + pose.popPose(); + if (state.drawHead) { + pose.pushPose(); + pose.translate(0.0F, HEAD_PIVOT, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-state.headYaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.headPitch)); + pose.translate(0.0F, -HEAD_PIVOT, 0.0F); + draw(mesh.part("$head"), pose, buffers, light); + pose.popPose(); + } + } + if (state.laser) { + float yaw = state.headYaw * Mth.DEG_TO_RAD; + float pitch = state.headPitch * Mth.DEG_TO_RAD; + Vec3 look = new Vec3(-Mth.sin(yaw) * Mth.cos(pitch), -Mth.sin(pitch), Mth.cos(yaw) * Mth.cos(pitch)); + Vec3 head = new Vec3(0.0, HEAD_PIVOT + 0.2, 0.0); + Vec3 toCamera = new Vec3(state.x, state.y, state.z).add(head).subtract(camera.getPosition()); + pose.pushPose(); + pose.translate(head); + // Brightest at the tripod, fading out along the beam. + Glow.streak(pose, buffers, toCamera, look.scale(LASER_LENGTH), 0.02F, LASER, 0.25F); + pose.popPose(); + } + super.render(state, pose, buffers, packedLight); + } + + private static void draw(List groups, PoseStack pose, MultiBufferSource buffers, int light) { + if (!groups.isEmpty()) { + Geometry.submit(buffers, pose, RenderType.entityCutoutNoCull(TEXTURE), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY)); + } + } +} From e63c8e7ca78b37904b7c69de6467db392c29532a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:25 +0800 Subject: [PATCH 0946/1461] Add client/render/IrisCompat.java to the 1.21.5 shared sources --- .../mcheli/client/render/IrisCompat.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java new file mode 100644 index 00000000..3803102e --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/IrisCompat.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.pipeline.RenderPipeline; + +import java.lang.reflect.Method; + +/** + * Tells Iris which shader program draws our own render pipelines. Iris only knows the vanilla + * pipelines and draws any other with a program whose vertex layout does not match, which throws + * the vertices far off: the glows turn into long coloured lines running to the horizon. Iris is + * optional, so its API is looked up by reflection instead of compiled against. + */ +final class IrisCompat { + private static final String API = "net.irisshaders.iris.api.v0.IrisApi"; + private static final String PROGRAM = "net.irisshaders.iris.api.v0.IrisProgram"; + + private IrisCompat() { + } + + /** + * Assigns {@code pipeline} to the Iris program named {@code program}, such as + * {@code EMISSIVE_ENTITIES}. Does nothing without Iris. + * + * @return whether Iris took it + */ + static boolean assign(RenderPipeline pipeline, String program) { + Class api; + try { + api = Class.forName(API); + } catch (ClassNotFoundException absent) { + MCHeli.LOGGER.debug("Iris is not installed, {} keeps its own shader", pipeline.getLocation()); + return false; + } + try { + Class programs = Class.forName(PROGRAM); + Object instance = api.getMethod("getInstance").invoke(null); + Object target = programs.getField(program).get(null); + Method assign = api.getMethod("assignPipeline", RenderPipeline.class, programs); + assign.invoke(instance, pipeline, target); + MCHeli.LOGGER.debug("Iris draws {} with its {} program", pipeline.getLocation(), program); + return true; + } catch (ReflectiveOperationException | LinkageError e) { + // An Iris without this API: glows may look wrong under a shader pack, nothing worse. + MCHeli.LOGGER.warn("Could not hand {} to Iris, glows may look wrong with shaders: {}", pipeline.getLocation(), e.toString()); + return false; + } + } +} From 92bcd588c0197c61b3518551c190bf1f5dba9de6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:26 +0800 Subject: [PATCH 0947/1461] Add client/render/MeshCache.java to the 1.21.5 shared sources --- .../mcheli/client/render/MeshCache.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java new file mode 100644 index 00000000..1fc66d2f --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshLoader; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Lazily loads vehicle models, from the resource packs (so they can replace them) or else from the + * runtime roots of {@link RuntimeAssets}. Models are parsed the first time a vehicle of that kind + * is drawn, and dropped on resource reload and on content reload ({@link #clear}). + */ +public final class MeshCache implements ResourceManagerReloadListener { + public static final MeshCache INSTANCE = new MeshCache(); + private static final Logger LOGGER = LoggerFactory.getLogger(MeshCache.class); + private static final String[] EXTENSIONS = {".mqo", ".obj"}; + + private final Map> meshes = new ConcurrentHashMap<>(); + + private MeshCache() { + } + + /** + * @param folder content folder, for example {@code helicopters}, or empty for the models root + * @param name definition name + * @return the mesh, or {@code null} if no model exists or it failed to load + */ + public @Nullable Mesh get(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, true)).orElse(null); + } + + /** Like {@link #get}, for optional models such as separate part files; a missing one is not logged. */ + public @Nullable Mesh find(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, false)).orElse(null); + } + + private static Optional load(String folder, String name, boolean required) { + for (String extension : EXTENSIONS) { + ResourceLocation id = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, + (folder.isEmpty() ? "models/" : "models/" + folder + "/") + name + extension); + try (InputStream in = RuntimeAssets.open(id)) { + if (in == null) { + continue; + } + Mesh mesh = MeshLoader.load(id.toString(), in); + LOGGER.debug("Loaded {}", mesh); + return Optional.of(mesh); + } catch (IOException | RuntimeException e) { + LOGGER.error("Failed to load model {}", id, e); + return Optional.empty(); + } + } + if (required) { + LOGGER.warn("No model found for {}/{}", folder, name); + } + return Optional.empty(); + } + + /** Forgets every model and glass split, so the next frame reads them again from disk. */ + public void clear() { + meshes.clear(); + GlassSplit.INSTANCE.clear(); + } + + @Override + public void onResourceManagerReload(ResourceManager resourceManager) { + clear(); + RuntimeAssets.clearTextures(); + } +} From 6d7581d224f2d07f1dec82e393308a0db71e95f2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:26 +0800 Subject: [PATCH 0948/1461] Add client/render/MeshRenderer.java to the 1.21.5 shared sources --- .../mcheli/client/render/MeshRenderer.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java new file mode 100644 index 00000000..47cc66e3 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java @@ -0,0 +1,88 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.List; + +/** Streams {@link MeshGroup} triangles into an entity render type. */ +final class MeshRenderer { + private static final int WHITE = 0xFFFFFFFF; + + private MeshRenderer() { + } + + /** + * Entity render types draw quads, so each triangle is sent as a quad whose last corner repeats + * the third one. The GPU discards the degenerate half for free. + */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay) { + emit(groups, pose, buffer, light, overlay, WHITE); + } + + /** As above, tinted with an ARGB colour, for bullets drawn in their {@code BulletColor}. */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + /** + * As above, but only the triangles that face the camera. No vanilla type is both culled and + * free of depth writes, so glass is culled here. Back to back glass faces would otherwise sort + * in an arbitrary order and swap places as the view moves. + * + *

Entities are submitted in world-aligned space with the camera at the origin, so a front + * face (counter-clockwise, as in the original's GL setup) is one whose normal points back + * towards the origin. + */ + static void emitFrontFaces(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + Matrix4f m = pose.pose(); + Vector3f a = new Vector3f(); + Vector3f b = new Vector3f(); + Vector3f c = new Vector3f(); + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + m.transformPosition(p[v * 3], p[v * 3 + 1], p[v * 3 + 2], a); + m.transformPosition(p[v * 3 + 3], p[v * 3 + 4], p[v * 3 + 5], b); + m.transformPosition(p[v * 3 + 6], p[v * 3 + 7], p[v * 3 + 8], c); + b.sub(a); + c.sub(a); + if (b.cross(c).dot(a) >= 0.0F) { + continue; + } + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float[] p, float[] n, float[] uv, int i, int light, int overlay, int color) { + int a = i * 3; + int t = i * 2; + buffer.addVertex(pose, p[a], p[a + 1], p[a + 2]) + .setColor(color) + .setUv(uv[t], uv[t + 1]) + .setOverlay(overlay) + .setLight(light) + .setNormal(pose, n[a], n[a + 1], n[a + 2]); + } +} From 82f26a540d516bb67d421dc874315a3e9c40c74d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:27 +0800 Subject: [PATCH 0949/1461] Add client/render/ParachuteRenderer.java to the 1.21.5 shared sources --- .../client/render/ParachuteRenderer.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java new file mode 100644 index 00000000..8bbc1eab --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +/** + * {@code MCH_RenderParachute}: the item's round canopy, or the ejection seat with its canopy, + * from the models the import brought in. + */ +public class ParachuteRenderer extends EntityRenderer { + private static final ResourceLocation ROUND = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute1.png"); + private static final ResourceLocation SEAT = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/parachute2.png"); + + public ParachuteRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + public static class State extends EntityRenderState { + float yaw; + ParachuteEntity.Kind kind = ParachuteEntity.Kind.ITEM; + boolean open; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ParachuteEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.kind = entity.kind(); + state.open = entity.isOpen(); + } + + @Override + public void render(State state, PoseStack pose, MultiBufferSource buffers, int packedLight) { + List groups = groups(state); + if (!groups.isEmpty()) { + ResourceLocation texture = state.kind == ParachuteEntity.Kind.ITEM ? ROUND : SEAT; + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = packedLight; + Geometry.submit(buffers, pose, RenderType.entityCutoutNoCull(texture), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.render(state, pose, buffers, packedLight); + } + + private static List groups(State state) { + if (state.kind == ParachuteEntity.Kind.ITEM) { + Mesh mesh = MeshCache.INSTANCE.get("", "parachute1"); + return mesh != null ? mesh.groups() : List.of(); + } + Mesh mesh = MeshCache.INSTANCE.get("", "parachute2"); + boolean canopy = state.kind == ParachuteEntity.Kind.DROP || state.open; + return mesh != null ? mesh.part(canopy ? "$parachute" : "$seat") : List.of(); + } +} From f0ecb030f9e6c64edacd06a5c71aafc166de3daa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:27 +0800 Subject: [PATCH 0950/1461] Add client/render/PartRenderer.java to the 1.21.5 shared sources --- .../mcheli/client/render/PartRenderer.java | 554 ++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java new file mode 100644 index 00000000..f8f1eb4f --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java @@ -0,0 +1,554 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.CameraPart; +import com.jnhprojects.mcheli.content.aircraft.part.CrawlerTrack; +import com.jnhprojects.mcheli.content.aircraft.part.LandingGearPart; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.content.aircraft.part.PropellerPart; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import com.jnhprojects.mcheli.content.aircraft.part.ThrottlePart; +import com.jnhprojects.mcheli.content.aircraft.part.TrackRoller; +import com.jnhprojects.mcheli.content.aircraft.part.VehiclePart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.content.aircraft.part.WheelPart; +import com.jnhprojects.mcheli.content.aircraft.part.WingPart; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import org.joml.Vector3f; + +import java.util.List; + +/** + * Submits a vehicle's hull and moving parts for one frame. A port of the part routines in + * {@code MCH_RenderAircraft} and the per kind renderers; every transform is applied in hull space + * on top of the pose the caller already set up. + * + *

A part is looked up as {@code $name} inside the vehicle model, or as a separate + * {@code _} model file, the two places the original accepted. + */ +final class PartRenderer { + private static final Vector3 X_AXIS = new Vector3(1, 0, 0); + private static final Vector3 DOWN_AXIS = new Vector3(0, -1, 0); + + private final AircraftDefinition def; + private final Mesh mesh; + private final AircraftRenderState state; + private final PoseStack pose; + private final MultiBufferSource buffers; + private final ResourceLocation texture; + private final RenderType opaqueType; + private final RenderType glassType; + private final int light; + private final int overlay; + /** Glass draws unlit, so its colour carries the brightness of where the vehicle stands. */ + private final int glassColor; + + PartRenderer(AircraftDefinition def, Mesh mesh, AircraftRenderState state, PoseStack pose, MultiBufferSource buffers, + ResourceLocation texture, int light, int overlay, int glassColor) { + this.def = def; + this.mesh = mesh; + this.state = state; + this.pose = pose; + this.buffers = buffers; + this.texture = texture; + // Culled, as the original drew it. The models are full of back to back faces for thin + // panels, and drawing both sides of those makes them fight for the same depth. + this.opaqueType = RenderType.entityCutout(texture); + // The one entity translucent type that does not write depth, so water behind glass still + // draws. It does not cull, so MeshRenderer drops the back faces itself. + this.glassType = RenderType.entityTranslucentEmissive(texture); + this.light = light; + this.overlay = overlay; + this.glassColor = glassColor; + } + + void submitAll() { + switch (def) { + case PlaneDefinition plane -> { + nozzles(plane); + wings(plane); + propellers(plane); + hull(); + } + case HelicopterDefinition heli -> { + hull(); + rotors(heli); + } + case VehicleDefinition vehicle -> { + hull(); + for (VehiclePart part : vehicle.parts) { + vehiclePart(vehicle, part); + } + } + default -> hull(); + } + weapons(); + rotatingParts(); + movingParts(def.hatches, state.hatch); + trackRollers(); + crawlerTracks(); + steeringWheels(); + wheels(); + throttles(); + cameras(); + landingGear(); + weaponBays(); + movingParts(def.canopies, state.canopy); + } + + /** + * The body and, as a {@code $} object of its own so it can have its own {@code PartTexture}, + * the cockpit. A model without {@code $body} draws as one piece, cockpit included, and no original + * model has a {@code $cockpit}. + */ + private void hull() { + submit(mesh.body()); + if (mesh.contains("$body")) { + submit(mesh.part("$cockpit")); + } + } + + // Kind specific parts + + private void rotors(HelicopterDefinition heli) { + for (int r = 0; r < heli.rotors.size(); r++) { + RotorDefinition rotor = heli.rotors.get(r); + List blade = part(rotor.part()); + if (blade.isEmpty() || isZero(rotor.axis())) { + continue; + } + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + int blades = Math.max(rotor.blades(), 1); + for (int i = 0; i < blades; i++) { + pose.pushPose(); + Vector3 p = rotor.position(); + translate(p, 1.0F); + float[] angles = r < state.bladeAngles.length ? state.bladeAngles[r] : null; + rotate(angles != null && i < angles.length ? angles[i] : state.rotorAngle + i * step, rotor.axis()); + if (!rotor.legacyPivot()) { + translate(p, -1.0F); + } + submit(blade); + pose.popPose(); + } + } + } + + private void nozzles(PlaneDefinition plane) { + for (MovingPart nozzle : plane.nozzles) { + pose.pushPose(); + rotateAround(nozzle.position(), state.nozzle, nozzle.axis()); + submit(part(nozzle.part())); + pose.popPose(); + } + } + + private void wings(PlaneDefinition plane) { + for (WingPart wing : plane.wings) { + pose.pushPose(); + rotateAround(wing.position(), state.wing * wing.factor(), wing.axis()); + submit(part(wing.part())); + for (MovingPart pylon : wing.pylons()) { + pose.pushPose(); + rotateAround(pylon.position(), state.wing * pylon.factor(), pylon.axis()); + submit(part(pylon.part())); + pose.popPose(); + } + pose.popPose(); + } + } + + /** Tilt rotors follow the nozzle state, the blades spin with the engine. */ + private void propellers(PlaneDefinition plane) { + if (!plane.hasNozzleState()) { + return; + } + for (PropellerPart rotor : plane.propellers) { + pose.pushPose(); + rotateAround(rotor.position(), state.nozzle * rotor.factor(), rotor.axis()); + submit(part(rotor.part())); + for (PropellerPart.Blade blade : rotor.blades()) { + List groups = part(blade.part()); + pose.pushPose(); + rotateAround(blade.position(), state.rotorAngle, blade.axis()); + for (int i = 0; i < blade.count(); i++) { + rotateAround(blade.position(), blade.step(), blade.axis()); + submit(groups); + } + pose.popPose(); + } + pose.popPose(); + } + } + + /** {@code MCH_RenderVehicle.drawPart}: mount parts turn with the gunner's view. */ + private void vehiclePart(VehicleDefinition vehicle, VehiclePart part) { + pose.pushPose(); + if (part.yaw() || part.pitch() || part.type() == VehiclePart.TYPE_ROTATING_BARREL) { + translate(part.position(), 1.0F); + if (part.yaw()) { + rotate(seatYaw(0), DOWN_AXIS); + } + if (part.pitch()) { + rotate(Mth.clamp(seatPitch(0), vehicle.minRotationPitch, vehicle.maxRotationPitch), X_AXIS); + } + translate(part.position(), -1.0F); + } + for (VehiclePart child : part.children()) { + vehiclePart(vehicle, child); + } + boolean gunnerLooking = state.localSeat == 0 && state.localFirstPerson; + if (part.drawFirstPerson() || !gunnerLooking) { + submit(part(part.part())); + } + pose.popPose(); + } + + // Parts every kind can have + + /** + * Gun mounts, turrets and missiles, {@code renderWeapon}. Each follows its weapon's aim, kicks + * back as it fires, spins its barrels and, for missiles, disappears until reloaded. A part + * whose weapon is missing follows the view of the seat that would fire it. + */ + private void weapons() { + float turretYaw = seatYaw(0); + for (int i = 0; i < def.weaponParts.size(); i++) { + WeaponPart w = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState ws = i < state.weaponParts.length ? state.weaponParts[i] : null; + boolean live = ws != null && ws.hasWeapon; + WeaponMount mount = mount(w); + int seat = mount != null ? mount.seatIndex() : 0; + if (w.hideInFirstPerson() && state.localFirstPerson && state.localSeat == seat) { + continue; + } + float defaultYaw = live ? ws.defaultYaw : mount != null ? mount.defaultYaw() : 0.0F; + float yaw; + float pitch; + if (live) { + yaw = ws.yaw; + pitch = ws.pitch; + } else { + yaw = Mth.wrapDegrees(seatYaw(seat) - defaultYaw); + pitch = seatPitch(seat); + } + + pose.pushPose(); + if (w.turret()) { + rotateAround(def.turretPosition, turretYaw, DOWN_AXIS); + } + translate(w.position(), 1.0F); + if (w.yaw()) { + rotate(w.turret() && !live ? yaw - turretYaw : yaw, DOWN_AXIS); + } + if (w.turret() && live) { + rotate(-(turretYaw - ws.turretLag), DOWN_AXIS); + } + // Side facing guns pitch around their own axis, which the original did by turning + // into the gun's frame and flipping the sign for guns pointing left or right. + boolean sideways = false; + if ((int) defaultYaw != 0) { + float t = Mth.wrapDegrees(defaultYaw); + sideways = (t >= 45.0F && t <= 135.0F) || (t <= -45.0F && t >= -135.0F); + rotate(-defaultYaw, DOWN_AXIS); + } + if (w.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && w.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, w.recoil() * ws.recoil); + } + if (mount != null || live) { + rotate(defaultYaw, DOWN_AXIS); + } + if (live && w.rotatingBarrel()) { + rotate(ws.barrel, w.axis()); + } + translate(w.position(), -1.0F); + if (!w.missile() || !live || !ws.hidden) { + submit(part(w.part())); + for (WeaponPart.Child child : w.children()) { + pose.pushPose(); + translate(child.position(), 1.0F); + if (child.yaw()) { + rotate(yaw, DOWN_AXIS); + } + if (live && (int) defaultYaw != 0) { + rotate(-defaultYaw, DOWN_AXIS); + } + if (child.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && child.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, -child.recoil() * ws.recoil); + } + if (live && (int) defaultYaw != 0) { + rotate(defaultYaw, DOWN_AXIS); + } + translate(child.position(), -1.0F); + submit(part(child.part())); + pose.popPose(); + } + } + pose.popPose(); + } + } + + private WeaponMount mount(WeaponPart part) { + if (part.weapons().isEmpty()) { + return null; + } + int group = def.weaponGroup(part.weapons().getFirst()); + return group >= 0 ? def.weaponGroups.get(group).mounts().getFirst() : null; + } + + private void rotatingParts() { + for (int i = 0; i < def.rotatingParts.size() && i < state.rotatingParts.length; i++) { + RotatingPart part = def.rotatingParts.get(i); + pose.pushPose(); + rotateAround(part.position(), state.rotatingParts[i], part.axis()); + submit(part(part.part())); + pose.popPose(); + } + } + + /** Hatches and canopies: swing by {@code angle * factor}, or slide along their offset. */ + private void movingParts(List parts, float angle) { + for (MovingPart part : parts) { + pose.pushPose(); + moveOrSwing(part, angle); + submit(part(part.part())); + pose.popPose(); + } + } + + private void moveOrSwing(MovingPart part, float angle) { + if (part.slide()) { + translate(part.position(), angle / 90.0F); + } else { + rotateAround(part.position(), angle * part.factor(), part.axis()); + } + } + + private void trackRollers() { + for (TrackRoller roller : def.trackRollers) { + pose.pushPose(); + rotateAround(roller.position(), state.roller[roller.side()], X_AXIS); + submit(part(roller.part())); + pose.popPose(); + } + } + + /** + * Each link slides towards the next one by the track's run, so the loop appears to move. + * The link part is modelled at the origin and placed at each point of the outline. + */ + private void crawlerTracks() { + for (CrawlerTrack track : def.crawlerTracks) { + List link = part(track.part()); + List links = track.links(); + int count = links.size() - 1; + if (link.isEmpty() || count < 1) { + continue; + } + float run = state.track[track.side()]; + for (int i = 0; i < count; i++) { + CrawlerTrack.Link cur = links.get(i); + CrawlerTrack.Link next = links.get((i + 1) % count); + float angleNext = next.angle(); + if (angleNext - cur.angle() < -180.0F) { + angleNext += 360.0F; + } else if (angleNext - cur.angle() > 180.0F) { + angleNext -= 360.0F; + } + pose.pushPose(); + pose.translate(0.0F, cur.y() + (next.y() - cur.y()) * run, cur.z() + (next.z() - cur.z()) * run); + pose.mulPose(Axis.XN.rotationDegrees(cur.angle() + (angleNext - cur.angle()) * run)); + submit(link); + pose.popPose(); + } + } + } + + private void steeringWheels() { + for (WheelPart wheel : def.steeringWheels) { + pose.pushPose(); + rotateAround(wheel.position(), state.steer * wheel.steerAngle(), wheel.axis()); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void wheels() { + for (WheelPart wheel : def.wheels) { + pose.pushPose(); + rotateAround(wheel.pivot(), state.steer * wheel.steerAngle(), wheel.axis()); + rotateAround(wheel.position(), state.wheelRoll, X_AXIS); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void throttles() { + for (ThrottlePart lever : def.throttleParts) { + pose.pushPose(); + rotateAround(lever.position(), state.throttle * lever.angle(), lever.axis()); + translate(lever.slide(), state.throttle); + submit(part(lever.part())); + pose.popPose(); + } + } + + /** Sensor turrets look where the gunner looks, or the pilot when the gunner seat is empty. */ + private void cameras() { + int seat = state.seatOccupied.length > 1 && state.seatOccupied[1] ? 1 : 0; + for (CameraPart camera : def.cameraParts) { + pose.pushPose(); + translate(camera.position(), 1.0F); + if (camera.yawSync()) { + rotate(seatYaw(seat), DOWN_AXIS); + } + if (camera.pitchSync()) { + rotate(seatPitch(seat), X_AXIS); + } + translate(camera.position(), -1.0F); + submit(part(camera.part())); + pose.popPose(); + } + } + + /** + * Gear legs swing up with the gear state; reversed parts close as the gear rises, and gear bay + * doors open fully halfway through and shut again. + */ + private void landingGear() { + float angle = state.landingGear; + float reverse = 90.0F - angle; + float door = Math.min(90.0F, 90.0F * Mth.sin(angle * 2.0F * Mth.DEG_TO_RAD) * 3.0F); + for (LandingGearPart gear : def.landingGear) { + pose.pushPose(); + translate(gear.position(), 1.0F); + if (gear.reverse()) { + rotate(reverse * gear.factor(), gear.axis()); + } else { + rotate((gear.hatch() ? door : angle) * gear.factor(), gear.axis()); + } + if (gear.secondAxis() != null) { + rotate((gear.reverse() ? reverse : angle) * gear.secondFactor(), gear.secondAxis()); + } + translate(gear.position(), -1.0F); + if (gear.slide() != null) { + float f = angle / 90.0F; + translate(gear.slide(), gear.reverse() ? 1.0F - f : f); + } + submit(part(gear.part())); + pose.popPose(); + } + } + + /** Bays swing open while a crew member has one of their weapons selected. */ + private void weaponBays() { + for (int i = 0; i < def.weaponBays.size(); i++) { + WeaponBayPart bay = def.weaponBays.get(i); + pose.pushPose(); + moveOrSwing(bay.door(), i < state.bays.length ? state.bays[i] : 0.0F); + submit(part(bay.door().part())); + pose.popPose(); + } + } + + // Helpers + + private float seatYaw(int seat) { + return seat >= 0 && seat < state.seatYaw.length ? state.seatYaw[seat] : 0.0F; + } + + private float seatPitch(int seat) { + return seat >= 0 && seat < state.seatPitch.length ? state.seatPitch[seat] : 0.0F; + } + + private List part(String name) { + List groups = mesh.part("$" + name); + if (!groups.isEmpty()) { + return groups; + } + Mesh separate = MeshCache.INSTANCE.find(def.folder(), def.name() + "_" + name); + return separate != null ? separate.groups() : List.of(); + } + + private void submit(List groups) { + if (groups.isEmpty()) { + return; + } + if (def.partTextures.isEmpty()) { + submit(groups, texture, opaqueType, glassType); + return; + } + for (PartTextures.Bucket bucket : GlassSplit.INSTANCE.buckets(groups, def.partTextures)) { + if (bucket.texture() == null) { + submit(bucket.groups(), texture, opaqueType, glassType); + } else { + ResourceLocation own = RuntimeAssets.texture(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, + "textures/" + def.folder() + "/" + bucket.texture() + ".png")); + submit(bucket.groups(), own, RenderType.entityCutout(own), RenderType.entityTranslucentEmissive(own)); + } + } + } + + private void submit(List groups, ResourceLocation texture, RenderType opaqueType, RenderType glassType) { + GlassSplit.Split split = GlassSplit.INSTANCE.split(groups, texture); + if (!split.opaque().isEmpty()) { + Geometry.submit(buffers, pose, opaqueType, (p, buffer) -> MeshRenderer.emit(split.opaque(), p, buffer, light, overlay)); + } + if (!split.glass().isEmpty()) { + // Culling glass relies on the camera sitting at the origin, which is not so in a GUI. + Geometry.submit(buffers, pose, glassType, state.inGui + ? (p, buffer) -> MeshRenderer.emit(split.glass(), p, buffer, light, overlay, glassColor) + : (p, buffer) -> MeshRenderer.emitFrontFaces(split.glass(), p, buffer, light, overlay, glassColor)); + } + } + + private void translate(Vector3 v, float scale) { + pose.translate((float) v.x() * scale, (float) v.y() * scale, (float) v.z() * scale); + } + + private void rotateAround(Vector3 pivot, float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + translate(pivot, 1.0F); + rotate(degrees, axis); + translate(pivot, -1.0F); + } + + /** Like {@code glRotatef}: any axis length, a zero axis does nothing. */ + private void rotate(float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + Vector3f a = new Vector3f((float) axis.x(), (float) axis.y(), (float) axis.z()).normalize(); + pose.mulPose(Axis.of(a).rotationDegrees(degrees)); + } + + private static boolean isZero(Vector3 v) { + return v.x() * v.x() + v.y() * v.y() + v.z() * v.z() < 1.0E-12; + } +} From e5b2039f8acad6d308a75740f7719ca41c9ee46b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:27 +0800 Subject: [PATCH 0951/1461] Draw translucent rounds with vanilla's culled type on 1.21.5 --- .../client/render/ProjectileRenderer.java | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java new file mode 100644 index 00000000..5fed3cd5 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java @@ -0,0 +1,168 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.weapon.WeaponColor; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * Draws rounds, rockets, missiles and bombs from {@code models/bullets}, tinted with the weapon's + * {@code BulletColor} (its in-water colour below the surface) and always fully lit, as the + * original's {@code MCH_RenderBulletBase} did. + * + *

Not in the original: rounds trail a tracer and rocket motors burn with a glow, faint by day + * and bright at night, so a firefight in the dark can be followed by eye. Nothing glows under + * water. + */ +public class ProjectileRenderer extends EntityRenderer { + private static final int FULL_BRIGHT = 0xF000F0; + /** Warm tracer colour for rounds whose weapon keeps the default white tint. */ + private static final int TRACER = 0xFFC46A; + /** The longest tracer ribbon, in blocks; fast rounds cover far more than this per tick. */ + private static final double MAX_TRACER = 6.0; + /** How far behind the projectile's centre the motor glow sits, in blocks. */ + private static final double MOTOR_OFFSET = 0.6; + + public ProjectileRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + enum GlowKind { + NONE, + TRACER, + MOTOR + } + + public static class State extends EntityRenderState { + String model = ""; + float yaw; + float pitch; + int color = 0xFFFFFFFF; + boolean visible = true; + GlowKind glowKind = GlowKind.NONE; + float glow; + int glowColor = TRACER; + /** Distance covered over the last tick, which points the tracer and the motor glow. */ + Vec3 travel = Vec3.ZERO; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ProjectileEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.model = entity.model(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.visible = !entity.isSpent() && (!(entity instanceof BombEntity bomb) || bomb.isVisible()); + WeaponDefinition w = entity.weapon(); + boolean inWater = inWater(entity.level(), entity); + WeaponColor c = w != null ? (inWater ? w.colorInWater : w.color) : WeaponColor.WHITE; + state.color = c.argb(); + + state.glowKind = inWater || entity.isSpent() ? GlowKind.NONE : glowKind(entity); + if (state.glowKind != GlowKind.NONE) { + state.glow = Glow.strength(entity.level(), entity.blockPosition()); + state.travel = new Vec3(entity.getX() - entity.xo, entity.getY() - entity.yo, entity.getZ() - entity.zo); + if (state.glowKind == GlowKind.MOTOR) { + state.glowColor = GlowParticleOptions.FIRE; + } else { + // A weapon with its own tint keeps it for the tracer; the default white burns warm. + int tint = state.color & 0xFFFFFF; + state.glowColor = tint == 0xFFFFFF ? TRACER : tint; + } + } + } + + private static GlowKind glowKind(ProjectileEntity entity) { + if (entity instanceof BulletEntity) { + return GlowKind.TRACER; + } + if (entity instanceof RocketEntity || entity instanceof MarkerRocketEntity || entity instanceof AAMissileEntity + || entity instanceof ASMissileEntity || entity instanceof ATMissileEntity || entity instanceof TvMissileEntity) { + return GlowKind.MOTOR; + } + return GlowKind.NONE; + } + + /** The original looked for water at the round and up to two blocks above it. */ + private static boolean inWater(Level level, ProjectileEntity e) { + for (int y = 0; y < 3; y++) { + if (level.getFluidState(BlockPos.containing(e.getX() + 0.5, e.getY() + 1.5 - y, e.getZ() + 0.5)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + @Override + public void render(State state, PoseStack pose, MultiBufferSource buffers, int packedLight) { + Camera camera = Minecraft.getInstance().gameRenderer.getMainCamera(); + Mesh mesh = state.visible ? MeshCache.INSTANCE.find("bullets", state.model) : null; + if (mesh != null) { + ResourceLocation texture = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/" + state.model + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + int color = state.color; + Geometry.submit(buffers, pose, RenderType.itemEntityTranslucentCull(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, FULL_BRIGHT, OverlayTexture.NO_OVERLAY, color)); + pose.popPose(); + } + if (state.visible) { + submitGlow(state, pose, buffers, camera); + } + super.render(state, pose, buffers, packedLight); + } + + private static void submitGlow(State state, PoseStack pose, MultiBufferSource buffers, Camera camera) { + if (state.glowKind == GlowKind.NONE || state.glow <= 0.0F) { + return; + } + double speed = state.travel.length(); + Vec3 back = speed > 1.0E-3 ? state.travel.scale(-1.0 / speed) : Vec3.ZERO; + switch (state.glowKind) { + case TRACER -> { + Vec3 toCamera = new Vec3(state.x - camera.getPosition().x, state.y - camera.getPosition().y, state.z - camera.getPosition().z); + Glow.streak(pose, buffers, toCamera, back.scale(Math.min(speed, MAX_TRACER)), 0.08F, state.glowColor, state.glow); + Glow.halo(pose, buffers, camera, Vec3.ZERO, 0.35F, state.glowColor, state.glow); + } + case MOTOR -> { + Vec3 nozzle = back.scale(MOTOR_OFFSET); + Glow.halo(pose, buffers, camera, nozzle, 1.1F, state.glowColor, state.glow); + Glow.halo(pose, buffers, camera, nozzle, 0.4F, GlowParticleOptions.FLASH, state.glow); + } + default -> { + } + } + } +} From 8151b0ef207352a40dadb8e6d748d702dc61c21c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:28 +0800 Subject: [PATCH 0952/1461] Find riders through PlayerRenderState on 1.21.5 --- .../mcheli/client/render/RiderPose.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java new file mode 100644 index 00000000..9555fba8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.entity.state.PlayerRenderState; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Tilts players riding a vehicle with its hull, as the original did from its render hook. + * + *

Vanilla keeps riders upright, so a banked plane left the pilot standing straight up through + * the canopy. The tilt is the hull's pitch and roll expressed in the world frame, applied about + * the rider's vehicle attachment point (the hip), which is the point that sits on the seat. The + * rider's own body yaw is applied by the renderer afterwards, so turning in the seat still works. + * + *

The entity render dispatcher pushes a pose around every entity, so nothing needs popping here. + */ +public final class RiderPose { + private RiderPose() { + } + + /** Called before a living entity is drawn, with the pose the renderer is about to use. */ + public static void beforeRender(LivingEntityRenderState renderState, PoseStack pose, float partialTicks) { + // Render states carry no entity reference; players are the ones that keep their id. + if (!(renderState instanceof PlayerRenderState state)) { + return; + } + ClientLevel level = Minecraft.getInstance().level; + Entity rider = level != null ? level.getEntity(state.id) : null; + if (rider == null || !(rider.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + float yaw = vehicle.getYRot(partialTicks); + float pitch = vehicle.getXRot(partialTicks); + float roll = vehicle.roll(partialTicks); + if (pitch == 0.0F && roll == 0.0F) { + return; + } + Quaternionf tilt = AircraftRotation.orientation(yaw, pitch, roll).rotateY((float) Math.toRadians(yaw)); + Vec3 hip = rider.getVehicleAttachmentPoint(vehicle); + pose.rotateAround(tilt, (float) hip.x, (float) hip.y, (float) hip.z); + } +} From 8001e151a28b1fab0af45f27f57e03bd072c038c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:28 +0800 Subject: [PATCH 0953/1461] Add client/render/ThrowableRenderer.java to the 1.21.5 shared sources --- .../client/render/ThrowableRenderer.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java new file mode 100644 index 00000000..6f2213ac --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +/** {@code MCH_RenderThrowable}: the grenade's own model from {@code models/throwable}. */ +public class ThrowableRenderer extends EntityRenderer { + public ThrowableRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + public static class State extends EntityRenderState { + String name = ""; + float yaw; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(ThrowableEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.name = entity.definitionName(); + state.yaw = entity.getYRot(partialTicks); + } + + @Override + public void render(State state, PoseStack pose, MultiBufferSource buffers, int packedLight) { + Mesh mesh = state.name.isEmpty() ? null : MeshCache.INSTANCE.get("throwable", state.name); + if (mesh != null) { + ResourceLocation texture = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/throwable/" + state.name + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = packedLight; + Geometry.submit(buffers, pose, RenderType.entityCutoutNoCull(texture), + (p, buffer) -> MeshRenderer.emit(mesh.groups(), p, buffer, light, OverlayTexture.NO_OVERLAY)); + pose.popPose(); + } + super.render(state, pose, buffers, packedLight); + } +} From be506a15275699e39ab9d94f9a3d82a83c3367ef Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:29 +0800 Subject: [PATCH 0954/1461] Add client/render/UavStationRenderer.java to the 1.21.5 shared sources --- .../client/render/UavStationRenderer.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java new file mode 100644 index 00000000..9d0e59e2 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +/** + * The UAV station and the portable controller, {@code MCH_RenderUavStation}. The station's screen + * lights up while a drone is flown from it; the controller's lid, keyboard half and display fold + * open as it is opened, and light up once fully open. + */ +public class UavStationRenderer extends EntityRenderer { + private static final String[] MODEL = {"uav_station", "uav_portable_controller"}; + /** The original greyed both a little. */ + private static final int TINT = 0xFFBFBFBF; + + public UavStationRenderer(EntityRendererProvider.Context context) { + super(context); + shadowRadius = 1.0F; + } + + public static class State extends EntityRenderState { + float yaw; + int kind; + boolean lit; + float cover; + } + + @Override + public State createRenderState() { + return new State(); + } + + @Override + public void extractRenderState(UavStationEntity entity, State state, float partialTicks) { + super.extractRenderState(entity, state, partialTicks); + state.yaw = entity.getYRot(partialTicks); + state.kind = entity.kind(); + state.cover = entity.cover(partialTicks); + state.lit = state.kind == UavStationEntity.PORTABLE ? state.cover > 0.95F : entity.isLinked(); + } + + @Override + public void render(State state, PoseStack pose, MultiBufferSource buffers, int packedLight) { + int index = state.kind == UavStationEntity.PORTABLE ? 1 : 0; + Mesh mesh = MeshCache.INSTANCE.get("", MODEL[index]); + if (mesh != null) { + ResourceLocation texture = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + MODEL[index] + (state.lit ? "_on" : "") + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + int light = packedLight; + if (index == 0) { + draw(pose, buffers, texture, mesh.groups(), light); + } else { + draw(pose, buffers, texture, mesh.part("$body"), light); + folding(pose, buffers, texture, mesh.part("$cover"), state.cover * 60.0F, -0.1812F, -0.3186F, light); + folding(pose, buffers, texture, mesh.part("$laptop_cover"), state.cover * 95.0F, -0.1808F, -0.0422F, light); + folding(pose, buffers, texture, mesh.part("$display"), state.cover * -85.0F, -0.1807F, 0.2294F, light); + } + pose.popPose(); + } + super.render(state, pose, buffers, packedLight); + } + + /** {@code renderRotPart}: turns a part about the x axis through its hinge. */ + private static void folding(PoseStack pose, MultiBufferSource buffers, ResourceLocation texture, List part, + float angle, float y, float z, int light) { + pose.pushPose(); + pose.translate(0.0F, y, z); + pose.mulPose(Axis.XN.rotationDegrees(angle)); + pose.translate(0.0F, -y, -z); + draw(pose, buffers, texture, part, light); + pose.popPose(); + } + + private static void draw(PoseStack pose, MultiBufferSource buffers, ResourceLocation texture, List groups, int light) { + if (!groups.isEmpty()) { + Geometry.submit(buffers, pose, RenderType.entityCutoutNoCull(texture), + (p, buffer) -> MeshRenderer.emit(groups, p, buffer, light, OverlayTexture.NO_OVERLAY, TINT)); + } + } +} From 9f7fdd199ebecddcd7d72fb4b8a9c1e91a68a77c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:29 +0800 Subject: [PATCH 0955/1461] Add client/screen/DraftingTableScreen.java to the 1.21.5 shared sources --- .../client/screen/DraftingTableScreen.java | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java new file mode 100644 index 00000000..a88e5213 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java @@ -0,0 +1,337 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.workshop.WorkshopHomeScreen; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.network.DraftingTablePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeCollection; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; +import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * The drafting table's recipe browser, {@code MCH_DraftingTableGui}. Pick a list (helicopters, + * planes, tanks, ground vehicles or equipment), step through its recipes, and Create makes the one + * shown into the output slot. The right page shows the original's illustrated description for + * equipment and a vehicle's numbers for vehicles; the original turned a 3D model there instead. + * + *

The recipes come from the player's recipe book, which holds every MCHeli recipe from login. + */ +public class DraftingTableScreen extends AbstractContainerScreen { + private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/drafting_table.png"); + private static final int TEXTURE_SIZE = 512; + /** The description pages take this part of their 512 square texture. */ + private static final int PAGE_WIDTH = 340; + private static final int PAGE_HEIGHT = 380; + private static final int PAGE_X = 210; + private static final int PAGE_Y = 20; + private static final int GRID_X = 121; + private static final int GRID_Y = 34; + private static final int TEXT = 0xFF6A3A00; + private static final int WHITE = 0xFFFFFFFF; + private static final String[] LISTS = {"helicopter", "plane", "vehicle", "tank", "item"}; + + private final List> lists = new ArrayList<>(); + private int list; + private int index; + private int page; + private int pageCount; + private @Nullable Button create; + private @Nullable Button previous; + private @Nullable Button next; + private @Nullable Button previousPage; + private @Nullable Button nextPage; + + public DraftingTableScreen(DraftingTableMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 400; + imageHeight = 240; + inventoryLabelX = 30; + inventoryLabelY = 130; + } + + @Override + protected void init() { + super.init(); + collectRecipes(); + for (int i = 0; i < LISTS.length; i++) { + int which = i; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.list." + LISTS[i]), b -> selectList(which)) + .bounds(leftPos + 20, topPos + 20 + i * 20, 90, 20).build()); + } + create = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.create"), b -> create()) + .bounds(leftPos + 120, topPos + 89, 50, 20).build()); + previous = addRenderableWidget(Button.builder(Component.literal("<<"), b -> step(-1)).bounds(leftPos + 120, topPos + 111, 36, 20).build()); + next = addRenderableWidget(Button.builder(Component.literal(">>"), b -> step(1)).bounds(leftPos + 155, topPos + 111, 35, 20).build()); + previousPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.previous_page"), b -> turnPage(-1)) + .bounds(leftPos + 210, topPos + 210, 60, 20).build()); + nextPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.next_page"), b -> turnPage(1)) + .bounds(leftPos + 270, topPos + 210, 60, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.workshop"), b -> openWorkshop()) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.drafting_table.workshop_hint"))) + .bounds(leftPos + 334, topPos + 210, 56, 20).build()); + selectList(list); + } + + /** The workshop is a screen of its own and needs no block, so the table's menu closes first. */ + private void openWorkshop() { + minecraft.player.closeContainer(); + minecraft.setScreen(new WorkshopHomeScreen()); + } + + /** Every MCHeli recipe the recipe book holds, sorted into the five lists by what it makes. */ + private void collectRecipes() { + lists.clear(); + for (int i = 0; i < LISTS.length; i++) { + lists.add(new ArrayList<>()); + } + ContextMap context = SlotDisplayContext.fromLevel(minecraft.level); + for (RecipeCollection collection : minecraft.player.getRecipeBook().getCollections()) { + for (RecipeDisplayEntry entry : collection.getRecipes()) { + ItemStack result = entry.display().result().resolveForFirstStack(context); + if (!result.isEmpty() && BuiltInRegistries.ITEM.getKey(result.getItem()).getNamespace().equals(MCHeli.MOD_ID)) { + lists.get(listOf(result.getItem())).add(entry); + } + } + } + for (List entries : lists) { + entries.sort(Comparator.comparing(e -> e.display().result().resolveForFirstStack(context).getHoverName().getString())); + } + } + + private static int listOf(Item item) { + if (item == ModItems.HELICOPTER.get()) { + return 0; + } + if (item == ModItems.PLANE.get()) { + return 1; + } + if (item == ModItems.VEHICLE.get()) { + return 2; + } + if (item == ModItems.TANK.get()) { + return 3; + } + return 4; + } + + private void selectList(int which) { + list = which; + index = 0; + showRecipe(); + } + + private void step(int direction) { + int size = lists.get(list).size(); + if (size > 0) { + index = Math.floorMod(index + direction, size); + showRecipe(); + } + } + + private void turnPage(int direction) { + page = Math.clamp(page + direction, 0, Math.max(pageCount - 1, 0)); + updateButtons(); + } + + private void showRecipe() { + page = 0; + ItemStack result = result(); + pageCount = descriptionPages(result); + updateButtons(); + } + + private @Nullable RecipeDisplayEntry current() { + List entries = lists.get(list); + return entries.isEmpty() ? null : entries.get(Math.min(index, entries.size() - 1)); + } + + private ItemStack result() { + RecipeDisplayEntry entry = current(); + return entry != null ? entry.display().result().resolveForFirstStack(SlotDisplayContext.fromLevel(minecraft.level)) : ItemStack.EMPTY; + } + + /** The original's description pages for equipment: {@code drafting_table_desc/_.png}. */ + private int descriptionPages(ItemStack result) { + if (result.isEmpty() || AircraftItem.vehicleOf(result) != null) { + return 0; + } + String name = descriptionName(result); + int pages = 0; + while (pages < 10 && minecraft.getResourceManager().getResource(pageTexture(name, pages)).isPresent()) { + pages++; + } + return pages; + } + + private static String descriptionName(ItemStack result) { + if (result.getItem() == ModItems.THROWABLE.get()) { + String throwable = result.get(ModDataComponents.THROWABLE.get()); + return throwable != null ? throwable : "throwable"; + } + return BuiltInRegistries.ITEM.getKey(result.getItem()).getPath(); + } + + private static ResourceLocation pageTexture(String name, int page) { + return ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/drafting_table_desc/" + name + "_" + page + ".png"); + } + + private boolean canCreate() { + RecipeDisplayEntry entry = current(); + if (entry == null || menu.getSlot(menu.slots.size() - 1).hasItem() && !minecraft.player.getAbilities().instabuild) { + return false; + } + if (minecraft.player.getAbilities().instabuild) { + return true; + } + StackedItemContents contents = new StackedItemContents(); + minecraft.player.getInventory().fillStackedContents(contents); + return entry.canCraft(contents); + } + + private void create() { + RecipeDisplayEntry entry = current(); + if (entry != null) { + ModNetwork.sendToServer(new DraftingTablePacket(entry.id())); + } + } + + private void updateButtons() { + if (create == null || previous == null || next == null || previousPage == null || nextPage == null) { + return; + } + boolean any = current() != null; + create.active = any && canCreate(); + previous.active = lists.get(list).size() > 1; + next.active = lists.get(list).size() > 1; + previousPage.visible = pageCount > 1; + nextPage.visible = pageCount > 1; + previousPage.active = page > 0; + nextPage.active = page < pageCount - 1; + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, TEXTURE_SIZE, TEXTURE_SIZE); + if (pageCount > 0) { + String name = descriptionName(result()); + graphics.blit(RenderType::guiTextured, pageTexture(name, page), leftPos + PAGE_X, topPos + PAGE_Y, 0.0F, 0.0F, + PAGE_WIDTH / 2, PAGE_HEIGHT / 2, PAGE_WIDTH, PAGE_HEIGHT, TEXTURE_SIZE, TEXTURE_SIZE); + } + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, TEXT, false); + RecipeDisplayEntry entry = current(); + if (entry == null) { + graphics.drawString(font, Component.translatable("gui.mcheli.drafting_table.empty"), 120, 20, TEXT, false); + return; + } + ItemStack result = result(); + graphics.drawString(font, font.plainSubstrByWidth(result.getHoverName().getString(), 85), 120, 20, WHITE, true); + graphics.drawString(font, (index + 1) + " / " + lists.get(list).size(), 120, 134, TEXT, false); + ingredients(graphics, entry.display(), mouseX, mouseY); + graphics.renderItem(result, 178, 52); + if (pageCount == 0) { + details(graphics, result); + } + } + + /** {@code drawItemRecipe}: the 3x3 grid, each slot cycling through what it accepts once a second. */ + private void ingredients(GuiGraphics graphics, RecipeDisplay display, int mouseX, int mouseY) { + ContextMap context = SlotDisplayContext.fromLevel(minecraft.level); + List slots; + int width; + switch (display) { + case ShapedCraftingRecipeDisplay shaped -> { + slots = shaped.ingredients(); + width = shaped.width(); + } + case ShapelessCraftingRecipeDisplay shapeless -> { + slots = shapeless.ingredients(); + width = 3; + } + default -> { + return; + } + } + int tick = (int) (minecraft.level.getGameTime() / 20); + for (int i = 0; i < slots.size() && i < 9; i++) { + List options = slots.get(i).resolveForStacks(context); + if (options.isEmpty()) { + continue; + } + ItemStack stack = options.get(tick % options.size()); + int x = GRID_X + (i % width) * 18; + int y = GRID_Y + (i / width) * 18; + graphics.renderItem(stack, x, y); + if (mouseX - leftPos >= x && mouseX - leftPos < x + 16 && mouseY - topPos >= y && mouseY - topPos < y + 16) { + graphics.renderTooltip(font, stack, mouseX - leftPos, mouseY - topPos); + } + } + } + + /** The vehicle's numbers, where the original turned its model. */ + private void details(GuiGraphics graphics, ItemStack result) { + String name = AircraftItem.vehicleOf(result); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + return; + } + List lines = new ArrayList<>(); + lines.add(Component.translatable("gui.mcheli.drafting_table.hp", def.maxHp)); + lines.add(Component.translatable("gui.mcheli.drafting_table.speed", "%.2f".formatted(def.speed))); + lines.add(Component.translatable("gui.mcheli.drafting_table.seats", def.seatCount())); + lines.add(Component.translatable("gui.mcheli.drafting_table.fuel", def.maxFuel)); + lines.add(Component.translatable("gui.mcheli.drafting_table.weapons")); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + String weaponName = weapon != null && weapon.displayName != null ? weapon.displayName : group.weaponType(); + lines.add(Component.literal(" " + weaponName + (group.mounts().size() > 1 ? " x" + group.mounts().size() : ""))); + } + int y = PAGE_Y + 20; + for (Component line : lines) { + if (y > PAGE_Y + 180) { + break; + } + graphics.drawString(font, font.split(line, 170).getFirst(), PAGE_X, y, TEXT, false); + y += 10; + } + } +} From ea1edd8c94810a9662fd130a524566cb9c47f4dc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:30 +0800 Subject: [PATCH 0956/1461] Add client/screen/TeamManagerScreen.java to the 1.21.5 shared sources --- .../client/screen/TeamManagerScreen.java | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java new file mode 100644 index 00000000..3ec2606f --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java @@ -0,0 +1,136 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.TeamPacket; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.scores.PlayerTeam; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * The team manager, {@code MCH_GuiScoreboard_Main} and {@code MCH_GuiScoreboard_CreateTeam}, for + * operators setting up a match: switch PvP, shuffle everyone into the teams, make a team, send + * everyone to their spawn point and clear away every vehicle. The teams and their players are + * listed on the right. + */ +public class TeamManagerScreen extends Screen { + private static final int WIDTH = 150; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + + private @Nullable EditBox name; + private int color = TeamManager.COLORS.indexOf(ChatFormatting.RED); + private boolean friendlyFire; + private @Nullable Button colorButton; + private @Nullable Button friendlyFireButton; + private @Nullable Button createButton; + + public TeamManagerScreen() { + super(Component.translatable("gui.mcheli.team.title")); + } + + @Override + protected void init() { + int left = width / 2 - WIDTH - 10; + int y = 40; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.pvp"), b -> send(TeamPacket.Action.PVP)) + .bounds(left, y, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.shuffle"), b -> send(TeamPacket.Action.SHUFFLE)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.shuffle_hint"))) + .bounds(left, y + 22, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.jump_spawn"), b -> send(TeamPacket.Action.JUMP_TO_SPAWN)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.jump_spawn_hint"))) + .bounds(left, y + 44, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.destroy_all"), b -> send(TeamPacket.Action.DESTROY_ALL)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.destroy_all_hint"))) + .bounds(left, y + 66, WIDTH, 20).build()); + + int create = y + 104; + name = addRenderableWidget(new EditBox(font, left, create, WIDTH, 20, Component.translatable("gui.mcheli.team.name"))); + name.setMaxLength(16); + name.setHint(Component.translatable("gui.mcheli.team.name")); + name.setResponder(text -> updateButtons()); + colorButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + color = (color + 1) % TeamManager.COLORS.size(); + updateButtons(); + }).bounds(left, create + 22, WIDTH, 20).build()); + friendlyFireButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + friendlyFire = !friendlyFire; + updateButtons(); + }).bounds(left, create + 44, WIDTH, 20).build()); + createButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.create"), b -> { + ModNetwork.sendToServer(new TeamPacket(TeamPacket.Action.CREATE_TEAM, name.getValue(), color, friendlyFire)); + name.setValue(""); + }).bounds(left, create + 66, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 - 50, height - 28, 100, 20).build()); + updateButtons(); + } + + private void send(TeamPacket.Action action) { + ModNetwork.sendToServer(new TeamPacket(action)); + } + + private void updateButtons() { + if (colorButton == null || friendlyFireButton == null || createButton == null || name == null) { + return; + } + ChatFormatting teamColor = TeamManager.COLORS.get(color); + colorButton.setMessage(Component.translatable("gui.mcheli.team.color", + Component.literal(teamColor.getName()).withStyle(teamColor))); + friendlyFireButton.setMessage(Component.translatable(friendlyFire ? "gui.mcheli.team.friendly_fire_on" : "gui.mcheli.team.friendly_fire_off")); + String value = name.getValue().trim(); + createButton.active = !value.isEmpty() && !value.contains(" "); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 15, TEXT); + int left = width / 2 - WIDTH - 10; + graphics.drawString(font, Component.translatable("gui.mcheli.team.new_team"), left, 132, DIM); + teams(graphics, width / 2 + 10, 40); + } + + /** The scoreboard's teams and who is in them, as this client sees the scoreboard. */ + private void teams(GuiGraphics graphics, int x, int y) { + if (minecraft.level == null) { + return; + } + List teams = new ArrayList<>(minecraft.level.getScoreboard().getPlayerTeams()); + if (teams.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.team.none"), x, y, DIM); + return; + } + int bottom = height - 40; + for (PlayerTeam team : teams) { + if (y > bottom) { + break; + } + graphics.drawString(font, team.getFormattedDisplayName(), x, y, TEXT); + y += 11; + for (String player : team.getPlayers()) { + if (y > bottom) { + break; + } + graphics.drawString(font, " " + player, x, y, DIM); + y += 10; + } + y += 4; + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From 0814734e03d8fc49b7f30ded8424ceb968324d79 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:30 +0800 Subject: [PATCH 0957/1461] Add client/screen/UavStationScreen.java to the 1.21.5 shared sources --- .../client/screen/UavStationScreen.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java new file mode 100644 index 00000000..9dca5645 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java @@ -0,0 +1,96 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_GuiUavStation}: the slot for the drone, the launch offset + * per axis with its buttons, and Continue to take the last drone flown from here again. + */ +public class UavStationScreen extends AbstractContainerScreen { + private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/uav_station.png"); + private static final String[] AXES = {"X", "Y", "Z"}; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF0000; + + private @Nullable Button continueButton; + + public UavStationScreen(UavStationMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = 166; + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + int x = width / 2 - 5; + int y = height / 2 - 76; + for (int axis = 0; axis < 3; axis++) { + for (int step = 0; step < UavStationMenu.STEPS.length; step++) { + int s = UavStationMenu.STEPS[step]; + int id = axis * 4 + step; + addRenderableWidget(Button.builder(Component.literal((s > 0 ? "+" : "") + s), b -> press(id)) + .bounds(x + step * 20, y + axis * 22, 20, 20).build()); + } + } + continueButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.uav_station.continue"), b -> press(UavStationMenu.BUTTON_CONTINUE)) + .bounds(x - 77, y + 44, 50, 20).build()); + continueButton.active = menu.canContinue(); + } + + @Override + protected void containerTick() { + super.containerTick(); + if (continueButton != null) { + continueButton.active = menu.canContinue(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, 256, 256); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + UavStationEntity station = menu.station(); + ItemStack stack = menu.getSlot(0).getItem(); + AircraftDefinition def = stack.isEmpty() ? null : UavStationEntity.definitionOf(stack); + Component label; + int color = WHITE; + if (!stack.isEmpty() && (def == null || !def.uav)) { + label = Component.translatable("gui.mcheli.uav_station.not_uav"); + color = RED; + } else if (station != null && station.kind() == UavStationEntity.PORTABLE && def != null && !def.smallUav) { + label = Component.translatable("gui.mcheli.uav_station.small_only"); + color = RED; + } else { + label = title; + } + graphics.drawString(font, label, 8, 6, color, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, WHITE, false); + for (int axis = 0; axis < 3; axis++) { + graphics.drawString(font, String.format("%s.%+d", AXES[axis], menu.offset(axis)), 58, 15 + axis * 22, WHITE, false); + } + } +} From 0b053d4346692aea8b7dbea2e0aa16981a285bda Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:31 +0800 Subject: [PATCH 0958/1461] Add client/screen/VehicleScreen.java to the 1.21.5 shared sources --- .../mcheli/client/screen/VehicleScreen.java | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java new file mode 100644 index 00000000..8969f1b3 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java @@ -0,0 +1,182 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGui}: fuel and parachute slots on the left, the weapon + * panel on the right with its reload cost and button, and a button into the cargo chest for + * vehicles that have one. The panel is drawn from the chest texture so it looks like vanilla. + */ +public class VehicleScreen extends AbstractContainerScreen { + private static final ResourceLocation CHEST = ResourceLocation.withDefaultNamespace("textures/gui/container/generic_54.png"); + private static final ResourceLocation SLOT = ResourceLocation.withDefaultNamespace("container/slot"); + /** Height of the part above the player inventory. */ + private static final int TOP = 88; + private static final int PANEL_X = 72; + private static final int PANEL_WIDTH = 96; + private static final int LABEL = 0xFF404040; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF5555; + private static final int GREEN = 0xFF55FF55; + + private int weapon; + private boolean cargoButton; + private @Nullable Button reload; + private @Nullable Button previous; + private @Nullable Button next; + + public VehicleScreen(VehicleMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = TOP + 96; + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + previous = addRenderableWidget(Button.builder(Component.literal("<"), b -> step(-1)) + .bounds(leftPos + PANEL_X, topPos + 68, 16, 16).build()); + reload = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.reload"), b -> press(weapon)) + .bounds(leftPos + PANEL_X + 18, topPos + 68, PANEL_WIDTH - 36, 16).build()); + next = addRenderableWidget(Button.builder(Component.literal(">"), b -> step(1)) + .bounds(leftPos + PANEL_X + PANEL_WIDTH - 16, topPos + 68, 16, 16).build()); + AircraftEntity vehicle = menu.vehicle(); + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + cargoButton = def != null && def.cargoRows() > 0; + if (cargoButton) { + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.cargo"), b -> press(VehicleMenu.BUTTON_CARGO)) + .bounds(leftPos + imageWidth - 52, topPos + 3, 46, 13).build()); + } + updateButtons(); + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + private int weaponCount() { + AircraftEntity vehicle = menu.vehicle(); + VehicleWeapons weapons = vehicle != null ? vehicle.weapons() : null; + return weapons != null ? Math.min(weapons.sets().size(), VehicleMenu.MAX_WEAPONS) : 0; + } + + private void step(int direction) { + int count = weaponCount(); + if (count > 0) { + weapon = Math.floorMod(weapon + direction, count); + updateButtons(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + private void updateButtons() { + if (reload == null || previous == null || next == null) { + return; + } + int count = weaponCount(); + reload.visible = count > 0; + previous.visible = count > 1; + next.visible = count > 1; + reload.active = menu.canReload(weapon); + AircraftEntity vehicle = menu.vehicle(); + boolean grounded = vehicle != null && vehicle.canSupply(); + reload.setTooltip(grounded ? null : Tooltip.create(Component.translatable("gui.mcheli.vehicle.reload_hint"))); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + int x = leftPos; + int y = topPos; + // The chest's title bar, its plain middle stretched down to the inventory, then the inventory. + graphics.blit(RenderType::guiTextured, CHEST, x, y, 0.0F, 0.0F, imageWidth, 17, 256, 256); + graphics.blit(RenderType::guiTextured, CHEST, x, y + 17, 0.0F, 12.0F, imageWidth, TOP - 17, imageWidth, 1, 256, 256); + graphics.blit(RenderType::guiTextured, CHEST, x, y + TOP, 0.0F, 126.0F, imageWidth, 96, 256, 256); + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + graphics.blitSprite(RenderType::guiTextured, SLOT, x + VehicleMenu.FUEL_X + i * 18 - 1, y + VehicleMenu.FUEL_Y - 1, 18, 18); + } + for (int i = 0; i < menu.parachuteSlots(); i++) { + graphics.blitSprite(RenderType::guiTextured, SLOT, x + VehicleMenu.PARACHUTE_X + i * 18 - 1, y + VehicleMenu.PARACHUTE_Y - 1, 18, 18); + } + graphics.fill(x + PANEL_X - 2, y + 17, x + PANEL_X + PANEL_WIDTH + 2, y + 66, 0xFF373737); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + AircraftEntity vehicle = menu.vehicle(); + // Long names only give way to the Cargo button, where there is one. + int titleWidth = cargoButton ? imageWidth - 64 : imageWidth - 16; + graphics.drawString(font, font.plainSubstrByWidth(title.getString(), titleWidth), titleLabelX, titleLabelY, LABEL, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, LABEL, false); + if (vehicle == null) { + return; + } + if (vehicle.maxFuel() > 0) { + // A percentage and a gauge: the exact units would run into the weapon panel. + int percent = Math.round(vehicle.fuelRatio() * 100.0F); + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.fuel", percent), VehicleMenu.FUEL_X, VehicleMenu.FUEL_Y - 11, LABEL, false); + int gaugeY = VehicleMenu.FUEL_Y + 18; + int width = VehicleInventory.FUEL_SLOTS * 18 - 2; + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width, gaugeY + 3, 0xFF373737); + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width * percent / 100, gaugeY + 3, percent <= 10 ? RED : GREEN); + } + if (menu.parachuteSlots() > 0) { + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.parachute"), VehicleMenu.PARACHUTE_X, VehicleMenu.PARACHUTE_Y - 11, LABEL, false); + } + weaponPanel(graphics, vehicle); + } + + /** Name, rounds carried against the maximum, and what one reload costs. */ + private void weaponPanel(GuiGraphics graphics, AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null && weaponCount() > 0 ? weapons.set(Math.min(weapon, weaponCount() - 1)) : null; + if (set == null) { + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.no_weapons"), PANEL_X, 20, WHITE, false); + return; + } + graphics.drawString(font, font.plainSubstrByWidth(set.displayName(), PANEL_WIDTH), PANEL_X, 20, WHITE, false); + int rounds = menu.rounds(weapon); + int max = set.maxAmmo(); + int color = rounds == 0 && max > 0 ? RED : rounds >= max ? GREEN : WHITE; + graphics.drawString(font, rounds + " / " + max, PANEL_X, 31, color, false); + int itemX = PANEL_X; + for (RoundItem cost : set.definition().roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null) { + continue; + } + graphics.renderItem(new ItemStack(item), itemX, 42); + graphics.drawString(font, String.valueOf(cost.count()), itemX + 17, 47, WHITE, false); + itemX += 32; + if (itemX > PANEL_X + PANEL_WIDTH - 16) { + break; + } + } + } +} From dd90c92f7e45c5b2129576706914fee7dc6ecb4b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:31 +0800 Subject: [PATCH 0959/1461] Add client/sound/EngineSound.java to the 1.21.5 shared sources --- .../mcheli/client/sound/EngineSound.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java new file mode 100644 index 00000000..b32040f5 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** + * A looping sound of one vehicle: its engine, or the rotor loop some vehicles have alongside it. + * Ported from {@code MCH_SoundUpdater}. + * + *

Volume and pitch follow the vehicle's throttle every tick, the pitch along the definition's + * curve if it has one and otherwise the way the kind of vehicle always has. For listeners outside + * the vehicle the sound is placed between them and the vehicle, so that the 16 block range of a + * normal sound stretches to the definition's {@code SoundRange}, and the pitch bends slightly as the + * vehicle approaches or leaves, which is what the original did. + */ +final class EngineSound extends RuntimeSoundInstance { + private static final float MAX_DOPPLER = 0.2F; + + private final AircraftEntity vehicle; + private final EngineSoundSource source; + private final boolean rotor; + private float doppler; + private double lastDistance = -1.0; + + EngineSound(AircraftEntity vehicle, EngineSoundSource source, String sound, boolean rotor) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.vehicle = vehicle; + this.source = source; + this.rotor = rotor; + this.looping = true; + this.delay = 0; + update(); + } + + @Override + public void tick() { + if (vehicle.isRemoved() || vehicle.definition() == null) { + stop(); + return; + } + update(); + if (volume <= 0.0F) { + stop(); + } + } + + private void update() { + AircraftDefinition def = vehicle.definition(); + LocalPlayer player = Minecraft.getInstance().player; + if (def == null || player == null) { + volume = 0.0F; + return; + } + volume = (rotor ? def.rotorSoundVolume : def.soundVolume) * source.engineVolume(); + PitchCurve curve = rotor ? def.rotorSoundPitchCurve : def.soundPitchCurve; + float pitch = def.soundPitch * (curve != null ? curve.at(vehicle.visibleThrottle()) : source.enginePitch()); + boolean aboard = player.getRootVehicle() == vehicle.getRootVehicle(); + if (aboard) { + x = vehicle.getX(); + y = vehicle.getY(); + z = vehicle.getZ(); + } else { + double dx = vehicle.getX() - player.getX(); + double dz = vehicle.getZ() - player.getZ(); + double distance = Math.sqrt(dx * dx + dz * dz); + if (lastDistance >= 0.0) { + double closing = lastDistance - distance; + if (Math.abs(closing) > 0.3) { + doppler = Math.clamp(doppler + (float) (closing / 40.0), -MAX_DOPPLER, MAX_DOPPLER); + } + } + doppler *= 0.9F; + pitch += doppler; + lastDistance = distance; + + double scale = def.soundRange / 16.0; + x = player.getX() + dx / scale; + y = player.getY() + (vehicle.getY() - player.getY()) / scale; + z = player.getZ() + dz / scale; + } + this.pitch = Math.max(pitch, 0.0F); + } + + @Override + public boolean canStartSilent() { + return true; + } +} From d1ddb611fb1e4cb88fdbe878e94738cd447be893 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:31 +0800 Subject: [PATCH 0960/1461] Add client/sound/EngineSoundPlayer.java to the 1.21.5 shared sources --- .../client/sound/EngineSoundPlayer.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java new file mode 100644 index 00000000..62e52284 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.Entity; + +import java.util.HashMap; +import java.util.Map; + +/** + * Starts engine and rotor loops for vehicles near the player and restarts them after they fall + * silent. The sounds themselves track their vehicle until it stops or disappears. + */ +public final class EngineSoundPlayer { + /** Ticks to wait before restarting a loop, as the original did, so a stuttering engine does not spam. */ + private static final int RESTART_DELAY = 20; + + /** By {@link #key}, since a vehicle can have two loops. */ + private static final Map PLAYING = new HashMap<>(); + private static final Map COOLDOWN = new HashMap<>(); + + private EngineSoundPlayer() { + } + + public static void tick() { + Minecraft mc = Minecraft.getInstance(); + ClientLevel level = mc.level; + LocalPlayer player = mc.player; + if (level == null || player == null || mc.isPaused()) { + if (level == null) { + PLAYING.clear(); + COOLDOWN.clear(); + } + return; + } + PLAYING.values().removeIf(EngineSound::isStopped); + COOLDOWN.replaceAll((id, ticks) -> ticks - 1); + COOLDOWN.values().removeIf(ticks -> ticks <= 0); + + for (Entity entity : level.entitiesForRendering()) { + if (!(entity instanceof AircraftEntity vehicle) || !(entity instanceof EngineSoundSource source)) { + continue; + } + AircraftDefinition def = vehicle.definition(); + if (def == null) { + continue; + } + double range = def.soundRange * 1.2; + if (vehicle.distanceToSqr(player) > range * range) { + continue; + } + start(mc, vehicle, source, def.soundOrDefault(), def.soundVolume, false); + start(mc, vehicle, source, def.rotorSound, def.rotorSoundVolume, true); + } + } + + private static void start(Minecraft mc, AircraftEntity vehicle, EngineSoundSource source, String sound, float volume, boolean rotor) { + long key = key(vehicle, rotor); + if (sound.isEmpty() || PLAYING.containsKey(key) || COOLDOWN.containsKey(key) || volume * source.engineVolume() <= 0.01F) { + return; + } + EngineSound loop = new EngineSound(vehicle, source, sound, rotor); + mc.getSoundManager().play(loop); + PLAYING.put(key, loop); + COOLDOWN.put(key, RESTART_DELAY); + } + + private static long key(AircraftEntity vehicle, boolean rotor) { + return (long) vehicle.getId() << 1 | (rotor ? 1L : 0L); + } +} From aa1bffa79e1039349a31ae6404fb77cefc7070ae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:32 +0800 Subject: [PATCH 0961/1461] Add client/sound/OneShotSound.java to the 1.21.5 shared sources --- .../mcheli/client/sound/OneShotSound.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java new file mode 100644 index 00000000..31825a4f --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java @@ -0,0 +1,22 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** A sound from a content pack played once at a point, such as a gun firing. */ +public final class OneShotSound extends RuntimeSoundInstance { + public OneShotSound(String sound, double x, double y, double z, float volume, float pitch) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.x = x; + this.y = y; + this.z = z; + this.volume = volume; + this.pitch = pitch; + } + + @Override + public void tick() { + // It stays where it was started and ends when the file does. + } +} From 978e8d01bb67d4c3c80df14d5ac5057b067561a0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:32 +0800 Subject: [PATCH 0962/1461] Add client/sound/RuntimeSoundInstance.java to the 1.21.5 shared sources --- .../client/sound/RuntimeSoundInstance.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java new file mode 100644 index 00000000..e24bf68d --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -0,0 +1,79 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.JOrbisAudioStream; +import net.minecraft.client.sounds.LoopingAudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.client.sounds.WeighedSoundEvents; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.Util; +import net.minecraft.util.valueproviders.ConstantFloat; +import org.jspecify.annotations.Nullable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * A sound that plays through the sound manager when a resource pack defines its event, and + * otherwise streams the OGG file a runtime root holds for it ({@link RuntimeAssets#soundFile}). + * Content added while the game runs can bring sounds this way without touching + * {@code sounds.json} or reloading resources. + */ +public abstract class RuntimeSoundInstance extends AbstractTickableSoundInstance { + private @Nullable Path file; + + protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSource random) { + super(event, source, random); + } + + @Override + public @Nullable WeighedSoundEvents resolve(SoundManager manager) { + WeighedSoundEvents events = super.resolve(manager); + if (events != null) { + return events; + } + file = RuntimeAssets.soundFile(location.getPath()); + if (file == null) { + return null; + } + // Streamed, so the file is read as it plays and never cached as a buffer. + sound = new Sound(location, ConstantFloat.of(1.0F), ConstantFloat.of(1.0F), 1, Sound.Type.FILE, true, false, 16); + return new WeighedSoundEvents(location, null); + } + + /** + * Forge and NeoForge call this through their sound instance extension, which it overrides + * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + */ + public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { + return openStream(buffers, sound.getPath(), looping); + } + + /** Streams the file found on disk, or the resource {@code path} when there is none. */ + public CompletableFuture openStream(SoundBufferLibrary buffers, ResourceLocation path, boolean looping) { + Path source = file; + if (source == null) { + return buffers.getStream(path, looping); + } + return CompletableFuture.supplyAsync(() -> { + try { + InputStream in = new BufferedInputStream(Files.newInputStream(source)); + return looping ? new LoopingAudioStream(JOrbisAudioStream::new, in) : new JOrbisAudioStream(in); + } catch (IOException e) { + throw new CompletionException(e); + } + }, Util.nonCriticalIoPool()); + } +} From bbc382c7770493b902641324ea050fc34f7090a8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:33 +0800 Subject: [PATCH 0963/1461] Add client/workshop/WorkshopClient.java to the 1.21.5 shared sources --- .../client/workshop/WorkshopClient.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java new file mode 100644 index 00000000..26401727 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSubmitPacket; +import com.jnhprojects.mcheli.network.TestFlightPacket; +import net.minecraft.client.Minecraft; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; + +/** + * What the workshop does beyond its screens: takes the player to a test flight and back, and sends + * a pack to the server. The screen stays alive during a flight, so the undo history and the view are + * as they were when the pilot gets out. + */ +public final class WorkshopClient { + /** If no vehicle has come after this long, the server turned the flight down and said why. */ + private static final int SPAWN_TIMEOUT_TICKS = 100; + /** About 1.2 MB a second, so a download-sized pack does not crowd out the game's own packets. */ + private static final int SUBMIT_CHUNKS_PER_TICK = 2; + + private static @Nullable WorkshopScreen waiting; + private static boolean airborne; + private static int ticks; + private static byte @Nullable [] submitting; + private static int submitted; + + private WorkshopClient() { + } + + static void startTestFlight(WorkshopScreen screen, String vehicle) { + waiting = screen; + airborne = false; + ticks = 0; + Minecraft.getInstance().setScreen(null); + ModNetwork.sendToServer(new TestFlightPacket(vehicle)); + } + + /** Starts sending a pack's zip to the server, a few chunks a tick. */ + static void submit(byte[] zip) { + submitting = zip; + submitted = 0; + } + + /** How far along the zip being sent is, from 0 to 100, or -1 when none is. */ + static int submitProgress() { + byte[] zip = submitting; + return zip != null ? (int) (submitted * 100L / zip.length) : -1; + } + + public static void tick() { + tickSubmit(); + WorkshopScreen screen = waiting; + Minecraft mc = Minecraft.getInstance(); + if (screen == null || mc.player == null) { + return; + } + boolean flying = mc.player.getVehicle() instanceof AircraftEntity; + airborne |= flying; + ticks++; + if (airborne ? !flying : ticks > SPAWN_TIMEOUT_TICKS) { + waiting = null; + // Not over another screen the player has opened in flight, such as the pause menu. + if (mc.screen == null) { + mc.setScreen(screen); + } + } + } + + private static void tickSubmit() { + byte[] zip = submitting; + if (zip == null) { + return; + } + for (int i = 0; i < SUBMIT_CHUNKS_PER_TICK && submitted < zip.length; i++) { + int end = Math.min(submitted + PackSubmitPacket.CHUNK_SIZE, zip.length); + ModNetwork.sendToServer(new PackSubmitPacket(submitted, zip.length, Arrays.copyOfRange(zip, submitted, end))); + submitted = end; + } + if (submitted >= zip.length) { + submitting = null; + } + } + + public static void loggedOut() { + submitting = null; + WorkshopScreen screen = waiting; + waiting = null; + if (screen != null) { + screen.releaseAssets(); + } + } +} From 4d013169fa041fe0e29be4d4b97ddf29ca1bd2ec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:33 +0800 Subject: [PATCH 0964/1461] Add client/workshop/WorkshopHomeScreen.java to the 1.21.5 shared sources --- .../client/workshop/WorkshopHomeScreen.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java new file mode 100644 index 00000000..cc44546a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java @@ -0,0 +1,201 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.workshop.ModelImport; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopKind; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Where the workshop starts: the vehicles in the content packs on the left, ready to open, and on + * the right a form to start a new one from an OBJ file, which can be dropped on the window. + */ +public class WorkshopHomeScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int FORM_WIDTH = 190; + private static final int MAX_LIST_WIDTH = 300; + private static final int LINE_HEIGHT = 10; + + private final List entries; + private int scroll; + private WorkshopKind kind = WorkshopKind.HELICOPTER; + private String vehicleText = ""; + private String packText = ""; + private String modelText = ""; + private @Nullable Component error; + private @Nullable EditBox vehicleBox; + private @Nullable EditBox packBox; + private @Nullable EditBox modelBox; + + public WorkshopHomeScreen() { + super(Component.translatable("gui.mcheli.workshop.title")); + entries = WorkshopProject.list(MCHeli.packs().folder()); + } + + @Override + protected void init() { + listRows(); + form(); + } + + private int listWidth() { + return Math.clamp(width - FORM_WIDTH - 30, 100, MAX_LIST_WIDTH); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + private void listRows() { + int rows = visibleRows(); + for (int i = scroll; i < entries.size() && i < scroll + rows; i++) { + WorkshopProject.Entry entry = entries.get(i); + String suffix = entry.zipped() ? " (zip)" : ""; + addRenderableWidget(Button.builder(Component.literal(entry.pack() + " / " + entry.vehicle() + suffix), b -> open(entry)) + .bounds(10, TOP + (i - scroll) * ROW, listWidth(), 20).build()); + } + } + + private void form() { + int x = width - FORM_WIDTH - 10; + int y = TOP; + vehicleBox = box(x, y + 10, Component.translatable("gui.mcheli.workshop.new.vehicle"), vehicleText, 64); + vehicleBox.setResponder(text -> { + // The pack follows the vehicle's name until the player gives it its own. + if (packBox != null && packBox.getValue().equals(vehicleText)) { + packBox.setValue(text); + } + vehicleText = text; + }); + packBox = box(x, y + 44, Component.translatable("gui.mcheli.workshop.new.pack"), packText, 64); + packBox.setResponder(text -> packText = text); + addRenderableWidget(Button.builder(kindName(), b -> { + kind = WorkshopKind.values()[(kind.ordinal() + 1) % WorkshopKind.values().length]; + b.setMessage(kindName()); + }).bounds(x, y + 68, FORM_WIDTH, 20).build()); + modelBox = box(x, y + 102, Component.translatable("gui.mcheli.workshop.new.model"), modelText, 1024); + modelBox.setResponder(text -> modelText = text); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.new.create"), b -> create()) + .bounds(x, y + 126, FORM_WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.home.packs"), b -> minecraft.setScreen(new WorkshopPacksScreen())) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private EditBox box(int x, int y, Component name, String value, int maxLength) { + EditBox box = new EditBox(font, x, y, FORM_WIDTH, 18, name); + box.setMaxLength(maxLength); + box.setValue(value); + return addRenderableWidget(box); + } + + private Component kindName() { + return Component.translatable("gui.mcheli.workshop.new.kind", Component.translatable("gui.mcheli.workshop.kind." + kind.id())); + } + + private void open(WorkshopProject.Entry entry) { + try { + minecraft.setScreen(new WorkshopScreen(WorkshopProject.open(MCHeli.packs().folder(), entry))); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + private void create() { + String vehicle = vehicleText.trim(); + AircraftDefinition existing = MCHeli.content().any(vehicle); + if (existing != null && existing.pack().equals(ContentSource.ORIGINAL)) { + // The import's models come from a resource pack, which would win over this vehicle's. + error = Component.translatable("gui.mcheli.workshop.new.name_taken", vehicle); + return; + } + try { + Path obj = Path.of(modelText.trim().replace("\"", "")); + String author = minecraft.player != null ? minecraft.player.getGameProfile().getName() : ""; + WorkshopProject project = WorkshopProject.create(MCHeli.packs().folder(), packText.trim(), kind, vehicle, author, + obj, ModelImport.findTexture(obj)); + minecraft.setScreen(new WorkshopScreen(project)); + } catch (InvalidPathException e) { + error = Component.translatable("gui.mcheli.workshop.new.bad_path", modelText); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + /** A file dropped on the window fills in the form, its name standing in for the vehicle's. */ + @Override + public void onFilesDrop(List files) { + if (files.isEmpty() || modelBox == null || vehicleBox == null) { + return; + } + Path file = files.getFirst(); + modelBox.setValue(file.toString()); + if (vehicleText.isEmpty()) { + String name = file.getFileName().toString(); + int dot = name.lastIndexOf('.'); + vehicleBox.setValue((dot > 0 ? name.substring(0, dot) : name).toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "-")); + } + error = null; + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int max = Math.max(entries.size() - visibleRows(), 0); + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, max); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 10, TEXT); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.open"), 10, TOP - 12, DIM); + if (entries.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.none"), 10, TOP, DIM); + } + int x = width - FORM_WIDTH - 10; + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.new"), x, TOP - 12, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.vehicle"), x, TOP, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.pack"), x, TOP + 34, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.model"), x, TOP + 92, DIM); + int y = TOP + 150; + for (var line : font.split(Component.translatable("gui.mcheli.workshop.home.drop"), FORM_WIDTH)) { + graphics.drawString(font, line, x, y, DIM); + y += LINE_HEIGHT; + } + if (error != null) { + y += 2; + for (var line : font.split(error, FORM_WIDTH)) { + graphics.drawString(font, line, x, y, ERROR); + y += LINE_HEIGHT; + } + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From 8bc5ed562222a243c2b068c9b16febd5849d1e9d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:34 +0800 Subject: [PATCH 0965/1461] Add client/workshop/WorkshopPacksScreen.java to the 1.21.5 shared sources --- .../client/workshop/WorkshopPacksScreen.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java new file mode 100644 index 00000000..b8abc314 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java @@ -0,0 +1,218 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.ClientPacks; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.workshop.PackCatalog; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import net.minecraft.Util; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * The package browser: the packs in {@code mcheli/packs} and those the server in use sent, who made + * them and what is in them. A zip someone gave the player can be dropped on it to install. + * + *

This only ever reads the packs and adds to the folder. Removing one is left to the file manager + * the Open folder button opens, since a pack can be the only copy of someone's work. + */ +public class WorkshopPacksScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int GOOD = 0xFF80E080; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int LIST_WIDTH = 170; + private static final int LINE_HEIGHT = 10; + private static final int VEHICLES_SHOWN = 8; + + /** A pack in the list, and where it came from. */ + private record Row(PackCatalog.Info info, boolean fromServer) { + } + + private final List rows = new ArrayList<>(); + private final List problems = new ArrayList<>(); + private int scroll; + private int selected; + private @Nullable Component message; + private int messageColor = DIM; + + public WorkshopPacksScreen() { + super(Component.translatable("gui.mcheli.workshop.packs.title")); + read(); + } + + private void read() { + rows.clear(); + problems.clear(); + for (ContentPack pack : ClientPacks.INSTANCE.serverPacks()) { + try { + rows.add(new Row(PackCatalog.info(pack), true)); + } catch (IOException e) { + problems.add(new PackProblem(pack.id(), "could not be read: " + e)); + } + } + PackCatalog.Listing own = PackCatalog.read(MCHeli.packs().folder()); + for (PackCatalog.Info info : own.packs()) { + // A pack the player has that the server also uses is listed once, as the server's. + if (rows.stream().noneMatch(row -> row.info().hash().equals(info.hash()))) { + rows.add(new Row(info, false)); + } + } + problems.addAll(own.problems()); + selected = Math.clamp(selected, 0, Math.max(rows.size() - 1, 0)); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + @Override + protected void init() { + scroll = Math.clamp(scroll, 0, Math.max(rows.size() - visibleRows(), 0)); + for (int i = scroll; i < rows.size() && i < scroll + visibleRows(); i++) { + int index = i; + Button row = addRenderableWidget(Button.builder(Component.literal(rows.get(i).info().manifest().name()), b -> { + selected = index; + rebuildWidgets(); + }).bounds(10, TOP + (i - scroll) * ROW, LIST_WIDTH, 20).build()); + row.active = i != selected; + } + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.packs.open_folder"), b -> openFolder()) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private void openFolder() { + Path folder = MCHeli.packs().folder(); + try { + Files.createDirectories(folder); + Util.getPlatform().openFile(folder.toFile()); + } catch (IOException e) { + say(Component.literal(e.toString()), ERROR); + } + } + + private void say(Component text, int color) { + message = text; + messageColor = color; + } + + @Override + public void onFilesDrop(List files) { + for (Path file : files) { + if (!file.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) { + say(Component.translatable("gui.mcheli.workshop.packs.not_zip", file.getFileName().toString()), ERROR); + continue; + } + try { + PackManifest installed = PackCatalog.install(file, MCHeli.packs().folder()); + say(Component.translatable("gui.mcheli.workshop.packs.installed", installed.name()), GOOD); + read(); + rebuildWidgets(); + } catch (WorkshopException e) { + say(Component.literal(e.getMessage()), ERROR); + } + } + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, Math.max(rows.size() - visibleRows(), 0)); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void onClose() { + minecraft.setScreen(new WorkshopHomeScreen()); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 10, TEXT); + if (rows.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.packs.none"), 10, TOP, DIM); + } + int x = 10 + LIST_WIDTH + 10; + int w = width - x - 10; + int y = TOP; + if (selected < rows.size()) { + y = details(graphics, rows.get(selected), x, y, w) + LINE_HEIGHT; + } + for (PackProblem problem : problems) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.problem", problem.pack(), problem.message()), x, y, w, ERROR); + } + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.drop"), x, y + 4, w, DIM); + if (message != null) { + wrapped(graphics, message, x, y + 4, w, messageColor); + } + } + + private int details(GuiGraphics graphics, Row row, int x, int y, int w) { + PackCatalog.Info info = row.info(); + PackManifest m = info.manifest(); + y = wrapped(graphics, Component.literal(m.name() + " " + m.version()), x, y, w, TEXT); + if (!m.author().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.author", m.author()), x, y, w, TEXT); + } + if (!m.description().isEmpty()) { + y = wrapped(graphics, Component.literal(m.description()), x, y + 2, w, DIM); + } + y += 4; + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.id", m.id()), x, y, w, DIM); + // A small pack is still something, and 0.0 MB reads as an empty one. + String megabytes = "%.1f".formatted(Math.max(info.size() / 1048576.0, 0.1)); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.size", megabytes, ContentPack.shortHash(info.hash())), x, y, w, DIM); + Component source = row.fromServer() ? Component.translatable("gui.mcheli.workshop.packs.from_server") + : Component.literal(info.source().getFileName().toString() + (info.zipped() ? "" : "/")); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.source", source), x, y, w, DIM); + y += 4; + if (!info.vehicles().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.vehicles", info.vehicles().size(), shortList(info.vehicles())), x, y, w, TEXT); + } + if (!info.weapons().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.weapons", info.weapons().size(), shortList(info.weapons())), x, y, w, TEXT); + } + return y; + } + + private static String shortList(List names) { + if (names.size() <= VEHICLES_SHOWN) { + return String.join(", ", names); + } + return String.join(", ", names.subList(0, VEHICLES_SHOWN)) + ", ..."; + } + + private int wrapped(GuiGraphics graphics, Component text, int x, int y, int w, int color) { + for (FormattedCharSequence line : font.split(text, w)) { + graphics.drawString(font, line, x, y, color); + y += LINE_HEIGHT; + } + return y; + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From 897504750d84e771d0320a880c8c3b92bd42ecfa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:34 +0800 Subject: [PATCH 0966/1461] Port WorkshopScreen to 1.21.5, drawing the preview through AircraftRenderer.draw --- .../client/workshop/WorkshopScreen.java | 1670 +++++++++++++++++ 1 file changed, 1670 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java new file mode 100644 index 00000000..2f386337 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java @@ -0,0 +1,1670 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.render.AircraftRenderState; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.workshop.AssetImport; +import com.jnhprojects.mcheli.workshop.EditHistory; +import com.jnhprojects.mcheli.workshop.PartField; +import com.jnhprojects.mcheli.workshop.PartType; +import com.jnhprojects.mcheli.workshop.TuningKey; +import com.jnhprojects.mcheli.workshop.WorkshopCamera; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopPart; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.TreeSet; +import java.util.function.Consumer; + +/** + * The workshop's editor. The vehicle turns in the middle, drawn by the game's own vehicle renderer + * through a GUI entity, so what shows is what will fly. The parts list is on the left, the values + * of what is selected on the right, and gizmos for every part lie over the model. + * + *

Gizmos are plain GUI drawing at the points {@link WorkshopCamera#project} gives. They always + * show, even when the part is inside the hull, which is where most seats and pivots are. + * + *

Left drag on empty space turns the view, right drag moves it, the wheel zooms. Dragging a + * handle moves the part in the plane of the screen and an arrow along its axis, snapping to a + * sixteenth of a block unless Shift is held. + */ +public class WorkshopScreen extends Screen { + private static final int LEFT = 112; + private static final int RIGHT = 144; + private static final int TOP = 26; + private static final int BOTTOM = 26; + private static final int LIST_ROW = 16; + private static final int FIELD_ROW = 20; + private static final int LABEL_ROW = 10; + + private static final int PANEL = 0xFF181818; + private static final int VIEW = 0xFF2A2F36; + private static final int GRID = 0xFF3A414A; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int WARNING = 0xFFFFC040; + private static final int INVALID = 0xFFFF6060; + private static final int X_COLOR = 0xFFFF5050; + private static final int Y_COLOR = 0xFF60E060; + private static final int Z_COLOR = 0xFF5090FF; + private static final int SELECTED = 0xFFFFFFFF; + private static final int HIT_BOX = 0xFF00E0FF; + private static final int AXIS = 0xFFFF60FF; + + private static final int HANDLE_RADIUS = 3; + private static final double PICK_DISTANCE = 6.0; + private static final double ARROW_LENGTH = 34.0; + private static final double SNAP = 16.0; + private static final int GRID_HALF_SIZE = 16; + private static final int GRID_STEP = 2; + private static final double ZOOM_STEP = 1.15; + + private static final int PREVIEW_SIZE = 64; + private static final int PLOT_HEIGHT = 56; + private static final float PITCH_SNAP = 20.0F; + private static final String DEFAULT_CURVE = "0:0.7, 0.25:0.85, 0.5:1, 0.75:1.15, 1:1.3"; + + private enum Tab { + PARTS, PAINT, SOUND, TUNING, PACK + } + + private enum Drag { + NONE, ORBIT, PAN, PLANE, AXIS_X, AXIS_Y, AXIS_Z, CURVE + } + + private record Label(Component text, int x, int y, int color) { + } + + private record WrappedLine(FormattedCharSequence text, int x, int y) { + } + + private record MergeKey(int line, String field, int component) { + } + + private final WorkshopProject project; + private final WorkshopCamera camera = new WorkshopCamera(); + private final EditHistory> history = new EditHistory<>(); + private final List

Movement is simulated wherever {@link #isLocalInstanceAuthoritative()} is true. With a player + * at the controls that is the pilot's client, exactly like vanilla boats, and the server checks + * the result through the regular vehicle move packet. Without a pilot the server simulates the + * vehicle on its own. Roll, throttle and key state are not part of the vanilla packet, so the + * pilot sends them separately and the server republishes them through synched data. + * + *

Seats are explicit. Vanilla only knows an ordered passenger list, so the seat each passenger + * occupies is kept in a synched seat map of entity ids. Seat zero is the pilot and decides who + * controls the vehicle; crew seats follow, then cargo racks. + */ +public abstract class AircraftEntity extends VehicleEntity { + private static final EntityDataAccessor DATA_TYPE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TEXTURE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_HP = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_THROTTLE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_REVERSE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_ROLL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_HOVERING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_GUNNER_MODE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_INPUT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_PARTS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SEATS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_SELECTION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_USE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TV_MISSILE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_FLARE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SINKING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_FUEL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + /** The UAV station a drone is flown from, by entity id, or 0. */ + private static final EntityDataAccessor DATA_STATION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_REPELLING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SEARCH_LIGHT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + + /** Marks a player flying a drone from a station, so a stale hide can be undone at login. */ + public static final String REMOTE_PILOT_TAG = "mcheli_remote_pilot"; + + // Bits of DATA_PARTS, the same assignment the original used for its part status word. + protected static final int PART_CANOPY = 0; + protected static final int PART_NOZZLE = 1; + protected static final int PART_LANDING_GEAR = 2; + protected static final int PART_WING = 3; + protected static final int PART_HATCH = 4; + + private static final int EMPTY = -1; + /** How far above its hit box the original drew a vehicle's model; see {@link #waterDepth}. */ + private static final float LEGACY_MODEL_LIFT = 0.35F; + /** How fast a cooked-off boat goes under, in blocks per tick (about 0.4 blocks a second). */ + private static final double SINK_SPEED = 0.02; + /** The bow-up pitch and the list a sinking boat settles into, in degrees. */ + private static final float SINK_PITCH = 14.0F; + private static final float SINK_LIST = 12.0F; + + private final InterpolationHandler interpolation = new VehicleInterpolation(this); + protected final ControlInput input = new ControlInput(); + public final AnimationState animation = new AnimationState(); + + /** Throttle in [0, 1] as seen by the simulating side. Other sides read {@link #DATA_THROTTLE}. */ + protected double throttle; + /** Reverse throttle in [0, 0.6] for vehicles with {@code EnableBack}. */ + protected float reverse; + protected float roll; + public float rollO; + protected int modeSwitchCooldown; + private int ticksAlive; + + private @Nullable AircraftDefinition cachedDefinition; + private String cachedName = ""; + private int cachedGeneration; + + protected @Nullable PartState canopy; + protected @Nullable PartState landingGear; + protected @Nullable PartState hatch; + private final List parts = new ArrayList<>(); + + private int[] seatIds = new int[0]; + private int pendingSeat = EMPTY; + private final Map savedSeats = new HashMap<>(); + private final Map lastSeats = new HashMap<>(); + /** The station a drone is flown from, and where its pilot goes back to. Server only. */ + private @Nullable UUID station; + private @Nullable Vec3 stationReturn; + /** Remote pilots leaving this tick, by entity id, with where they come back down. */ + private final Map remoteReturns = new HashMap<>(); + /** Where a workshop test flight started, or {@code null} for an ordinary vehicle. Server only. */ + private @Nullable Vec3 testFlightStart; + private boolean testFlightOver; + /** How far the ropes hang below their hooks, as a negative number; both sides let them out. */ + private float ropeLength; + private int repellingTicks; + private int lastRope; + /** Passengers are leaving one by one under parachutes. Server only. */ + private boolean droppingTroops; + + private @Nullable VehicleWeapons weapons; + private int @Nullable [] savedAmmo; + private @Nullable TvMissileEntity tvMissile; + + // Damage model + /** Damage factor of the extra hit box the last projectile ray struck, {@code lastBBDamageFactor}. */ + private float hitBoxFactor = 1.0F; + private int timeSinceHit; + /** The fuel load from which an aircraft counts as heavy: the H8K flying boat and up. */ + private static final int HEAVY_FUEL = 3000; + /** How long a wreck stays, burning, before it disappears: 30 seconds. */ + private static final int WRECK_TICKS = 600; + /** The wreck fire dies down over the last five seconds. */ + private static final int WRECK_FADE_TICKS = 100; + /** Ticks until a destroyed wreck disappears; above zero means destroyed. Server only. */ + private int despawnCount; + /** Ticks the client has seen this vehicle as a wreck, for the fire to die down on time. */ + private int wreckTicks; + /** Ticks since this aircraft was shot down, for the crash spin. Counted on both sides. */ + private int spinTicks; + private boolean exploded; + /** Blasts still to come while a wrecked boat cooks off, and ticks until the next one. */ + private int cookOffBlasts; + private int cookOffDelay; + private @Nullable Entity cookOffCause; + private int alertCount; + + /** Fuel burnt but not yet taken from the tank, which only holds whole units. Server only. */ + private double fuelBurnt; + /** Ticks a fuel truck nearby keeps this vehicle from burning fuel. Server only. */ + private int fuelSuppliedCount; + /** One bit per seat whose occupant has night vision on. Server only, not saved. */ + private int nightVisionSeats; + /** Fuel and parachute slots and the cargo chest. Only the server's copy holds anything. */ + private final VehicleInventory inventory = new VehicleInventory(this); + + // Crashes, measured from position so they work whichever side simulates the vehicle. + /** Blocks per tick into terrain, sideways, that count as a crash: about 25 m/s. */ + private static final double TERRAIN_CRASH_SPEED = 1.25; + /** + * How far a hull reaches past its main box, at most, when looking for neighbours: other + * vehicles are found by their main box, and the longest hulls stretch about this far from it. + */ + private static final double HULL_REACH = 24.0; + /** Closing speed between two vehicles that counts as a crash: about 10 m/s. */ + private static final double VEHICLE_CRASH_SPEED = 0.5; + /** Highest point since a ground vehicle left the ground, or NaN while it is on it. */ + private double airborneFromY = Double.NaN; + private @Nullable Vec3 lastTickPos; + private Vec3 lastVelocity = Vec3.ZERO; + private int collisionCooldown; + + protected AircraftEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_TYPE, ""); + builder.define(DATA_TEXTURE, ""); + builder.define(DATA_HP, 0); + builder.define(DATA_THROTTLE, 0.0F); + builder.define(DATA_REVERSE, 0.0F); + builder.define(DATA_ROLL, 0.0F); + builder.define(DATA_HOVERING, false); + builder.define(DATA_GUNNER_MODE, false); + builder.define(DATA_INPUT, (byte) 0); + builder.define(DATA_PARTS, 0); + builder.define(DATA_SEATS, ""); + builder.define(DATA_WEAPON_SELECTION, ""); + builder.define(DATA_WEAPON_USE, 0); + builder.define(DATA_TV_MISSILE, 0); + builder.define(DATA_FLARE, false); + builder.define(DATA_SINKING, false); + builder.define(DATA_FUEL, 0); + builder.define(DATA_STATION, 0); + builder.define(DATA_REPELLING, false); + builder.define(DATA_SEARCH_LIGHT, false); + } + + // Definition + + /** + * Looks the definition up by name. Returns {@code null} for vehicles whose content was removed. + * After a content reload the next call picks up the new definition object. + */ + public @Nullable AircraftDefinition definition() { + String name = typeName(); + int generation = MCHeli.content().generation(); + if (!name.equals(cachedName) || generation != cachedGeneration) { + boolean reloaded = name.equals(cachedName); + cachedName = name; + cachedGeneration = generation; + AircraftDefinition previous = cachedDefinition; + cachedDefinition = name.isEmpty() ? null : lookup(name); + if (cachedDefinition != null && cachedDefinition != previous) { + if (reloaded && weapons != null) { + // The same vehicle with new numbers: keep the rounds it carries. + savedAmmo = weapons.saveAmmo(); + } + onDefinitionChanged(cachedDefinition); + } + } + return cachedDefinition; + } + + /** Finds a definition of this entity's kind. */ + protected abstract @Nullable AircraftDefinition lookup(String name); + + /** Builds the per vehicle state that depends on the definition: seats, parts, animation arrays. */ + protected void onDefinitionChanged(AircraftDefinition def) { + if (seatIds.length != def.seatAndRackCount()) { + int[] resized = new int[def.seatAndRackCount()]; + Arrays.fill(resized, EMPTY); + System.arraycopy(seatIds, 0, resized, 0, Math.min(seatIds.length, resized.length)); + seatIds = resized; + } + parts.clear(); + canopy = def.canopies.isEmpty() ? null : addPart(PART_CANOPY, 3.5F, PartState.Sounds.CANOPY); + landingGear = def.landingGear.isEmpty() ? null : addPart(PART_LANDING_GEAR, 2.5F, PartState.Sounds.LANDING_GEAR); + hatch = def.hatches.isEmpty() ? null : addPart(PART_HATCH, 1.5F, PartState.Sounds.HATCH); + animation.resize(def); + weapons = new VehicleWeapons(this, def); + if (savedAmmo != null) { + weapons.loadAmmo(savedAmmo); + savedAmmo = null; + } + } + + protected PartState addPart(int bit, float speed, PartState.Sounds sounds) { + PartState state = new PartState(this, bit, speed, sounds); + // A vehicle that comes into view with a part already switched should not animate it. + state.force(state.status()); + parts.add(state); + return state; + } + + /** Sets which vehicle this is. Call once, right after construction, before adding to the level. */ + public void setType(AircraftDefinition definition) { + entityData.set(DATA_TYPE, definition.name()); + entityData.set(DATA_TEXTURE, definition.textures().getFirst()); + entityData.set(DATA_HP, definition.maxHp); + entityData.set(DATA_FUEL, definition.maxFuel); + refreshDimensions(); + definition(); + } + + public String typeName() { + return entityData.get(DATA_TYPE); + } + + public String texture() { + String texture = entityData.get(DATA_TEXTURE); + return texture.isEmpty() ? typeName() : texture; + } + + public int hp() { + return entityData.get(DATA_HP); + } + + /** + * {@code repair}: adds hit points up to the maximum. A wreck stays a wreck. Server only. + * + * @return whether anything was repaired + */ + public boolean repair(int amount) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || hp() >= def.maxHp) { + return false; + } + entityData.set(DATA_HP, Math.min(hp() + amount, def.maxHp)); + return true; + } + + /** + * {@code switchNextTextureName}: the next paint scheme the definition lists. Server only. + * + * @return false when the vehicle has only one + */ + public boolean nextTexture() { + AircraftDefinition def = definition(); + if (def == null || def.textures().size() < 2) { + return false; + } + List textures = def.textures(); + int next = (textures.indexOf(texture()) + 1) % textures.size(); + entityData.set(DATA_TEXTURE, textures.get(next)); + return true; + } + + // Fuel + + public int fuel() { + return entityData.get(DATA_FUEL); + } + + public int maxFuel() { + AircraftDefinition def = definition(); + return def != null ? def.maxFuel : 0; + } + + /** {@code getFuelP}: how full the tank is, 0 to 1. Vehicles without a tank read as full. */ + public float fuelRatio() { + int max = maxFuel(); + return max <= 0 ? 1.0F : (float) fuel() / max; + } + + /** {@code canUseFuel}: a vehicle without a tank never runs dry. */ + public boolean hasFuel() { + return maxFuel() <= 0 || fuel() > 1; + } + + /** Sets the tank outright; a drone leaves its station with 5%. Server only. */ + public void setFuelLevel(int fuel) { + entityData.set(DATA_FUEL, Math.clamp(fuel, 0, Math.max(maxFuel(), 0))); + } + + /** + * Pours up to {@code amount} into the tank. Server only. + * + * @return how much went in + */ + public int refuel(int amount) { + int added = Math.clamp(maxFuel() - fuel(), 0, Math.max(amount, 0)); + if (added > 0) { + entityData.set(DATA_FUEL, fuel() + added); + } + return added; + } + + // Night vision + + /** A crew member switched night vision on or off. Server only. */ + public void setNightVision(Player player, boolean on) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (seat < 0 || seat >= 31) { + return; + } + if (on && def != null && def.nightVision && !isDestroyed()) { + nightVisionSeats |= 1 << seat; + } else if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(player); + } + } + + /** Keeps night vision on for everyone in the crew who switched it on, see {@link NightVision}. */ + private void tickNightVision() { + if (nightVisionSeats == 0) { + return; + } + if (isDestroyed()) { + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null) { + NightVision.end(crew); + } + } + nightVisionSeats = 0; + return; + } + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (hasNightVision(seat) && crew != null) { + NightVision.topUp(crew); + } + } + } + + /** Only the first 31 seats can switch it on; racks and the like come after them anyway. */ + private boolean hasNightVision(int seat) { + return seat >= 0 && seat < 31 && (nightVisionSeats & 1 << seat) != 0; + } + + /** + * {@code updateFuel}: once a second the engine burns its {@code FuelConsumption}, scaled by the + * throttle and topping out at 70% throttle. Nothing burns next to a fuel truck or with the + * server's infinite fuel on. + */ + private void burnFuel(AircraftDefinition def) { + if (def.maxFuel <= 0 || isDestroyed()) { + return; + } + if (fuelSuppliedCount > 0) { + fuelSuppliedCount--; + return; + } + if (ticksAlive % 20 != 0 || fuel() <= 1 || throttle <= 0.0 || ServerConfig.infiniteFuel()) { + return; + } + fuelBurnt += Math.min(throttle * 1.4, 1.0) * def.fuelConsumption; + if (fuelBurnt > 1.0) { + int whole = (int) fuelBurnt; + fuelBurnt -= whole; + entityData.set(DATA_FUEL, Math.max(fuel() - whole, 0)); + } + } + + /** + * {@code supplyFuel}: a fuel truck ({@code FuelSupplyRange}) tops up every vehicle within range, + * 30 units every half second. While it is on the ground it only reaches vehicles that are on + * the ground too. + */ + private void supplyFuel(AircraftDefinition def) { + float range = def.fuelSupplyRange; + if (range <= 0.0F || ticksAlive % 10 != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + if (other == this) { + continue; + } + if (!onGround() || other.onGround()) { + other.refuel(30); + } + other.fuelSuppliedCount = 40; + } + } + + // Inventory and supply + + public VehicleInventory inventory() { + return inventory; + } + + /** + * {@code canSupply}: fuel cans and resupply only work while the vehicle stands on something, + * within three blocks; a vehicle that cannot float must not be in the water either. + */ + public boolean canSupply() { + AircraftDefinition def = definition(); + return def != null && hasBlockBelow(1, 3, false) && (def.floats || !isInWater()); + } + + /** {@code openGui}: shows a crew member the vehicle screen. Server only. */ + public void openScreen(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || player.getVehicle() != this) { + return; + } + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new VehicleMenu(id, playerInventory, this), displayName(def))); + } + + /** + * {@code openInventory}: shows the cargo chest of a vehicle with an {@code InventorySize}. + * Server only. + * + * @return false when the vehicle has none + */ + public boolean openCargo(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || def.cargoRows() <= 0 || isDestroyed() || !inventory.stillValid(player)) { + return false; + } + int rows = Math.min(def.cargoRows(), 6); + MenuType type = switch (rows) { + case 1 -> MenuType.GENERIC_9x1; + case 2 -> MenuType.GENERIC_9x2; + case 3 -> MenuType.GENERIC_9x3; + case 4 -> MenuType.GENERIC_9x4; + case 5 -> MenuType.GENERIC_9x5; + default -> MenuType.GENERIC_9x6; + }; + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new ChestMenu(type, id, playerInventory, inventory.cargo(), rows), + displayName(def))); + return true; + } + + /** The vehicle's name as its item shows it. */ + private static Component displayName(AircraftDefinition def) { + return Component.translatableWithFallback("item.mcheli." + def.name(), def.displayName); + } + + /** + * {@code supplyAmmoToOtherAircraft}: an ammunition truck ({@code AmmoSupplyRange}) tops up the + * weapons of every vehicle within range that stands on something, a tenth of each weapon's + * maximum every two seconds. + */ + private void supplyAmmo(AircraftDefinition def) { + float range = def.ammoSupplyRange; + if (range <= 0.0F || ticksAlive % SupplyRules.AMMO_TRUCK_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + VehicleWeapons theirs = other.weapons(); + if (other == this || theirs == null || other.isDestroyed() || !other.canSupply()) { + continue; + } + for (WeaponSet set : theirs.sets()) { + int rounds = SupplyRules.roundsFromTruck(set.ammo() + set.restAmmo(), set.maxAmmo()); + if (rounds > 0) { + set.setRestAmmo(set.restAmmo() + rounds); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + } + } + } + } + + /** {@code repairOtherAircraft}: a repair vehicle ({@code RepairOtherVehicles}) mends those in range. */ + private void repairOthers(AircraftDefinition def) { + float range = def.repairOtherVehiclesRange; + if (range <= 0.0F || ticksAlive % SupplyRules.REPAIR_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + AircraftDefinition theirs = other.definition(); + if (other != this && theirs != null) { + other.repair(SupplyRules.repair(other.hp(), theirs.maxHp, def.repairOtherVehiclesValue)); + } + } + } + + /** Whether the flight model holds a hover: in hover mode, or while the pilot mans the guns. */ + public boolean isHovering() { + return isHoveringMode() || isGunnerMode(); + } + + /** The hover mode the helicopter pilot switches with jump. */ + public boolean isHoveringMode() { + return entityData.get(DATA_HOVERING); + } + + protected void setHovering(boolean hovering) { + entityData.set(DATA_HOVERING, hovering); + } + + // Pilot gunner mode + + /** + * Pilot gunner mode ({@code EnableGunnerMode}): the pilot leaves the controls to man the + * weapons from the camera position. Helicopters hold a hover; planes level off and circle. + */ + public boolean isGunnerMode() { + return entityData.get(DATA_GUNNER_MODE); + } + + /** {@code canSwitchGunnerMode}: the kinds add their own attitude and speed limits. */ + public boolean canSwitchGunnerMode() { + AircraftDefinition def = definition(); + if (def == null || !def.gunnerMode || !isCanopyClosed() || isDestroyed() || isHoveringMode()) { + return false; + } + // Without ConcurrentGunnerMode a player in the gunner seat has the guns already. + return def.concurrentGunnerMode || !(occupant(1) instanceof Player); + } + + /** Server side. Leaving gunner mode is always allowed; entering it only when {@link #canSwitchGunnerMode}. */ + public void setGunnerMode(boolean on) { + if (!on || canSwitchGunnerMode()) { + entityData.set(DATA_GUNNER_MODE, on); + } + } + + /** Whether {@code entity} is the pilot and currently in gunner mode. */ + public boolean isGunnerMode(Entity entity) { + return isGunnerMode() && seatOf(entity) == 0; + } + + /** Drops gunner mode once its conditions no longer hold. Server side, every tick. */ + private void checkGunnerMode(AircraftDefinition def) { + if (isGunnerMode() && (occupant(0) == null || isDestroyed() + || (!def.concurrentGunnerMode && occupant(1) instanceof Player))) { + entityData.set(DATA_GUNNER_MODE, false); + } + } + + // Throttle, roll and input + + /** Throttle for animation and sound, valid on every side. */ + public float visibleThrottle() { + return isSimulatingHere() ? (float) throttle : entityData.get(DATA_THROTTLE); + } + + public float visibleReverse() { + return isSimulatingHere() ? reverse : entityData.get(DATA_REVERSE); + } + + public float roll() { + return isSimulatingHere() ? roll : entityData.get(DATA_ROLL); + } + + public float roll(float partialTicks) { + return Mth.rotLerp(partialTicks, rollO, roll()); + } + + public void setRoll(float roll) { + this.roll = roll; + } + + protected boolean isSimulatingHere() { + return isLocalInstanceAuthoritative() && !isPassenger(); + } + + public ControlInput input() { + return input; + } + + /** + * Applies the pilot's view change for one frame. Only called on the pilot's client, and only + * for kinds that steer with the mouse. + * + * @param dYaw change of the player's yaw since the last frame, in degrees + * @param dPitch change of the player's pitch since the last frame, in degrees + */ + public void applyPilotLook(float dYaw, float dPitch) { + } + + /** Whether the pilot steers with the mouse, as in helicopters and planes. */ + public boolean steersWithMouse() { + return false; + } + + /** Server side entry point for the pilot's control packet. */ + public void acceptPilotState(float throttle, float reverse, float roll, boolean hovering, byte keys) { + this.throttle = Mth.clamp(throttle, 0.0F, 1.0F); + this.reverse = Mth.clamp(reverse, 0.0F, 1.0F); + this.roll = AircraftRotation.wrap(roll); + input.read(keys); + entityData.set(DATA_THROTTLE, (float) this.throttle); + entityData.set(DATA_REVERSE, this.reverse); + entityData.set(DATA_ROLL, this.roll); + entityData.set(DATA_HOVERING, hovering); + entityData.set(DATA_INPUT, keys); + } + + public float reverse() { + return reverse; + } + + // Tick + + /** + * Makes this a workshop test flight: it takes no damage, is not saved with the world, and goes + * away once its pilot gets out, putting them back where they started. + */ + public void startTestFlight(Vec3 start) { + testFlightStart = start; + } + + @Override + public boolean shouldBeSaved() { + return testFlightStart == null && super.shouldBeSaved(); + } + + @Override + public void tick() { + if (testFlightOver) { + discard(); + return; + } + rollO = roll(); + if (getHurtTime() > 0) { + setHurtTime(getHurtTime() - 1); + } + super.tick(); + interpolation.interpolate(); + ticksAlive++; + lastSeats.clear(); + remoteReturns.clear(); + if (modeSwitchCooldown > 0) { + modeSwitchCooldown--; + } + + AircraftDefinition def = definition(); + if (def == null) { + // Content for this vehicle no longer exists. Keep the entity inert rather than crash. + setDeltaMovement(Vec3.ZERO); + return; + } + if (timeSinceHit > 0) { + timeSinceHit--; + } + if (alertCount > 0) { + alertCount--; + } + boolean groundedBefore = onGround(); + double fallBefore = getDeltaMovement().y; + tickDestroyed(); + + if (!level().isClientSide()) { + supplyFuel(def); + supplyAmmo(def); + repairOthers(def); + burnFuel(def); + if (def.maxFuel > 0 && fuel() < def.maxFuel && !isDestroyed()) { + inventory.pourFuelCans(ticksAlive); + } + tickNightVision(); + tickStation(); + } + tickRopes(def); + + if (isSimulatingHere()) { + if (!(getControllingPassenger() instanceof Player) || isDestroyed()) { + input.clear(); + } + if (isRepelling()) { + // {@code MCH_AircraftClientTickHandler}: the pilot holds a hover while the ropes are out. + input.throttleUp = false; + input.throttleDown = false; + input.left = false; + input.right = false; + } + if (!hasFuel()) { + // An empty tank: the throttle runs down by itself, as with the pilot's hand off it. + input.throttleUp = false; + } + if (entityData.get(DATA_SINKING)) { + sink(); + } else { + simulate(); + } + float[] kick = weapons != null ? weapons.recoilKick() : null; + if (kick != null) { + setXRot(getXRot() + kick[0]); + roll += kick[1]; + } + if (!level().isClientSide()) { + entityData.set(DATA_THROTTLE, (float) throttle); + entityData.set(DATA_REVERSE, reverse); + entityData.set(DATA_ROLL, roll); + entityData.set(DATA_INPUT, input.write()); + } + } else { + if (!isLocalInstanceAuthoritative()) { + input.read(entityData.get(DATA_INPUT)); + } + setDeltaMovement(Vec3.ZERO); + } + for (PartState part : parts) { + part.tick(); + } + if (!level().isClientSide()) { + updatePartsServer(def); + checkGunnerMode(def); + } + animation.tick(this, def); + tickAnimation(); + if (weapons != null) { + weapons.tick(); + if (!level().isClientSide()) { + entityData.set(DATA_FLARE, weapons.flares.isActive()); + warnIncomingMissiles(); + } + } + if (level().isClientSide()) { + damageSmoke(def); + VehicleParticles.tick(this, def); + } else { + tickImpacts(def, groundedBefore, fallBefore); + } + } + + /** + * Server side crashes. A wreck explodes when it lands; an intact vehicle takes damage when it + * meets the ground steeply tilted ({@code onUpdate_CollisionGroundDamage}), sits under water + * without floats, flies into terrain, or rams another vehicle. + */ + private void tickImpacts(AircraftDefinition def, boolean groundedBefore, double fallBefore) { + Vec3 velocity = lastTickPos != null ? position().subtract(lastTickPos) : Vec3.ZERO; + Vec3 previous = lastVelocity; + lastTickPos = position(); + lastVelocity = velocity; + if (collisionCooldown > 0) { + collisionCooldown--; + } + double speed = Math.max(velocity.length(), previous.length()); + + if (isDestroyed()) { + boolean falling = fallBefore < -0.2 || previous.y < -0.2; + boolean landed = onGround() && !groundedBefore; + boolean smashed = horizontalCollision && previous.horizontalDistance() > 0.5; + if (!exploded && (landed && falling || smashed)) { + crashExplosion(); + } + return; + } + + if (isNearGround(3)) { + float roll = Math.abs(Mth.wrapDegrees(roll())); + float pitch = Math.abs(Mth.wrapDegrees(getXRot())); + if (roll > crashTilt() || pitch > crashTilt()) { + float damage = roll + pitch; + damage *= damage < 90.0F ? 0.4F * (float) velocity.length() : 0.4F; + if (damage > 1.0F && random.nextInt(4) == 0) { + hurtServer((ServerLevel) level(), damageSources().inWall(), damage); + } + } + } + if (ticksAlive % 30 == 0 && !def.floats && level().getFluidState( + BlockPos.containing(getX() + 0.5, getY() + 1.5 + def.submergedDamageHeight, getZ() + 0.5)).is(FluidTags.WATER)) { + hurtServer((ServerLevel) level(), damageSources().inWall(), Math.max(def.maxHp / 10, 1)); + } + + if (takesGroundImpacts()) { + groundImpacts(def, previous); + } + + // Not in the original: flying into terrain or another vehicle at speed is a crash. + if (horizontalCollision && previous.horizontalDistance() > TERRAIN_CRASH_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), crashDamage(previous.horizontalDistance())); + } + if (speed > VEHICLE_CRASH_SPEED && collisionCooldown == 0) { + // A blocked move can leave the hulls up to a tick of travel apart; when the vehicle + // stopped short this tick, look that far for what stopped it. + boolean stopped = velocity.length() < previous.length() * 0.5; + double reach = stopped ? 0.5 + previous.length() : 0.5; + for (AircraftEntity other : solidNeighbours(hullBounds().inflate(reach))) { + if (!hullsTouch(other, reach)) { + continue; + } + double closing = velocity.subtract(other.lastVelocity).length(); + closing = Math.max(closing, previous.subtract(other.lastVelocity).length()); + if (closing > VEHICLE_CRASH_SPEED && other.collisionCooldown == 0) { + collisionCooldown = 20; + other.collisionCooldown = 20; + float damage = crashDamage(closing); + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), damage); + other.hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), damage); + break; + } + } + } + } + + /** {@code getGiveDamageRot}: how far a vehicle may lean near the ground before it scrapes. */ + protected float crashTilt() { + return 40.0F; + } + + /** Crash damage grows with the square of the impact speed, like the energy involved. */ + private static float crashDamage(double speed) { + return (float) (speed * speed * 60.0); + } + + /** Whether hitting walls and falling hurt this vehicle. Tanks and cars, which live on the ground. */ + protected boolean takesGroundImpacts() { + return false; + } + + /** + * A ground vehicle that hits a wall or lands after a long drop takes a share of its hit points + * by the speed or the height, see {@link GroundImpact}. The same height costs a car and a tank + * the same share before armour. + */ + private void groundImpacts(AircraftDefinition def, Vec3 previous) { + if (horizontalCollision && previous.horizontalDistance() > GroundImpact.WALL_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurtServer((ServerLevel) level(), damageSources().flyIntoWall(), GroundImpact.wall(previous.horizontalDistance()) * def.maxHp); + } + if (isInWater() || isPassenger()) { + airborneFromY = Double.NaN; + } else if (!onGround()) { + airborneFromY = Double.isNaN(airborneFromY) ? getY() : Math.max(airborneFromY, getY()); + } else if (!Double.isNaN(airborneFromY)) { + double drop = airborneFromY - getY(); + airborneFromY = Double.NaN; + if (drop > GroundImpact.SAFE_DROP) { + hurtServer((ServerLevel) level(), damageSources().fall(), GroundImpact.drop(drop) * def.maxHp); + } + } + } + + /** {@code MCH_MissileDetector}: the crew hears a warning while a missile homes on them. */ + private void warnIncomingMissiles() { + if (alertCount > 0 || isDestroyed() || weapons == null || !weapons.flares.isFitted() || ticksAlive % 5 != 0) { + return; + } + boolean tracked = !level().getEntitiesOfClass(ProjectileEntity.class, getBoundingBox().inflate(60.0), + p -> p.target() != null && (p.target() == this || hasPassenger(p.target()))).isEmpty(); + if (tracked) { + alertCount = 20; + ModSounds.playAt(this, "alert", 1.0F, 1.0F); + } + } + + /** One tick of control and physics on the simulating side. */ + protected abstract void simulate(); + + /** Visual state that every side updates, for example rotor angles. */ + protected void tickAnimation() { + } + + /** Ticks since this entity instance was created, used to delay controls right after spawning. */ + protected int ticksAlive() { + return ticksAlive; + } + + // Moving parts + + int partBits() { + return entityData.get(DATA_PARTS); + } + + void setPartBit(int bit, boolean on) { + int bits = entityData.get(DATA_PARTS); + entityData.set(DATA_PARTS, on ? bits | (1 << bit) : bits & ~(1 << bit)); + } + + public @Nullable PartState canopy() { + return canopy; + } + + public @Nullable PartState landingGear() { + return landingGear; + } + + public @Nullable PartState hatch() { + return hatch; + } + + public boolean isCanopyClosed() { + return canopy == null || canopy.isOff(); + } + + protected void openCanopy() { + if (canopy != null && modeSwitchCooldown <= 0) { + canopy.set(true, true); + modeSwitchCooldown = 20; + } + } + + protected void closeCanopy() { + if (canopy != null && modeSwitchCooldown <= 0 && canopy.status()) { + canopy.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean isLandingGearUp() { + return landingGear != null && landingGear.status(); + } + + /** Gear can be raised once the vehicle is at least ten blocks above anything solid. */ + public boolean canRaiseLandingGear() { + return landingGear != null && landingGear.angle() < 1.0F && !landingGear.status() && !hasBlockBelow(3, 10, true); + } + + public boolean canLowerLandingGear() { + return landingGear != null && landingGear.angle() >= 89.0F && landingGear.status(); + } + + public void toggleLandingGear() { + if (landingGear == null || modeSwitchCooldown > 0) { + return; + } + if (canRaiseLandingGear()) { + landingGear.set(true, true); + modeSwitchCooldown = 20; + } else if (canLowerLandingGear()) { + landingGear.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean canFoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOff(); + } + + public boolean canUnfoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOn(); + } + + /** The fold key: hatches by default, planes fold or sweep their wings instead. */ + public void toggleFold() { + if (canFoldHatch()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldHatch()) { + hatch.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Throttle below which the landing gear comes down by itself near the ground. */ + protected float autoLowerGearThrottle() { + return 0.8F; + } + + /** {@code updateParts}: automatic landing gear, plus whatever a kind adds. Server only. */ + protected void updatePartsServer(AircraftDefinition def) { + if (landingGear == null) { + return; + } + if (!landingGear.status() && landingGear.factor() <= 0.1F) { + // Amphibians retract the gear when they settle on water. + boolean onWater = isInWater() || isWaterBelow(20); + if (def.floats && onWater && (throttle <= 0.8 || onGround() || hasBlockBelow(3, 20, true))) { + landingGear.set(true, true); + } + } else if (landingGear.status() && landingGear.factor() >= 0.9F) { + boolean ground = hasBlockBelow(3, 10, true); + if (throttle < autoLowerGearThrottle() && ground) { + if (!def.floats || !isWaterBelow(150)) { + landingGear.set(false, true); + } + } else if (vtolMode() == 2 && ground) { + landingGear.set(false, true); + } + } + } + + /** 0 off, 1 tilting, 2 fully in VTOL mode. Only planes have one. */ + public int vtolMode() { + return 0; + } + + // Seats + + /** Seat (or rack) index of {@code entity}, or -1. */ + public int seatOf(Entity entity) { + int id = entity.getId(); + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == id) { + return i; + } + } + return EMPTY; + } + + public @Nullable Entity occupant(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] == EMPTY) { + return null; + } + int id = seatIds[seat]; + for (Entity passenger : getPassengers()) { + if (passenger.getId() == id) { + return passenger; + } + } + return null; + } + + public @Nullable SeatDefinition seat(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && seat >= 0 && seat < def.seatCount() ? def.seats.get(seat) : null; + } + + private boolean isSeatFree(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] != EMPTY) { + return false; + } + AircraftDefinition def = definition(); + if (def != null) { + for (int[] group : def.exclusiveSeats) { + if (contains(group, seat)) { + for (int other : group) { + if (other != seat && other >= 0 && other < seatIds.length && seatIds[other] != EMPTY) { + return false; + } + } + } + } + } + return true; + } + + private static boolean contains(int[] values, int value) { + for (int v : values) { + if (v == value) { + return true; + } + } + return false; + } + + /** First crew seat a player boarding by hand would take. Drones keep their pilot seat for the remote. */ + private int firstFreeCrewSeat() { + AircraftDefinition def = definition(); + if (def == null) { + return EMPTY; + } + for (int i = def.uav ? 1 : 0; i < def.seatCount(); i++) { + if (isSeatFree(i)) { + return i; + } + } + return EMPTY; + } + + private void assignSeat(Entity entity, int seat) { + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == entity.getId()) { + seatIds[i] = EMPTY; + if (weapons != null) { + weapons.onLeft(i); + } + } + } + if (seat >= 0 && seat < seatIds.length) { + seatIds[seat] = entity.getId(); + } + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onSeated(entity, seat); + weapons.resendAmmo(); + } + } + + private String encodeSeats() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < seatIds.length; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(seatIds[i]); + } + return sb.toString(); + } + + private void decodeSeats(String encoded) { + if (encoded.isEmpty()) { + Arrays.fill(seatIds, EMPTY); + return; + } + String[] parts = encoded.split(","); + if (parts.length != seatIds.length) { + seatIds = new int[parts.length]; + } + for (int i = 0; i < parts.length; i++) { + try { + seatIds[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + seatIds[i] = EMPTY; + } + } + } + + /** Moves {@code entity} to the next free crew seat, wrapping around. Server only. */ + public void switchSeat(Entity entity) { + AircraftDefinition def = definition(); + int current = seatOf(entity); + if (def == null || current < 0 || current >= def.seatCount() || current == 0 && station != null) { + // A remote pilot stays at the controls; they are not really aboard. + return; + } + int count = def.seatCount(); + seatIds[current] = EMPTY; + for (int step = 1; step < count; step++) { + int seat = (current + step) % count; + if (def.uav && seat == 0) { + continue; + } + if (isSeatFree(seat)) { + assignSeat(entity, seat); + if (seat == 0) { + closeCanopy(); + } + return; + } + } + assignSeat(entity, current); + } + + // UAV stations + + /** Whether a player at a station could take this drone's controls now. */ + public boolean canTakeRemotePilot() { + AircraftDefinition def = definition(); + return def != null && def.uav && !def.targetDrone && !isDestroyed() && isSeatFree(0); + } + + /** + * Seats {@code player}, who sits at {@code from}, at this drone's controls. The pilot is hidden + * and takes no harm while flying, since in the original they never left the station. Server only. + */ + public boolean takeRemotePilot(ServerPlayer player, UavStationEntity from) { + if (!canTakeRemotePilot()) { + return false; + } + pendingSeat = 0; + boolean seated = player.startRiding(this, true); + pendingSeat = EMPTY; + if (!seated) { + return false; + } + station = from.getUUID(); + stationReturn = from.returnPosition(); + entityData.set(DATA_STATION, from.getId()); + player.addTag(REMOTE_PILOT_TAG); + player.setInvisible(true); + player.setInvulnerable(true); + return true; + } + + /** Sends the remote pilot back to the station, if one is flying. Server only. */ + public void releaseRemotePilot() { + Entity pilot = occupant(0); + if (station != null && pilot != null) { + pilot.stopRiding(); + } + } + + /** The station this drone is flown from, as the client sees it. */ + public @Nullable UavStationEntity stationEntity() { + return level().getEntity(entityData.get(DATA_STATION)) instanceof UavStationEntity s ? s : null; + } + + /** Flat distance to the station this drone is flown from, or -1 without one. */ + public double stationDistance() { + UavStationEntity from = stationEntity(); + if (from == null) { + return -1.0; + } + double dx = getX() - from.getX(); + double dz = getZ() - from.getZ(); + return Math.sqrt(dx * dx + dz * dz); + } + + /** Whether {@code entity} flies this drone from a station rather than sitting in it. */ + public boolean isRemotePilot(Entity entity) { + return (station != null || entityData.get(DATA_STATION) != 0) && occupant(0) == entity; + } + + private void endRemoteControl(Entity pilot) { + UavStationEntity from = level() instanceof ServerLevel server && station != null + && server.getEntity(station) instanceof UavStationEntity s ? s : null; + Vec3 home = from != null ? from.returnPosition() : stationReturn; + if (home != null) { + remoteReturns.put(pilot.getId(), home); + } + clearRemotePilot(pilot); + if (from != null) { + from.setLinked(false); + } + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + } + + /** Undoes what {@link #takeRemotePilot} did to the player. */ + public static void clearRemotePilot(Entity pilot) { + if (pilot.removeTag(REMOTE_PILOT_TAG)) { + pilot.setInvisible(false); + pilot.setInvulnerable(false); + } + } + + /** + * {@code updateUAV}: the link breaks when the station is gone, and flying out of its range + * loses the drone, as in the original. + */ + private void tickStation() { + if (station == null || !(level() instanceof ServerLevel server)) { + return; + } + Entity pilot = occupant(0); + if (pilot == null) { + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + return; + } + UavStationEntity from = server.getEntity(station) instanceof UavStationEntity s && s.isAlive() ? s : null; + if (from == null) { + pilot.stopRiding(); + return; + } + entityData.set(DATA_STATION, from.getId()); + if (!from.inRange(this) && !isDestroyed()) { + if (pilot instanceof Player player) { + player.displayClientMessage(Component.translatable("message.mcheli.uav.lost"), true); + } + pilot.stopRiding(); + hurtServer(server, damageSources().fellOutOfWorld(), hp() + 10.0F); + } + } + + @Override + public @Nullable LivingEntity getControllingPassenger() { + // A wreck is nobody's to fly; the server lets it fall, so its crash is seen the same way by everyone. + return occupant(0) instanceof Player player && !isDestroyed() ? player : null; + } + + @Override + protected boolean canAddPassenger(Entity passenger) { + AircraftDefinition def = definition(); + if (def == null || !def.canRide) { + return false; + } + if (pendingSeat >= 0) { + return isSeatFree(pendingSeat); + } + // Coming back aboard after a reload, a remote pilot included. + Integer saved = savedSeats.get(passenger.getUUID()); + if (saved != null && isSeatFree(saved)) { + return true; + } + // Cargo only boards through a rack, never into a crew seat. + return !(passenger instanceof AircraftEntity || passenger instanceof ContainerEntity) && firstFreeCrewSeat() >= 0; + } + + @Override + protected void addPassenger(Entity passenger) { + super.addPassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = pendingSeat; + pendingSeat = EMPTY; + if (seat < 0) { + Integer saved = savedSeats.remove(passenger.getUUID()); + seat = saved != null && isSeatFree(saved) ? saved : firstFreeCrewSeat(); + } + assignSeat(passenger, seat); + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = seatOf(passenger); + if (seat >= 0) { + if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(passenger); + } + lastSeats.put(passenger.getId(), seat); + seatIds[seat] = EMPTY; + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onLeft(seat); + } + } + if (seat == 0) { + input.clear(); + if (station != null) { + endRemoteControl(passenger); + } + if (testFlightStart != null) { + remoteReturns.put(passenger.getId(), testFlightStart); + testFlightOver = true; + } + } + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + AircraftDefinition def = definition(); + int seat = seatOf(passenger); + if (def == null || seat < 0 || seat >= def.seatAndRackCount()) { + return super.getPassengerAttachmentPoint(passenger, dimensions, scale); + } + Vector3f p = seatPosition(def, seat, 1.0F); + return new Vec3(p.x, p.y, p.z); + } + + /** World offset of a seat from the vehicle origin. Turret seats turn with the pilot's view. */ + public Vector3f seatPosition(AircraftDefinition def, int seat, float partialTicks) { + Vector3 pos; + boolean turret; + if (seat < def.seatCount()) { + SeatDefinition s = def.seats.get(seat); + pos = s.position(); + turret = s.rotatingSeat(); + } else { + RackDefinition rack = def.rack(seat); + pos = rack.position(); + turret = rack.rotatingSeat(); + } + return toWorldOffset(turret ? onTurret(def, pos, partialTicks) : pos); + } + + /** Rotates a hull-space point around {@code TurretPosition} by the pilot's view yaw. */ + public Vector3 onTurret(AircraftDefinition def, Vector3 pos, float partialTicks) { + Vector3 t = def.turretPosition; + Vector3f v = AircraftRotation.toWorld(animation.seatYaw(0, partialTicks), 0.0F, 0.0F, + pos.x() - t.x(), pos.y() - t.y(), pos.z() - t.z()); + return new Vector3(v.x + t.x(), v.y + t.y(), v.z + t.z()); + } + + /** Rotates a hull-space point into the world frame, relative to the vehicle origin. */ + public Vector3f toWorldOffset(Vector3 local) { + return AircraftRotation.toWorld(getYRot(), getXRot(), roll(), local.x(), local.y(), local.z()); + } + + public Vec3 toWorld(Vector3 local) { + Vector3f v = toWorldOffset(local); + return position().add(v.x, v.y, v.z); + } + + @Override + protected void positionRider(Entity passenger, MoveFunction moveFunction) { + super.positionRider(passenger, moveFunction); + AircraftDefinition def = definition(); + if (def != null && passenger instanceof AircraftEntity cargo) { + RackDefinition rack = def.rack(seatOf(passenger)); + float yaw = rack != null ? rack.yaw() : 0.0F; + float pitch = rack != null ? rack.pitch() : 0.0F; + cargo.setYRot(getYRot() + yaw); + cargo.setXRot(getXRot() + pitch); + cargo.roll = roll(); + cargo.setDeltaMovement(Vec3.ZERO); + } else if (def != null && passenger instanceof ContainerEntity container) { + RackDefinition rack = def.rack(seatOf(passenger)); + container.setYRot(getYRot() + (rack != null ? rack.yaw() : 0.0F)); + container.setDeltaMovement(Vec3.ZERO); + } + } + + @Override + public Vec3 getDismountLocationForPassenger(LivingEntity passenger) { + Vec3 home = remoteReturns.remove(passenger.getId()); + if (home != null) { + return home; + } + AircraftDefinition def = definition(); + Integer seat = lastSeats.get(passenger.getId()); + if (def != null) { + Vector3 local; + if (def.unmountPosition != null) { + local = def.unmountPosition; + } else { + Vector3 seatPos = seat != null && seat < def.seatCount() ? def.seats.get(seat).position() : def.seats.getFirst().position(); + double x = seatPos.x() >= 0.0 ? seatPos.x() + 3.0 : seatPos.x() - 3.0; + local = new Vector3(x, 2.0, seatPos.z()); + } + Vec3 at = toWorld(local); + AABB box = passenger.getDimensions(Pose.STANDING).makeBoundingBox(at); + if (level().noCollision(passenger, box)) { + return at; + } + } + return super.getDismountLocationForPassenger(passenger); + } + + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + ItemStack held = player.getItemInHand(hand); + if (held.getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (definition() != null && (held.getItem() instanceof FuelItem || held.getItem() instanceof WrenchItem)) { + if (level().isClientSide()) { + return InteractionResult.SUCCESS; + } + return held.getItem() instanceof FuelItem ? FuelItem.pour(held, this, player) : WrenchItem.useOn(held, this, player, hand); + } + InteractionResult result = super.interact(player, hand); + AircraftDefinition def = definition(); + if (result == InteractionResult.PASS && player.isSecondaryUseActive() && def != null && def.cargoRows() > 0 + && !isDestroyed() && player.getVehicle() != this) { + // Sneaking opens the cargo chest from outside, as in the original. + if (player instanceof ServerPlayer server) { + openCargo(server); + } + return InteractionResult.SUCCESS; + } + if (result != InteractionResult.PASS || player.isSecondaryUseActive()) { + return result; + } + if (def == null || !def.canRide || player.getVehicle() == this) { + return InteractionResult.PASS; + } + boolean boardingPilot = !def.uav && isSeatFree(0); + if (boardingPilot && canopy != null && isCanopyClosed()) { + // The first click opens the canopy, the next one climbs in. + if (!level().isClientSide()) { + openCanopy(); + } + return InteractionResult.SUCCESS; + } + if (boardingPilot && modeSwitchCooldown > 0) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + if (!player.startRiding(this)) { + return InteractionResult.PASS; + } + if (seatOf(player) == 0) { + closeCanopy(); + boardNearbyMobs(); + } + } + return InteractionResult.SUCCESS; + } + + // Racks + + /** Loads {@code cargo} into the given rack seat. Server only. */ + private boolean loadCargo(Entity cargo, int seat) { + if (!isSeatFree(seat) || cargo.isPassenger()) { + return false; + } + pendingSeat = seat; + boolean loaded = cargo.startRiding(this, true); + pendingSeat = EMPTY; + if (loaded) { + level().playSound(null, getX(), getY(), getZ(), SoundEvents.UI_BUTTON_CLICK, SoundSource.NEUTRAL, 1.0F, 1.0F); + } + return loaded; + } + + /** Whether {@code cargo} may ride rack {@code seat} of this vehicle. */ + public boolean acceptsCargo(AircraftEntity cargo, int seat) { + AircraftDefinition def = definition(); + AircraftDefinition cargoDef = cargo.definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + if (rack == null || cargoDef == null || cargo == this || isPassenger() || cargo.isPassenger()) { + return false; + } + // A vehicle that carries cargo itself cannot be loaded. + for (Entity p : cargo.getPassengers()) { + if (p instanceof AircraftEntity) { + return false; + } + } + if (rack.accepts(cargoDef.name(), cargoDef.kind())) { + return true; + } + for (RideRack rr : cargoDef.rideRacks) { + if (rr.carrier().equalsIgnoreCase(def.name()) && def.seatCount() + rr.rack() - 1 == seat) { + return true; + } + } + return false; + } + + /** Whether rack {@code seat} carries containers, {@code AddRack} naming {@code container}. */ + public boolean acceptsContainer(ContainerEntity container, int seat) { + AircraftDefinition def = definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + return rack != null && !isPassenger() && !container.isPassenger() + && rack.accepts(ContainerEntity.CARGO_NAME, ContainerEntity.CARGO_NAME); + } + + private boolean isNearRackEntry(AircraftEntity cargo, RackDefinition rack) { + Vec3 entry = toWorld(rack.entry()); + float r = rack.range(); + return Math.abs(cargo.getX() - entry.x) <= r && Math.abs(cargo.getY() - entry.y) <= r && Math.abs(cargo.getZ() - entry.z) <= r; + } + + /** Carrier pilot loads every nearby vehicle or container that fits a free rack. Server only. */ + public void loadNearbyCargo() { + AircraftDefinition def = definition(); + if (def == null || def.racks.isEmpty() || throttle > 0.3 || !hasBlockBelow(1, 3, true)) { + return; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (!isSeatFree(seat)) { + continue; + } + RackDefinition rack = def.rack(seat); + Vec3 entry = toWorld(rack.entry()); + AABB area = new AABB(entry, entry).inflate(rack.range()); + boolean loaded = false; + for (AircraftEntity cargo : level().getEntitiesOfClass(AircraftEntity.class, area)) { + if (acceptsCargo(cargo, seat) && loadCargo(cargo, seat)) { + loaded = true; + break; + } + } + if (!loaded) { + for (ContainerEntity container : level().getEntitiesOfClass(ContainerEntity.class, area)) { + if (acceptsContainer(container, seat) && loadCargo(container, seat)) { + break; + } + } + } + } + } + + /** Drops the last loaded piece of cargo at its rack's entry point. Server only. */ + public void unloadCargo() { + AircraftDefinition def = definition(); + if (def == null) { + return; + } + for (int seat = def.seatAndRackCount() - 1; seat >= def.seatCount(); seat--) { + Entity cargo = occupant(seat); + if (cargo == null) { + continue; + } + RackDefinition rack = def.rack(seat); + Vector3 drop = rack.entry(); + if (cargo instanceof AircraftEntity) { + // Aircraft roll out well clear of the carrier's tail or nose. + drop = drop.add(0.0, 0.0, drop.z() >= centreZ(def) ? 12.0 : -12.0); + } + Vec3 at = toWorld(drop); + cargo.stopRiding(); + cargo.snapTo(at.x, at.y, at.z, getYRot() + rack.yaw(), 0.0F); + // As in the original, with no ground in the 20 blocks below it goes down under a canopy. + if (!(cargo instanceof AircraftEntity) && !hasBlockBelow(3, 20, false)) { + ParachuteEntity.open(level(), cargo, ParachuteEntity.Kind.DROP); + } + return; + } + } + + /** Leaves the rack this vehicle rides on, at the rack entry point. Server only. */ + public void leaveRack() { + if (!(getVehicle() instanceof AircraftEntity carrier)) { + return; + } + AircraftDefinition carrierDef = carrier.definition(); + RackDefinition rack = carrierDef != null ? carrierDef.rack(carrier.seatOf(this)) : null; + Vec3 at = rack != null ? carrier.toWorld(rack.entry()) : position(); + stopRiding(); + snapTo(at.x, at.y, at.z, getYRot(), 0.0F); + } + + /** + * Pilot of a vehicle near a free rack of a carrier boards that rack. Server only. + * + * @return whether a rack was found + */ + public boolean boardNearbyRack() { + if (isPassenger()) { + return false; + } + for (AircraftEntity carrier : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(60.0))) { + AircraftDefinition def = carrier.definition(); + if (carrier == this || def == null) { + continue; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (carrier.isSeatFree(seat) && carrier.acceptsCargo(this, seat) && carrier.isNearRackEntry(this, def.rack(seat)) + && carrier.loadCargo(this, seat)) { + return true; + } + } + } + return false; + } + + private static double centreZ(AircraftDefinition def) { + if (def.boundingBoxes.isEmpty()) { + return 0.0; + } + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + for (var box : def.boundingBoxes) { + min = Math.min(min, box.offset().z() - box.width() / 2.0); + max = Math.max(max, box.offset().z() + box.width() / 2.0); + } + return (min + max) / 2.0; + } + + // Ejection + + /** Seats zero and one of vehicles with {@code EnableEjectionSeat} can bail out. */ + public boolean canEject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && def.ejectionSeat && seat >= 0 && seat < 2 && !(seat == 0 && def.uav); + } + + /** + * {@code ejectSeat}: throws the occupant clear of the aircraft in an ejection seat, whose + * parachute opens once it starts to fall. Each ejection takes a parachute from the parachute + * slots of the vehicle screen; without one the seat stays put. Creative players need none. + * Server only. + */ + public void eject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + if (def == null || !canEject(entity)) { + return; + } + boolean creative = entity instanceof Player player && player.getAbilities().instabuild; + if (!creative && !inventory.takeParachute()) { + if (entity instanceof ServerPlayer player) { + ModSounds.playTo(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.no_parachute"), true); + } + return; + } + Vector3 seatPos = def.seats.get(seat).position(); + Vec3 out = toWorld(seatPos.add(0.0, 2.0, 0.0)); + Vector3f push = toWorldOffset(new Vector3(0.0, 2.0, 0.0)); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement().add(push.x + (random.nextFloat() - 0.5) * 0.1, push.y, + push.z + (random.nextFloat() - 0.5) * 0.1)); + entity.hasImpulse = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.EJECTION_SEAT); + if (canopy != null && !canopy.status()) { + canopy.set(true, false); + modeSwitchCooldown = 40; + } + ModSounds.playAt(entity, "eject_seat", 5.0F, 1.0F); + } + + // Search lights + + public boolean isSearchLightOn() { + AircraftDefinition def = definition(); + return def != null && !def.searchLights.isEmpty() && entityData.get(DATA_SEARCH_LIGHT); + } + + /** {@code canSwitchSearchLight}: the pilot or the first crew seat, in vehicles that have one. */ + public boolean canSwitchSearchLight(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && !def.searchLights.isEmpty() && seat >= 0 && seat <= 1; + } + + /** Server only. */ + public void toggleSearchLight(Entity entity) { + if (canSwitchSearchLight(entity)) { + entityData.set(DATA_SEARCH_LIGHT, !entityData.get(DATA_SEARCH_LIGHT)); + } + } + + /** + * Where a turning search light points: the first crew seat's view, else the pilot's, as + * {@code renderLight} took them. Null with nobody aboard to aim it. + */ + public @Nullable Entity searchLightAimer() { + Entity crew = occupant(1); + return crew != null ? crew : occupant(0); + } + + // Troops: parachute drops and ropes + + public boolean isRepelling() { + return entityData.get(DATA_REPELLING); + } + + /** How far the ropes hang below their hooks, as a negative number. */ + public float ropeLength() { + return ropeLength; + } + + /** Where rope {@code index} hangs from, in the world. */ + public Vec3 ropePosition(int index) { + AircraftDefinition def = definition(); + return def != null && index >= 0 && index < def.repellingHooks.size() ? toWorld(def.repellingHooks.get(index).position()) : position(); + } + + /** + * {@code onUpdate_Repelling} and {@code onUpdate_UnmountCrew}: the ropes run out towards the + * ground, then the mobs aboard slide down them one per hook interval; a parachute drop lets one + * passenger out per {@code MobDropOption} interval while the hatch is open. + */ + private void tickRopes(AircraftDefinition def) { + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + int clear = airBelow(50); + if (ropeLength > -50.0F && ropeLength > -clear) { + ropeLength -= level().isClientSide() ? 0.3F : 0.25F; + } + } else { + ropeLength = 0.0F; + } + } + if (level().isClientSide()) { + return; + } + if (isRepelling() && isDestroyed()) { + entityData.set(DATA_REPELLING, false); + } + if (!isRepelling()) { + repellingTicks = 0; + } else if (repellingTicks < 60) { + repellingTicks++; + } else { + for (int rope = 0; rope < def.repellingHooks.size(); rope++) { + if (ticksAlive % def.repellingHooks.get(rope).interval() != 0) { + continue; + } + for (int seat = 1; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + rappel(mob, rope); + break; + } + } + } + } + if (droppingTroops) { + if (hasBlockBelow(3, 10, false) || isDestroyed()) { + droppingTroops = false; + } else if ((hatch == null || hatch.isOn()) && ticksAlive % def.dropInterval == 0 && !dropNextTroop(def)) { + droppingTroops = false; + } + } + } + + /** Air blocks straight below, up to {@code max}. */ + private int airBelow(int max) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + int i = 0; + while (i < max && pos.getY() - i > level().getMinY() && level().getBlockState(pos.setY(blockPosition().getY() - i - 1)).isAir()) { + i++; + } + return i; + } + + /** + * {@code canStartRepelling}: a helicopter in hover mode, level and holding still. The original + * compared against where it was a few ticks before. + */ + private boolean canStartRepelling() { + return this instanceof HelicopterEntity && isHoveringMode() && Math.abs(getXRot()) < 3.0F && Math.abs(roll()) < 3.0F + && lastTickPos != null && position().distanceTo(lastTickPos) < 0.05; + } + + /** + * The pilot's drop key, {@code unmountCrew}: with ropes, lets them down from a steady hover + * or pulls them up again; with {@code EnableParachuting}, starts or stops dropping the mobs + * aboard under parachutes; otherwise, or close to the ground, simply lets the mobs out. + * Server only. + * + * @return false if nothing could be done, so the pilot hears it + */ + public boolean dropCrew() { + AircraftDefinition def = definition(); + if (def == null || isDestroyed()) { + return false; + } + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + entityData.set(DATA_REPELLING, false); + return true; + } + if (hasBlockBelow(3, 4, false)) { + return unloadMobs(); + } + if (!canStartRepelling()) { + return false; + } + entityData.set(DATA_REPELLING, true); + repellingTicks = 0; + return true; + } + if (droppingTroops) { + droppingTroops = false; + return true; + } + if (def.parachuting && !hasBlockBelow(3, 10, false)) { + droppingTroops = true; + if (hatch != null && hatch.isOff()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } + return true; + } + return unloadMobs(); + } + + /** Lets every mob in a crew seat out, as in {@code unmountCrew(false)}. */ + private boolean unloadMobs() { + AircraftDefinition def = definition(); + boolean any = false; + for (int seat = 1; def != null && seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + mob.stopRiding(); + any = true; + } + } + return any; + } + + /** The next mob behind the second seat leaves at the drop point under a parachute. */ + private boolean dropNextTroop(AircraftDefinition def) { + for (int seat = 2; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + parachuteOut(mob, def); + return true; + } + } + return false; + } + + private void parachuteOut(Entity entity, AircraftDefinition def) { + Vec3 out = toWorld(def.dropPosition); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement()); + entity.hasImpulse = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.DROP); + } + + /** + * A passenger's own drop key, {@code MCH_ClientSeatTickHandler}: slide down a rope once the + * ropes are out, or jump with a parachute from behind the second seat of a vehicle with + * {@code EnableParachuting} and room below. Server only. + * + * @return false if neither is possible right now + */ + public boolean jumpOut(Player player) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (def == null || seat <= 0 || seat >= def.seatCount() || isDestroyed()) { + return false; + } + if (isRepelling() && repellingTicks > 50 && !def.repellingHooks.isEmpty()) { + lastRope = (lastRope + 1) % def.repellingHooks.size(); + rappel(player, lastRope); + return true; + } + if (def.parachuting && seat >= 2 && !hasBlockBelow(3, 13, false)) { + parachuteOut(player, def); + return true; + } + return false; + } + + /** {@code unmountEntityRepelling}: two blocks under the hook, onto a rope rider that lets them down. */ + private void rappel(Entity entity, int rope) { + Vec3 at = ropePosition(rope).add(0.0, -2.0, 0.0); + entity.stopRiding(); + entity.teleportTo(at.x, at.y, at.z); + entity.setDeltaMovement(Vec3.ZERO); + entity.resetFallDistance(); + RappelEntity rider = new RappelEntity(ModEntities.RAPPEL.get(), level()); + rider.attach(this, rope); + rider.setPos(at); + level().addFreshEntity(rider); + entity.startRiding(rider, true); + lastRope = rope; + } + + /** + * {@code mountMobToSeats}: when the pilot climbs in, mobs standing close by take the free crew + * seats, so villagers and the like can be flown somewhere. Server only. + */ + private void boardNearbyMobs() { + AircraftDefinition def = definition(); + if (def == null || def.uav) { + return; + } + for (Mob mob : level().getEntitiesOfClass(Mob.class, getBoundingBox().inflate(3.0, 2.0, 3.0), m -> !m.isPassenger() && !m.isVehicle())) { + for (int seat = 1; seat < def.seatCount(); seat++) { + if (isSeatFree(seat)) { + pendingSeat = seat; + mob.startRiding(this, true); + pendingSeat = EMPTY; + break; + } + } + } + } + + // Terrain helpers + + /** True if any solid block is within {@code depth} blocks below the vehicle. */ + protected boolean isNearGround(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(0, -1, 0); + if (!level().getBlockState(pos).getCollisionShape(level(), pos).isEmpty()) { + return true; + } + } + return false; + } + + /** + * Port of {@code MCH_Lib.getBlockY}: scans a {@code size} x {@code size} column from the + * vehicle's block down {@code depth} layers (the first layer being the vehicle's own). + * + * @param solidOnly ignore blocks without collision; fluids still count when this is false + */ + public boolean hasBlockBelow(int size, int depth, boolean solidOnly) { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < depth; y++) { + for (int x = -size / 2; x <= size / 2; x++) { + for (int z = -size / 2; z <= size / 2; z++) { + pos.set(px + x, py - y, pz + z); + BlockState state = level().getBlockState(pos); + if (state.isAir()) { + continue; + } + if (!solidOnly || !state.getCollisionShape(level(), pos).isEmpty() || !state.getFluidState().isEmpty()) { + return true; + } + } + } + } + return false; + } + + /** True if the first non-air block within {@code depth} below is water. */ + protected boolean isWaterBelow(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + BlockState state = level().getBlockState(pos); + if (!state.isAir()) { + return !state.getFluidState().isEmpty(); + } + pos.move(0, -1, 0); + } + return false; + } + + /** {@code getBlockY(this, 3, -2, false)}: standing on something that is neither air nor water. */ + protected boolean isOnSolidGround() { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < 2; y++) { + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + BlockState state = level().getBlockState(pos.set(px + x, py - y, pz + z)); + if (!state.isAir()) { + return state.getFluidState().isEmpty(); + } + } + } + } + return false; + } + + /** + * Port of {@code getWaterDepth}: the share of the hit box that is in water, from 0 to 1, sampled + * in five horizontal slices. {@code FloatOffset} raises the slices, so a hull with an offset rides + * higher. The float physics only pushes up once this reaches 1, which sets the waterline. + * + *

The original drew the model {@value #LEGACY_MODEL_LIFT} above the bottom of the hit box + * (its {@code yOffset}, half the 0.7 default height); this port draws it at the bottom. The + * slices are lowered by the same amount so boats sit in the water as they did in 1.0.3. + */ + protected double waterDepth() { + if (entityData.get(DATA_SINKING)) { + return 0.0; + } + AircraftDefinition def = definition(); + float offset = (def != null ? def.floatOffset : 0.0F) - LEGACY_MODEL_LIFT; + AABB box = getBoundingBox(); + double height = box.maxY - box.minY; + double depth = 0.0; + for (int i = 0; i < 5; i++) { + double bottom = box.minY + height * i / 5.0 - 0.125 + offset; + double top = box.minY + height * (i + 1) / 5.0 - 0.125 + offset; + AABB slice = new AABB(box.minX, bottom, box.minZ, box.maxX, top, box.maxZ); + if (level().getBlockStates(slice).anyMatch(state -> state.getFluidState().is(FluidTags.WATER))) { + depth += 1.0 / 5.0; + } + } + return depth; + } + + /** {@code applyOnGroundPitch}: eases pitch towards {@code OnGroundPitch} and roll towards level. */ + protected void applyOnGroundPitch(float factor) { + AircraftDefinition def = definition(); + if (def != null) { + float ground = def.onGroundPitch; + setXRot((getXRot() - ground) * factor + ground); + } + roll *= factor; + } + + /** {@code StepHeight}: how tall a ledge the vehicle drives up without jumping. */ + @Override + public float maxUpStep() { + AircraftDefinition def = definition(); + return def != null ? def.stepHeight : 0.0F; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith() { + return true; + } + + @Override + public boolean isPushable() { + return false; + } + + // Weapons + + public @Nullable VehicleWeapons weapons() { + definition(); + return weapons; + } + + public void publishWeaponSelection() { + if (weapons != null && !level().isClientSide()) { + entityData.set(DATA_WEAPON_SELECTION, weapons.encodeSelection()); + } + } + + public void publishWeaponUse(int stat) { + entityData.set(DATA_WEAPON_USE, stat); + } + + public int syncedWeaponUse() { + return entityData.get(DATA_WEAPON_USE); + } + + /** + * Whether {@code set} fires along the user's view instead of the hull: the shot direction and + * the muzzle positions are worked out as if the vehicle faced where the user looks. Static + * mounts do this; see {@link GroundVehicleEntity#aimsWithUser}. + */ + public boolean aimsWithUser(WeaponSet set) { + return false; + } + + /** {@code isInfinityAmmo}: creative players never run dry, nor does anyone when the server config says so. */ + public boolean hasInfiniteAmmo(@Nullable Entity user) { + return ServerConfig.infiniteAmmo() || user instanceof Player player && player.getAbilities().instabuild; + } + + /** Where the pilot looks relative to the hull: the turret's traverse. */ + public float turretYaw() { + return animation.seatYaw(0, 1.0F); + } + + public float stealth() { + AircraftDefinition def = definition(); + return def != null ? def.stealth : 0.0F; + } + + /** The TV missile the crew is flying, if it still exists. */ + public @Nullable TvMissileEntity tvMissile() { + if (level().isClientSide()) { + return level().getEntity(entityData.get(DATA_TV_MISSILE)) instanceof TvMissileEntity m && !m.isRemoved() ? m : null; + } + return tvMissile != null && !tvMissile.isRemoved() ? tvMissile : null; + } + + public void setTvMissile(@Nullable TvMissileEntity missile) { + tvMissile = missile; + entityData.set(DATA_TV_MISSILE, missile != null ? missile.getId() : 0); + } + + /** Whether the flares are burning right now, for the crew's display. */ + public boolean isFlareActive() { + return entityData.get(DATA_FLARE); + } + + /** Someone is locking on to this vehicle; its crew hears the warning. Server only. */ + public void warnLocked() { + if (alertCount == 0 && !isDestroyed() && weapons != null && weapons.flares.isFitted()) { + alertCount = 10; + for (int seat = 0; seat < 2; seat++) { + if (occupant(seat) instanceof ServerPlayer player) { + ModSounds.playTo(player, "locked", 1.0F, 1.0F); + } + } + } + } + + // Hit boxes + + /** + * Clips a ray against the main hit box and every {@code BoundingBox}, remembering the damage + * factor of the box hit first, as the original's composite bounding box did. + */ + public Optional clipHitBoxes(Vec3 from, Vec3 to, double inflate) { + hitBoxFactor = 1.0F; + Optional best = clip(getBoundingBox().inflate(inflate), from, to); + double bestDistance = best.map(from::distanceToSqr).orElse(Double.MAX_VALUE); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + Optional hit = clip(hullBox(box).inflate(inflate), from, to); + if (hit.isPresent() && from.distanceToSqr(hit.get()) < bestDistance) { + best = hit; + bestDistance = from.distanceToSqr(hit.get()); + hitBoxFactor = box.damageFactor(); + } + } + } + return best; + } + + /** + * One {@code BoundingBox} in world space. Like the original's, it stays axis aligned: the + * centre turns with the hull, the box itself does not. + */ + private AABB hullBox(BoundingBoxDefinition box) { + Vec3 c = toWorld(box.offset()); + double hw = box.width() / 2.0; + double hh = box.height() / 2.0; + return new AABB(c.x - hw, c.y - hh, c.z - hw, c.x + hw, c.y + hh, c.z + hw); + } + + /** The whole hull: the main hit box, every {@code BoundingBox} and the generated boxes, in world space. */ + public List hullBoxes() { + AircraftDefinition def = definition(); + if (def == null || def.hull().isEmpty()) { + return List.of(getBoundingBox()); + } + List boxes = new ArrayList<>(def.hull().size() + 1); + boxes.add(getBoundingBox()); + for (BoundingBoxDefinition box : def.hull()) { + boxes.add(hullBox(box)); + } + return boxes; + } + + /** The smallest box around the whole hull. */ + private AABB hullBounds() { + AABB bounds = getBoundingBox(); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + bounds = bounds.minmax(hullBox(box)); + } + } + return bounds; + } + + /** Whether any part of this hull touches any part of {@code other}'s, grown by {@code inflate}. */ + private boolean hullsTouch(AircraftEntity other, double inflate) { + // Hulls generated from big models run to a thousand boxes, so skip ours that are nowhere near. + AABB around = other.hullBounds().inflate(inflate); + List theirs = null; + for (AABB mine : hullBoxes()) { + AABB grown = mine.inflate(inflate); + if (!grown.intersects(around)) { + continue; + } + if (theirs == null) { + theirs = other.hullBoxes(); + } + for (AABB box : theirs) { + if (grown.intersects(box)) { + return true; + } + } + } + return false; + } + + /** + * Vehicles whose hulls block this one: everything nearby except its own carrier or cargo, and + * vehicles either of the two could load onto a rack, which have to drive into each other. + */ + private List solidNeighbours(AABB area) { + return level().getEntitiesOfClass(AircraftEntity.class, area.inflate(HULL_REACH), other -> other != this && !other.isRemoved() + && other.definition() != null && !other.isPassenger() && !isPassengerOfSameVehicle(other) + && other.getVehicle() != this && getVehicle() != other && !canCarry(other) && !other.canCarry(this)); + } + + /** Whether a free rack of this vehicle would take {@code cargo}. */ + private boolean canCarry(AircraftEntity cargo) { + AircraftDefinition def = definition(); + if (def == null) { + return false; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (acceptsCargo(cargo, seat)) { + return true; + } + } + return false; + } + + /** + * Not in the original, where only the main box at a vehicle's feet collided: the whole hull + * blocks other vehicles. A move that would push this hull into another vehicle's is undone, + * sideways first and then completely, so a helicopter can also set down on a parked truck. + * Hulls that already overlapped, say after two vehicles were placed into each other, may move + * apart. + */ + @Override + public void move(MoverType type, Vec3 delta) { + if (type != MoverType.SELF || isPassenger() || definition() == null) { + super.move(type, delta); + return; + } + List near = solidNeighbours(hullBounds().expandTowards(delta)); + if (near.isEmpty()) { + super.move(type, delta); + return; + } + List overlapping = new ArrayList<>(); + for (AircraftEntity other : near) { + if (hullsTouch(other, 0.0)) { + overlapping.add(other); + } + } + Vec3 before = position(); + super.move(type, delta); + if (!entersHull(near, overlapping)) { + return; + } + Vec3 m = getDeltaMovement(); + setPos(before.x, getY(), before.z); + setDeltaMovement(0.0, m.y, 0.0); + if (entersHull(near, overlapping)) { + setPos(before); + setDeltaMovement(Vec3.ZERO); + } + } + + private boolean entersHull(List near, List overlapping) { + for (AircraftEntity other : near) { + if (!overlapping.contains(other) && hullsTouch(other, 0.0)) { + return true; + } + } + return false; + } + + /** A ray that starts inside a box hits it where it starts, as it does for vanilla entity picking. */ + private static Optional clip(AABB box, Vec3 from, Vec3 to) { + return box.contains(from) ? Optional.of(from) : box.clip(from, to); + } + + public float consumeHitBoxFactor() { + float f = hitBoxFactor; + hitBoxFactor = 1.0F; + return f; + } + + // Damage + + public boolean isDestroyed() { + return definition() != null && hp() <= 0; + } + + /** + * Port of the original's {@code attackEntityFrom}. Armour caps a hit at {@code ArmorMaxDamage}, + * scales it by {@code ArmorDamageFactor} and the struck box's factor, then subtracts + * {@code ArmorMinDamage}. A player punching an empty vehicle packs it up instead. + */ + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + float boxFactor = consumeHitBoxFactor(); + if (testFlightStart != null || isRemoved() || isInvulnerableToBase(source) || timeSinceHit > 0) { + return false; + } + if (source.is(DamageTypes.IN_FIRE) || source.is(DamageTypes.CACTUS)) { + return false; + } + boolean lava = source.is(DamageTypes.LAVA); + boolean onFire = source.is(DamageTypes.ON_FIRE); + if (lava) { + damage *= random.nextInt(8) + 2; + timeSinceHit = 2; + } + if (source.is(DamageTypeTags.IS_EXPLOSION)) { + timeSinceHit = 1; + } else if (source.getEntity() != null && hasPassenger(source.getEntity())) { + return false; + } + if (onFire) { + timeSinceHit = 10; + } + + boolean creative = false; + boolean sneaking = false; + boolean packUp = false; + if (source.getEntity() instanceof Player player) { + creative = player.getAbilities().instabuild; + sneaking = player.isShiftKeyDown(); + if (source.is(DamageTypes.PLAYER_ATTACK)) { + packUp = creative || !hasPlayerPassenger(); + } + ModSounds.playAt(this, "hit", 1.0F, 1.0F); + } else { + ModSounds.playAt(this, "helidmg", 1.0F, 0.9F + random.nextFloat() * 0.1F); + } + + if (!isDestroyed()) { + if (!packUp) { + AircraftDefinition def = definition(); + if (def != null && !lava && !onFire) { + damage = Math.min(damage, def.armorMaxDamage); + if (boxFactor <= 1.0F) { + damage *= boxFactor; + } + damage *= def.armorDamageFactor; + damage -= def.armorMinDamage; + if (damage <= 0.0F) { + return false; + } + if (boxFactor > 1.0F) { + damage *= boxFactor; + } + } + damage *= ServerConfig.vehicleDamageFactor(); + entityData.set(DATA_HP, Math.max(hp() - (int) damage, 0)); + } + setHurtDir(-getHurtDir()); + // No markHurt(): it sends the server's motion to every client, which snaps a vehicle + // its pilot is flying back a step and looks like knockback on every hit. + setHurtTime(10); + if (hp() <= 0 || packUp) { + if (!packUp) { + entityData.set(DATA_HP, 0); + destroyVehicle(); + timeSinceHit = 20; + if (isAfloat() || isHeavy() && isNearGround(3)) { + startCookOff(source.getEntity()); + } else if (source.is(DamageTypes.IN_WALL) || source.is(DamageTypes.FLY_INTO_WALL) || source.is(DamageTypes.FALL)) { + crashExplosion(); + } else { + Explosions.explode(level, getX(), getY(), getZ(), + new Explosions.Params(null, source.getEntity(), 2.0F, 2.0F, true, true, 5, false, null)); + } + } else { + // In creative the item only drops when sneaking, as with the original's defaults. + if (!creative || sneaking) { + spawnAtLocation(level, getPickResult() != null ? getPickResult() : ItemStack.EMPTY); + } + discard(); + } + } + } else if (packUp && creative) { + discard(); + } + return true; + } + + private boolean hasPlayerPassenger() { + for (Entity passenger : getPassengers()) { + if (passenger instanceof Player) { + return true; + } + } + return false; + } + + /** {@code destroyAircraft}: the wreck tumbles, burns and disappears after 30 seconds. Server only. */ + private void destroyVehicle() { + AircraftDefinition def = definition(); + despawnCount = def != null && def.targetDrone ? 50 : WRECK_TICKS; + setHovering(false); + if (def != null && def.uav) { + Entity remote = occupant(0); + if (remote != null) { + remote.stopRiding(); + } + } + for (int seat = 0; seat < 2; seat++) { + Entity crew = occupant(seat); + if (crew != null && canEject(crew)) { + eject(crew); + } + } + } + + /** {@code explosionByCrash}: a wreck hitting the ground goes up in a fireball sized by its fuel. */ + private void crashExplosion() { + if (!(level() instanceof ServerLevel level)) { + return; + } + if (isHeavy()) { + startCookOff(null); + return; + } + exploded = true; + AircraftDefinition def = definition(); + float size = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + Vec3 at = def != null && def.fuelTankPosition != null ? toWorld(def.fuelTankPosition) : position(); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, null, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + if (!isInWater()) { + Explosions.crashFireball(level, at.x, at.y, at.z, size); + } + } + + /** + * Airliners, bombers and transports ({@code MaxFuel} of {@value HEAVY_FUEL} or more): like a + * boat, they go up in three blasts rather than one fireball. + */ + private boolean isHeavy() { + AircraftDefinition def = definition(); + return def != null && def.maxFuel >= HEAVY_FUEL; + } + + /** A floating vehicle sitting on the water, such as a patrol boat or a float plane at rest. */ + private boolean isAfloat() { + AircraftDefinition def = definition(); + return def != null && def.floats && waterDepth() > 0.0; + } + + /** + * A boat destroyed on the water, or a heavy aircraft wrecked on the ground, does not go up in + * one crash fireball; its fuel and magazine cook off in three blasts over about five seconds, + * then a hull in the water sinks. Not in the original, which only had the crash explosion. + */ + private void startCookOff(@Nullable Entity cause) { + exploded = true; + cookOffBlasts = 3; + cookOffDelay = 1; + cookOffCause = cause; + } + + private void tickCookOff(ServerLevel level) { + if (cookOffBlasts <= 0 || --cookOffDelay > 0) { + return; + } + AircraftDefinition def = definition(); + int blast = 4 - cookOffBlasts--; + float fuel = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + // The fuel tank first, then whatever the magazine still holds, then the hull goes up. + float size = switch (blast) { + case 1 -> fuel * 0.6F; + case 2 -> Math.max(munitionSize(), fuel * 0.4F); + default -> fuel; + }; + size = Mth.clamp(size, 1.0F, 15.0F); + Vec3 at = cookOffPoint(def); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, cookOffCause, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + // The blasts happen at the waterline, but the fire is above it, so show the full fireball. + Explosions.crashFireball(level, at.x, at.y, at.z, size); + if (cookOffBlasts > 0) { + // Three blasts inside five seconds: about 1.5 to 2.5 seconds apart. + cookOffDelay = 30 + random.nextInt(20); + } else { + // A boat or a flying boat goes under; a heavy aircraft on land stays where it burns. + if (isInWater() || waterDepth() > 0.0) { + entityData.set(DATA_SINKING, true); + } + cookOffCause = null; + } + } + + /** + * A cooked-off wreck going under, in place of the vehicle's own physics: it settles slowly, + * stern first with the bow raised and listing to one side, drifts to a stop and comes to rest + * on the bottom. Out of the water it simply falls. + */ + private void sink() { + Vec3 m = getDeltaMovement(); + double my = isInWater() ? Math.max(m.y - 0.003, -SINK_SPEED) : m.y - 0.04; + Vec3 motion = new Vec3(m.x * 0.9, my, m.z * 0.9); + move(MoverType.SELF, motion); + setDeltaMovement(onGround() ? motion.multiply(0.5, 0.0, 0.5) : motion); + if (!onGround()) { + // Which way it lists only has to be the same on every side, so take it from the id. + float list = (getId() & 1) == 0 ? SINK_LIST : -SINK_LIST; + setXRot(Mth.approach(getXRot(), -SINK_PITCH, 0.12F)); + roll = Mth.approach(roll, list, 0.15F); + } + } + + /** + * How big the ammunition aboard goes up: each weapon's {@code Explosion} scaled by how full its + * magazine and reserve still are. Guns and empty racks add nothing. + */ + private float munitionSize() { + if (weapons == null) { + return 0.0F; + } + float size = 0.0F; + for (WeaponSet set : weapons.sets()) { + int rounds = set.ammo() + set.restAmmo(); + if (rounds > 0 && set.definition().explosion > 0) { + size += set.definition().explosion * Math.min(1.0F, (float) rounds / Math.max(set.maxAmmo(), 1)); + } + } + return size; + } + + /** A random spot on the hull: one of its hit boxes, or the middle of the body. */ + private Vec3 cookOffPoint(@Nullable AircraftDefinition def) { + if (def == null || def.boundingBoxes.isEmpty() || random.nextInt(def.boundingBoxes.size() + 1) == 0) { + return position().add(0.0, def != null ? def.bodyHeight / 2.0 : 0.5, 0.0); + } + return toWorld(def.boundingBoxes.get(random.nextInt(def.boundingBoxes.size())).offset()); + } + + /** Destroyed state each tick: the wreck spins, sets its crew alight and explodes on impact. */ + private void tickDestroyed() { + if (!isDestroyed()) { + return; + } + input.clear(); + throttle *= 0.98; + crashSpin(); + if (!(level() instanceof ServerLevel level)) { + return; + } + tickCookOff(level); + if (ticksAlive % 20 == 0) { + AircraftDefinition def = definition(); + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null && !(seat == 0 && def != null && def.uav)) { + crew.igniteForSeconds(5.0F); + } + } + } + if (despawnCount > 0 && --despawnCount <= 1) { + ejectPassengers(); + discard(); + } + } + + /** + * Not in the original: a shot down plane rolls into a nose-down spiral and a helicopter spins + * with some wobble, until it is close to the ground. Tanks and cars just stop. + */ + private void crashSpin() { + if (!(this instanceof HelicopterEntity) && !(this instanceof PlaneEntity) || isNearGround(3) || isInWater()) { + return; + } + int direction = CrashSpin.direction(getId()); + int ticks = spinTicks++; + if (this instanceof HelicopterEntity) { + setYRot(getYRot() + CrashSpin.helicopterYawRate(ticks, direction)); + setXRot(Mth.approach(getXRot(), CrashSpin.helicopterWobblePitch(ticks, getId()), 2.0F)); + roll = Mth.approach(roll, CrashSpin.helicopterWobbleRoll(ticks, getId()), 3.0F); + } else { + setYRot(getYRot() + CrashSpin.planeYawRate(ticks, direction)); + setXRot(CrashSpin.planePitch(getXRot())); + roll = AircraftRotation.wrap(roll + CrashSpin.planeRollRate(ticks, direction)); + } + } + + /** {@code onUpdate_Particle2}: smoke from a damaged hull, thicker the lower its hit points. */ + private void damageSmoke(AircraftDefinition def) { + float ratio = (float) hp() / def.maxHp; + if (ratio > 0.5F) { + return; + } + int sources = 1 + def.boundingBoxes.size(); + int pick = random.nextInt(sources); + Vec3 at = pick == 0 ? position().add(0.0, def.bodyHeight / 2.0, 0.0) : toWorld(def.boundingBoxes.get(pick - 1).offset()); + if (ratio < random.nextFloat() / 2.0F) { + level().addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, at.x, at.y, at.z, + (random.nextDouble() - 0.5) * 0.3, random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.3); + } + if (isDestroyed()) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, at.x, at.y, at.z, 0.0, 0.07, 0.0); + if (random.nextInt(3) == 0) { + level().addParticle(ParticleTypes.FLAME, at.x + random.nextGaussian() * 0.5, at.y, at.z + random.nextGaussian() * 0.5, 0.0, 0.05, 0.0); + } + wreckFire(def); + } + } + + /** + * Not in the original: the wreck keeps burning until it disappears, with a fire glow that + * lights up the smoke at night and dies down over the last few seconds. Nothing burns under + * water. Client only. + */ + private void wreckFire(AircraftDefinition def) { + wreckTicks++; + if (isInWater()) { + return; + } + float left = Math.clamp((float) (WRECK_TICKS - wreckTicks) / WRECK_FADE_TICKS, 0.15F, 1.0F); + Vec3 centre = def.enginePosition != null ? toWorld(def.enginePosition) : position().add(0.0, def.bodyHeight / 2.0, 0.0); + if (wreckTicks % 8 == 0) { + // Each glow outlives the gap to the next, so the fire never goes dark in between. + float size = Math.clamp(def.bodyWidth * 0.8F, 2.0F, 8.0F) * left; + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size, 24), true, + centre.x, centre.y, centre.z, 0.0, 0.02, 0.0); + } + if (random.nextFloat() < left) { + double spread = def.bodyWidth * 0.4; + level().addParticle(ParticleTypes.FLAME, centre.x + random.nextGaussian() * spread, centre.y, + centre.z + random.nextGaussian() * spread, 0.0, 0.04, 0.0); + } + if (random.nextInt(10) == 0) { + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 20 + random.nextInt(20)), true, centre.x, centre.y, centre.z, + (random.nextDouble() - 0.5) * 0.15, 0.1 + random.nextDouble() * 0.15, (random.nextDouble() - 0.5) * 0.15); + } + } + + /** A vehicle that is destroyed, packed up or killed spills what it carried; one that unloads keeps it. */ + @Override + public void remove(RemovalReason reason) { + if (reason.shouldDestroy() && !level().isClientSide() && !isRemoved()) { + inventory.dropAll(); + } + super.remove(reason); + } + + @Override + protected Item getDropItem() { + AircraftDefinition def = definition(); + return def != null ? ModItems.forKind(def.kind()) : Items.AIR; + } + + @Override + public @Nullable ItemStack getPickResult() { + AircraftDefinition def = definition(); + return def != null ? AircraftItem.stackOf(def) : null; + } + + // Dimensions and syncing + + @Override + public EntityDimensions getDimensions(Pose pose) { + AircraftDefinition def = definition(); + return def != null ? EntityDimensions.scalable(def.bodyWidth, def.bodyHeight) : super.getDimensions(pose); + } + + @Override + public void onSyncedDataUpdated(EntityDataAccessor accessor) { + super.onSyncedDataUpdated(accessor); + if (DATA_TYPE.equals(accessor)) { + refreshDimensions(); + definition(); + } else if (DATA_SEATS.equals(accessor) && level().isClientSide()) { + decodeSeats(entityData.get(DATA_SEATS)); + } else if (DATA_WEAPON_SELECTION.equals(accessor) && level().isClientSide() && weapons() != null) { + weapons.decodeSelection(entityData.get(DATA_WEAPON_SELECTION), ClientAccess.localPlayer()); + } + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + output.putString("Type", typeName()); + output.putString("Texture", texture()); + output.putInt("Hp", hp()); + output.putInt("Fuel", fuel()); + output.putFloat("Roll", roll); + output.putBoolean("Hovering", isHoveringMode()); + output.putBoolean("GunnerMode", isGunnerMode()); + output.putInt("Parts", partBits()); + output.putInt("DespawnCount", despawnCount); + output.putInt("CookOff", cookOffBlasts); + output.putBoolean("Sinking", entityData.get(DATA_SINKING)); + output.putBoolean("SearchLight", entityData.get(DATA_SEARCH_LIGHT)); + if (weapons != null) { + output.putIntArray("WeaponsAmmo", weapons.saveAmmo()); + } + inventory.save(output, registryAccess()); + if (station != null && stationReturn != null) { + output.store("Station", UUIDUtil.CODEC, station); + output.store("StationReturn", Vec3.CODEC, stationReturn); + } + List seats = new ArrayList<>(); + for (int i = 0; i < seatIds.length; i++) { + Entity e = occupant(i); + seats.add(e != null ? e.getUUID().toString() : ""); + } + output.store("Seats", Codec.STRING.listOf(), seats); + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + entityData.set(DATA_TYPE, input.getStringOr("Type", "")); + entityData.set(DATA_TEXTURE, input.getStringOr("Texture", "")); + entityData.set(DATA_PARTS, input.getIntOr("Parts", 0)); + AircraftDefinition def = definition(); + entityData.set(DATA_HP, input.getIntOr("Hp", def != null ? def.maxHp : 1)); + // 1.0.0 and the earlier betas had no fuel; their vehicles come back with a full tank. + entityData.set(DATA_FUEL, input.getIntOr("Fuel", def != null ? def.maxFuel : 0)); + roll = input.getFloatOr("Roll", 0.0F); + setHovering(input.getBooleanOr("Hovering", false)); + entityData.set(DATA_GUNNER_MODE, input.getBooleanOr("GunnerMode", false)); + despawnCount = input.getIntOr("DespawnCount", 0); + if (hp() <= 0 && despawnCount <= 0) { + despawnCount = WRECK_TICKS; + } + cookOffBlasts = input.getIntOr("CookOff", 0); + cookOffDelay = cookOffBlasts > 0 ? 20 : 0; + entityData.set(DATA_SINKING, input.getBooleanOr("Sinking", false)); + entityData.set(DATA_SEARCH_LIGHT, input.getBooleanOr("SearchLight", false)); + exploded |= cookOffBlasts > 0 || entityData.get(DATA_SINKING); + input.getIntArray("WeaponsAmmo").ifPresent(ammo -> { + if (weapons != null) { + weapons.loadAmmo(ammo); + } else { + savedAmmo = ammo; + } + }); + inventory.load(input, registryAccess()); + station = input.read("Station", UUIDUtil.CODEC).orElse(null); + stationReturn = input.read("StationReturn", Vec3.CODEC).orElse(null); + savedSeats.clear(); + List seats = input.read("Seats", Codec.STRING.listOf()).orElse(List.of()); + for (int i = 0; i < seats.size(); i++) { + if (!seats.get(i).isEmpty()) { + try { + savedSeats.put(UUID.fromString(seats.get(i)), i); + } catch (IllegalArgumentException ignored) { + // A hand edited save; that occupant simply takes the first free seat. + } + } + } + for (PartState part : parts) { + part.force(part.status()); + } + refreshDimensions(); + } + + /** Raw pilot input, filled in by the client controller every tick. */ + public static final class ControlInput { + public boolean throttleUp; + public boolean throttleDown; + public boolean left; + public boolean right; + public boolean brake; + /** The pilot is looking around instead of steering; planes and tanks yaw with A/D then. */ + public boolean freeLook; + + public void clear() { + throttleUp = false; + throttleDown = false; + left = false; + right = false; + brake = false; + freeLook = false; + } + + public byte write() { + return (byte) ((throttleUp ? 1 : 0) | (throttleDown ? 2 : 0) | (left ? 4 : 0) | (right ? 8 : 0) + | (brake ? 16 : 0) | (freeLook ? 32 : 0)); + } + + public void read(byte bits) { + throttleUp = (bits & 1) != 0; + throttleDown = (bits & 2) != 0; + left = (bits & 4) != 0; + right = (bits & 8) != 0; + brake = (bits & 16) != 0; + freeLook = (bits & 32) != 0; + } + } +} From b1bc1435cf4b923bbf703f0a453453be3c31bc59 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:35 +0800 Subject: [PATCH 0968/1461] Add entity/AnimationState.java to the 1.21.5 shared sources --- .../mcheli/entity/AnimationState.java | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java new file mode 100644 index 00000000..eda5ed7b --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java @@ -0,0 +1,202 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.RackDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; + +/** + * Purely visual state that every side advances once per tick: wheels, tracks, spinning parts and + * where each crew member is looking relative to the hull. + * + *

Ported from {@code updatePartWheel}, {@code updatePartCrawlerTrack} and + * {@code onUpdate_PartRotation}. Nothing in here affects physics, so it is never synced; each + * client derives it from the synced throttle, input and crew rotations. + */ +public final class AnimationState { + /** Road wheel roll in degrees. */ + public float wheelRoll; + public float prevWheelRoll; + /** Steering from -1 (full right) to 1 (full left). */ + public float steer; + public float prevSteer; + /** Track roller angle per side, index 1 is positive X. */ + public final float[] roller = new float[2]; + public final float[] prevRoller = new float[2]; + /** Track run per side, in link spacings from 0 to 1. */ + public final float[] track = new float[2]; + public final float[] prevTrack = new float[2]; + private final float[] trackSpeed = new float[2]; + public float[] rotatingParts = new float[0]; + public float[] prevRotatingParts = new float[0]; + public float throttle; + public float prevThrottle; + /** Per seat and rack: view yaw and pitch of the occupant relative to the hull. */ + public float[] seatYaw = new float[0]; + public float[] prevSeatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public float[] prevSeatPitch = new float[0]; + + void resize(AircraftDefinition def) { + rotatingParts = new float[def.rotatingParts.size()]; + prevRotatingParts = new float[def.rotatingParts.size()]; + int seats = def.seatAndRackCount(); + seatYaw = new float[seats]; + prevSeatYaw = new float[seats]; + seatPitch = new float[seats]; + prevSeatPitch = new float[seats]; + for (int i = def.seatCount(); i < seats; i++) { + RackDefinition rack = def.rack(i); + seatYaw[i] = prevSeatYaw[i] = rack.yaw(); + seatPitch[i] = prevSeatPitch[i] = rack.pitch(); + } + } + + void tick(AircraftEntity vehicle, AircraftDefinition def) { + AircraftEntity.ControlInput input = vehicle.input(); + float visibleThrottle = vehicle.visibleThrottle(); + float reverse = vehicle.visibleReverse(); + prevThrottle = throttle; + throttle = visibleThrottle; + + tickWheels(def, input, visibleThrottle, reverse); + tickTracks(def, input, visibleThrottle, reverse); + tickRotatingParts(def, vehicle.getControllingPassenger() != null); + tickCrewAim(vehicle, def); + } + + private void tickWheels(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + prevWheelRoll = wheelRoll; + prevSteer = steer; + if (def.enableBack && reverse > 0.01F && throttle <= 0.0) { + throttle = -reverse * 15.0F; + } + if (input.left && !input.right) { + steer = Math.min(steer + 0.1F, 1.0F); + } else if (input.right && !input.left) { + steer = Math.max(steer - 0.1F, -1.0F); + } else { + steer *= 0.9F; + } + wheelRoll += (float) (throttle * def.partWheelRot); + if (wheelRoll >= 360.0F) { + wheelRoll -= 360.0F; + prevWheelRoll -= 360.0F; + } else if (wheelRoll < 0.0F) { + wheelRoll += 360.0F; + prevWheelRoll += 360.0F; + } + } + + private void tickTracks(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + System.arraycopy(roller, 0, prevRoller, 0, 2); + System.arraycopy(track, 0, prevTrack, 0, 2); + double pivot = def.pivotTurnThrottle <= 0.0F ? 1.0 : def.pivotTurnThrottle * 0.1F; + boolean left = input.left; + boolean right = input.right; + int dir = 1; + if (def.enableBack && reverse > 0.0F && throttle <= 0.0) { + throttle = -reverse * 5.0F; + // Reversing swaps which track leads a turn. + if (left != right) { + boolean t = left; + left = right; + right = t; + dir = -1; + } + } + if (left && !right) { + double t = 0.2 * dir; + trackSpeed[0] += (float) t; + trackSpeed[1] -= (float) (pivot * t); + } else if (right && !left) { + double t = 0.2 * dir; + trackSpeed[0] -= (float) (pivot * t); + trackSpeed[1] += (float) t; + } else { + float t = (float) Mth.clamp(throttle, -0.2, 0.2); + trackSpeed[0] += t; + trackSpeed[1] += t; + } + + for (int i = 0; i < 2; i++) { + trackSpeed[i] = Mth.clamp(trackSpeed[i], -0.72F, 0.72F); + roller[i] += trackSpeed[i] * def.trackRollerRot; + if (roller[i] >= 360.0F) { + roller[i] -= 360.0F; + prevRoller[i] -= 360.0F; + } else if (roller[i] < 0.0F) { + roller[i] += 360.0F; + prevRoller[i] += 360.0F; + } + track[i] -= trackSpeed[i]; + while (track[i] >= 1.0F) { + track[i]--; + prevTrack[i]--; + } + while (track[i] < 0.0F) { + track[i]++; + } + while (prevTrack[i] < 0.0F) { + prevTrack[i]++; + } + trackSpeed[i] *= 0.75F; + } + } + + private void tickRotatingParts(AircraftDefinition def, boolean piloted) { + for (int i = 0; i < rotatingParts.length && i < def.rotatingParts.size(); i++) { + RotatingPart part = def.rotatingParts.get(i); + prevRotatingParts[i] = rotatingParts[i]; + if (part.always() || piloted) { + float r = rotatingParts[i] + part.speed(); + if (r < 0.0F) { + r += 360.0F; + prevRotatingParts[i] += 360.0F; + } else if (r >= 360.0F) { + r -= 360.0F; + prevRotatingParts[i] -= 360.0F; + } + rotatingParts[i] = r; + } + } + } + + private void tickCrewAim(AircraftEntity vehicle, AircraftDefinition def) { + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + float hullRoll = vehicle.roll(); + for (int i = 0; i < seatYaw.length && i < def.seatCount(); i++) { + prevSeatYaw[i] = seatYaw[i]; + prevSeatPitch[i] = seatPitch[i]; + Entity crew = vehicle.occupant(i); + if (crew == null) { + continue; + } + float yaw = Mth.wrapDegrees(crew.getYRot() - hullYaw); + double rad = Math.toRadians(yaw); + // The hull tilt along the crew member's view direction, as in the original tank code. + float tilt = (float) (hullPitch * Math.cos(rad) + hullRoll * Math.sin(rad)); + float pitch = crew.getXRot() - tilt; + // Keep the previous value on the same side of the seam so interpolation never spins. + prevSeatYaw[i] = yaw + Mth.wrapDegrees(prevSeatYaw[i] - yaw); + seatYaw[i] = yaw; + seatPitch[i] = pitch; + } + } + + public float seatYaw(int seat, float partialTicks) { + if (seat < 0 || seat >= seatYaw.length) { + return 0.0F; + } + return prevSeatYaw[seat] + (seatYaw[seat] - prevSeatYaw[seat]) * partialTicks; + } + + public float seatPitch(int seat, float partialTicks) { + if (seat < 0 || seat >= seatPitch.length) { + return 0.0F; + } + return prevSeatPitch[seat] + (seatPitch[seat] - prevSeatPitch[seat]) * partialTicks; + } +} From ce2372ee2f4dba58523275583d9c2b53e214c4eb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:36 +0800 Subject: [PATCH 0969/1461] Add entity/ChainEntity.java to the 1.21.5 shared sources --- .../mcheli/entity/ChainEntity.java | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java new file mode 100644 index 00000000..4d18cd9a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java @@ -0,0 +1,192 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * A tow chain, {@code MCH_EntityChain}: it hangs above the vehicle that pulls and drags the towed + * one along, letting it drift up to the chain's length away and never more than two blocks past + * that. It goes when either end does. + */ +public class ChainEntity extends Entity { + public static final int MIN_LENGTH = 3; + public static final int MAX_LENGTH = 15; + /** Ticks a chain loaded from disk waits for both ends to load before it gives up. */ + private static final int SEARCH_TICKS = 50; + + private static final EntityDataAccessor DATA_TOW = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TOWED = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + + private @Nullable UUID towId; + private @Nullable UUID towedId; + private @Nullable Entity tow; + private @Nullable Entity towed; + private int length = 4; + private int searchTicks = SEARCH_TICKS; + + public ChainEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + /** Chains {@code towed} to {@code tow}, at the distance they are apart now. Server only. */ + public static ChainEntity connect(Level level, Entity towed, Entity tow) { + ChainEntity chain = new ChainEntity(ModEntities.CHAIN.get(), level); + chain.setPos(tow.position().add(towed.position()).scale(0.5)); + chain.length = Mth.clamp((int) towed.distanceTo(tow), MIN_LENGTH, MAX_LENGTH); + chain.attach(towed, tow); + level.addFreshEntity(chain); + ModSounds.playAt(chain.tow, "chain_ct", 1.0F, 1.0F); + return chain; + } + + /** Whether {@code entity} can pull the other end: only vehicles can. */ + public static boolean canTow(Entity entity) { + return entity instanceof AircraftEntity; + } + + /** + * Only a vehicle pulls. Hooked the other way round, a container would hold a helicopter down, + * so the ends swap whichever order they were clicked in (and chains saved that way are fixed + * when they load). + */ + private void attach(Entity towed, Entity tow) { + if (!canTow(tow) && canTow(towed)) { + Entity swap = tow; + tow = towed; + towed = swap; + } + this.towed = towed; + this.tow = tow; + this.towedId = towed.getUUID(); + this.towId = tow.getUUID(); + entityData.set(DATA_TOWED, towed.getId()); + entityData.set(DATA_TOW, tow.getId()); + searchTicks = 0; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_TOW, -1); + builder.define(DATA_TOWED, -1); + } + + /** The end that pulls; on a client, as far as it has loaded. */ + public @Nullable Entity tow() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOW)) : tow; + } + + /** The end that is pulled. */ + public @Nullable Entity towed() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOWED)) : towed; + } + + /** Whether {@code entity} is either end of this chain. */ + public boolean holds(Entity entity) { + return entity == tow() || entity == towed(); + } + + /** Takes the chain off, with the sound of it coming loose. Server only. */ + public void disconnect() { + ModSounds.playAt(this, "chain_ct", 1.0F, 1.0F); + discard(); + } + + /** The chain on {@code entity}, at either end, if there is one within reach. */ + public static @Nullable ChainEntity on(Entity entity) { + for (ChainEntity chain : entity.level().getEntitiesOfClass(ChainEntity.class, entity.getBoundingBox().inflate(25.0))) { + if (chain.holds(entity)) { + return chain; + } + } + return null; + } + + @Override + public void tick() { + super.tick(); + if (!(level() instanceof ServerLevel server)) { + return; + } + if (searchTicks > 0) { + searchTicks--; + Entity foundTowed = towedId != null ? server.getEntity(towedId) : null; + Entity foundTow = towId != null ? server.getEntity(towId) : null; + if (foundTowed != null && foundTow != null) { + attach(foundTowed, foundTow); + } + return; + } + if (tow == null || towed == null || tow.isRemoved() || towed.isRemoved() || tow == towed) { + disconnect(); + return; + } + towed.resetFallDistance(); + setPos(tow.getX(), tow.getY() + 2.0, tow.getZ()); + pull(tow, towed); + } + + /** {@code updateTowingEntityPosRot}: turns the towed end to face along the chain and drags it in. */ + private void pull(Entity tow, Entity towed) { + Vec3 d = towed.position().subtract(tow.position()); + double dist = d.length(); + if (dist <= length) { + return; + } + towed.setYRot((float) Math.toDegrees(Math.atan2(d.z, d.x)) + 90.0F); + double slack = dist - length; + towed.setDeltaMovement(towed.getDeltaMovement().subtract(d.scale(0.001 / slack))); + double max = length + 2.0; + if (dist > max) { + Vec3 at = tow.position().add(d.scale(max / dist)); + towed.setPos(at.x, at.y, at.z); + } + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + towId = input.read("Tow", UUIDUtil.CODEC).orElse(null); + towedId = input.read("Towed", UUIDUtil.CODEC).orElse(null); + length = Mth.clamp(input.getIntOr("Length", 4), MIN_LENGTH, MAX_LENGTH); + searchTicks = SEARCH_TICKS; + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + if (towId != null && towedId != null) { + output.store("Tow", UUIDUtil.CODEC, towId); + output.store("Towed", UUIDUtil.CODEC, towedId); + } + output.putInt("Length", length); + } +} From b3fcca8ff1f66c00bed26043b5da6b371309f56f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:36 +0800 Subject: [PATCH 0970/1461] Add entity/ClientAccess.java to the 1.21.5 shared sources --- .../mcheli/entity/ClientAccess.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java new file mode 100644 index 00000000..28f14851 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * The few client facts common code needs, such as who the local player is. The client installs + * the real supplier at startup; a dedicated server keeps the empty default and never loads a + * client class through here. + */ +public final class ClientAccess { + private static Supplier<@Nullable Entity> localPlayer = () -> null; + private static PackReceiver packs = new PackReceiver() { + @Override + public void onPackList(PackListPacket packet) { + } + + @Override + public void onPackData(PackDataPacket packet) { + } + + @Override + public void onPackSound(PackSoundPacket packet) { + } + }; + + private ClientAccess() { + } + + public static void install(Supplier<@Nullable Entity> player, PackReceiver receiver) { + localPlayer = player; + packs = receiver; + } + + public static @Nullable Entity localPlayer() { + return localPlayer.get(); + } + + /** Where the server's content pack packets go. Called on the client's main thread. */ + public static PackReceiver packs() { + return packs; + } + + /** The client half of the content pack sync. */ + public interface PackReceiver { + void onPackList(PackListPacket packet); + + void onPackData(PackDataPacket packet); + + void onPackSound(PackSoundPacket packet); + } +} From 52eb40c2f39964166af2343776b9df6a22b22a65 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:37 +0800 Subject: [PATCH 0971/1461] Save the container's items with the registries on 1.21.5 --- .../mcheli/entity/ContainerEntity.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java new file mode 100644 index 00000000..9310e332 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java @@ -0,0 +1,151 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A cargo container, {@code MCH_EntityContainer}: a crate with a large chest's worth of room that + * sits on the ground, floats, can be pushed around, and rides the racks of the vehicles that carry + * {@code container}. Dropped from a rack high enough it comes down under a parachute. + */ +public class ContainerEntity extends Entity { + /** What racks call it in their {@code AddRack} lines. */ + public static final String CARGO_NAME = "container"; + private static final int SLOTS = 54; + /** Punches add up; the original broke it past 40 with 20 per point of damage. */ + private static final int BREAK_DAMAGE = 40; + + private final InterpolationHandler interpolation = new VehicleInterpolation(this); + private final SimpleContainer items = new SimpleContainer(SLOTS); + private int damageTaken; + + public ContainerEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide() || isPassenger()) { + return; + } + if (damageTaken > 0) { + damageTaken--; + } + Vec3 m = getDeltaMovement(); + double my = m.y; + if (isInWater() || isInLava()) { + // Bobs up to the surface rather than sinking, as the original's boat physics did. + my = (my < 0.0 ? my / 2.0 : my) + (isEyeInFluid(FluidTags.WATER) ? 0.04 : 0.007); + } else { + my -= 0.04; + } + double mx = m.x; + double mz = m.z; + if (onGround()) { + mx *= 0.9; + mz *= 0.9; + } + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + if (player.getItemInHand(hand).getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (player.isSecondaryUseActive()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer serverPlayer) { + serverPlayer.openMenu(new SimpleMenuProvider((id, inventory, p) -> ChestMenu.sixRows(id, inventory, items), + Component.translatable("entity.mcheli.container"))); + } + return InteractionResult.SUCCESS; + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved() || !(source.getEntity() instanceof Player player)) { + // Only players take it apart; explosions and stray rounds leave the cargo alone. + return false; + } + ModSounds.playAt(this, "hit", 1.0F, 1.3F); + damageTaken += (int) (damage * 20.0F); + boolean creative = player.getAbilities().instabuild; + if (creative || damageTaken > BREAK_DAMAGE) { + if (!creative) { + spawnAtLocation(level, ModItems.CONTAINER.get()); + } + Containers.dropContents(level, this, items); + discard(); + } + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean isPushable() { + return !isPassenger(); + } + + @Override + public boolean canBeCollidedWith() { + return !isPassenger(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.CONTAINER.get()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + items.clearContent(); + ContainerHelper.loadAllItems(input, items.getItems(), registryAccess()); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + ContainerHelper.saveAllItems(output, items.getItems(), false, registryAccess()); + } +} From 4f65c7ada1cfa0be40876be05fcf9a1cb57468c0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:37 +0800 Subject: [PATCH 0972/1461] Add entity/EngineSoundSource.java to the 1.21.5 shared sources --- .../mcheli/entity/EngineSoundSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java new file mode 100644 index 00000000..6b25a388 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.entity; + +/** + * A vehicle with an engine loop. The client sound player polls these values every tick; the + * definition's {@code SoundVolume} and {@code SoundPitch} are applied on top. + */ +public interface EngineSoundSource { + /** Loudness before the definition's multiplier. Zero silences the loop. */ + float engineVolume(); + + /** Pitch before the definition's multiplier. */ + float enginePitch(); +} From 2379185a545d4ed270821d8e607f35e3351e795c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:37 +0800 Subject: [PATCH 0973/1461] Add entity/GltdEntity.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/entity/GltdEntity.java | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java new file mode 100644 index 00000000..d3e058a8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java @@ -0,0 +1,212 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.weapon.CasStrike; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The ground laser target designator, {@code MCH_EntityGLTD}: a tripod a player sits behind. Its + * sight looks up to 70 degrees either side of where it was set up, zooms and has night vision, + * and marking a spot calls in an A-10 for three gun runs over it. + */ +public class GltdEntity extends Entity { + /** How far the sight turns from the tripod's heading, either way, and up or down. */ + public static final float VIEW_LIMIT = 70.0F; + /** The CAS weapon the A-10s fire; the original hard coded it too. */ + private static final String WEAPON = "a10gau8"; + /** Added to the weapon's own {@code Delay} between two calls. */ + private static final int EXTRA_WAIT = 150; + private static final double SIGHT_RANGE = 300.0; + + private static final EntityDataAccessor DATA_WAIT = SynchedEntityData.defineId(GltdEntity.class, EntityDataSerializers.INT); + + private final InterpolationHandler interpolation = new VehicleInterpolation(this); + private final CasStrike strike = new CasStrike(WEAPON); + private boolean nightVision; + + public GltdEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WAIT, 0); + } + + /** Ticks until the next A-10 can be called; the sight shows it. */ + public int waitTicks() { + return entityData.get(DATA_WAIT); + } + + /** The eye of the sight, a little in front of and above the tripod. */ + public Vec3 sightPosition(float partialTicks) { + float yaw = getYRot(partialTicks) * Mth.DEG_TO_RAD; + Vec3 pos = getPosition(partialTicks); + return new Vec3(pos.x - Mth.sin(yaw) * 0.6, pos.y + 0.7, pos.z + Mth.cos(yaw) * 0.6); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.04, 0.0); + if (onGround()) { + m = m.scale(0.5); + } + move(MoverType.SELF, m); + setDeltaMovement(m.x * 0.99, m.y * 0.95, m.z * 0.99); + + if (waitTicks() > 0) { + entityData.set(DATA_WAIT, waitTicks() - 1); + } + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (def != null && level() instanceof ServerLevel server) { + strike.tick(server, def); + } + Entity viewer = getFirstPassenger(); + if (viewer == null) { + nightVision = false; + } else if (nightVision) { + NightVision.topUp(viewer); + } + } + + /** + * {@code useCurrentWeapon}: marks the block {@code player} looks at through the sight and calls + * the A-10 on it. Server only. + * + * @return whether the strike was called; the sight may be waiting, or see nothing in range + */ + public boolean callStrike(ServerPlayer player) { + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (getFirstPassenger() != player || waitTicks() > 0 || def == null || !(level() instanceof ServerLevel server)) { + return false; + } + Vec3 from = sightPosition(1.0F); + Vec3 to = from.add(player.getLookAngle().scale(SIGHT_RANGE)); + BlockHitResult hit = server.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + strike.start(hit.getLocation(), player.getYRot(), this, player); + entityData.set(DATA_WAIT, def.delay + EXTRA_WAIT); + ModSounds.playAt(server, getX(), getY(), getZ(), "gltd", 0.5F, 1.0F); + if (def.sound != null) { + ModSounds.playTo(player, def.sound, 1.0F, 1.0F); + } + return true; + } + + public void setNightVision(Player player, boolean on) { + if (getFirstPassenger() != player) { + return; + } + nightVision = on; + if (!on) { + NightVision.end(player); + } + } + + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + if (player.isSecondaryUseActive() || isVehicle()) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + player.setYRot(getYRot()); + player.setXRot(0.0F); + player.startRiding(this); + } + return InteractionResult.SUCCESS; + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (!level().isClientSide() && nightVision) { + NightVision.end(passenger); + nightVision = false; + } + } + + /** The viewer sits behind the tripod. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, -0.5).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + /** The original clamped the viewer's head the same way every tick. */ + @Override + public void onPassengerTurned(Entity passenger) { + float yaw = getYRot() + Mth.clamp(Mth.wrapDegrees(passenger.getYRot() - getYRot()), -VIEW_LIMIT, VIEW_LIMIT); + passenger.setYRot(yaw); + passenger.setYHeadRot(yaw); + passenger.setXRot(Mth.clamp(passenger.getXRot(), -VIEW_LIMIT, VIEW_LIMIT)); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved()) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + if (!creative) { + spawnAtLocation(level, ModItems.GLTD.get()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), "hit", 1.0F, 1.0F); + discard(); + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.GLTD.get()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + } +} From a54fcf1c1c087f8ce8561a74683610629b32b801 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:38 +0800 Subject: [PATCH 0974/1461] Add entity/GroundVehicleEntity.java to the 1.21.5 shared sources --- .../mcheli/entity/GroundVehicleEntity.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java new file mode 100644 index 00000000..30290141 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java @@ -0,0 +1,130 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Static guns and launchers, ported from {@code MCH_EntityVehicle}. + * + *

The mount itself does not turn; the gunner looks around and the {@code AddPart} tree turns + * with the view. {@code CanMove} lets W/S push the mount and A/D turn it, for the few that set it. + */ +public class GroundVehicleEntity extends AircraftEntity implements EngineSoundSource { + private static final double MIN_SPEED = 0.07; + private double currentSpeed = MIN_SPEED; + + public GroundVehicleEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable VehicleDefinition lookup(String name) { + return MCHeli.content().vehicle(name); + } + + @Override + public @Nullable VehicleDefinition definition() { + return (VehicleDefinition) super.definition(); + } + + /** + * {@code MCH_EntityVehicle.useCurrentWeapon}: the mount stays put while its {@code AddPart} + * tree turns with the gunner's view, so a weapon fires where the gunner looks. Only weapons + * with both yaw limits set aim like turrets instead. The original checked the first + * {@code AddWeapon} line of that weapon, and so does this. + */ + @Override + public boolean aimsWithUser(WeaponSet set) { + WeaponMount mount = set.mounts().getFirst(); + return mount.minYaw() == 0.0F || mount.maxYaw() == 0.0F; + } + + @Override + protected void simulate() { + VehicleDefinition def = definition(); + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + + if (getControllingPassenger() == null) { + throttle = Math.max(throttle - 0.00125, 0.0); + } else if (def.canMove) { + double x = 0.0; + double z = 0.0; + double yaw = Math.toRadians(getYRot()); + if (input.throttleUp) { + x += Math.sin(yaw); + z += Math.cos(yaw); + } + if (input.throttleDown) { + x -= Math.sin(yaw); + z -= Math.cos(yaw); + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + double d = Math.sqrt(x * x + z * z); + if (d > 1.0E-6) { + mx -= x / d * 0.03; + mz += z / d * 0.03; + } + } + if (isInWater()) { + setXRot(getXRot() * 0.9F); + } + + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public float engineVolume() { + return visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return visibleThrottle() * 0.5F; + } +} From d17ec5446bda068925a4c95d6992685136bdffc7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:38 +0800 Subject: [PATCH 0975/1461] Add entity/HelicopterEntity.java to the 1.21.5 shared sources --- .../mcheli/entity/HelicopterEntity.java | 552 ++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java new file mode 100644 index 00000000..0f246c96 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java @@ -0,0 +1,552 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A helicopter, ported from {@code MCH_EntityHeli}. + * + *

The flight model is the original one: throttle produces lift that weakens as the aircraft + * tilts, and pitching the nose down converts throttle into forward speed through a cubic curve. + * Hover mode holds full throttle, levels the aircraft and lets W/A/S/D slide it around instead. + * + *

Two deliberate differences from 1.7.10: + *

    + *
  • The original ran roll damping once per rendered frame, so it depended on frame rate. It + * now runs once per tick with constants tuned to match the feel at 60 FPS.
  • + *
  • Blades fold and unfold one after another at the same pace on both sides, from the + * synced fold state, instead of each client stepping its own copy.
  • + *
+ */ +public class HelicopterEntity extends AircraftEntity implements EngineSoundSource { + /** + * Rotation per mouse count. The original used {@code 0.06 * partialTicks} with the partial tick + * low-pass filtered to roughly 0.5. + */ + private static final float LOOK_SCALE = 0.03F; + /** Vanilla turns the view 0.15 degrees per mouse count, so this recovers the raw count. */ + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final float ROLL_DAMPING = 0.06F; + private static final float ROLL_DAMPING_HOVER = 0.1F; + private static final float ROLL_RATE = 1.8F; + private static final double MIN_SPEED = 0.07; + + private static final EntityDataAccessor DATA_FOLD = SynchedEntityData.defineId(HelicopterEntity.class, EntityDataSerializers.BYTE); + // DATA_FOLD values, as the original numbered them. + private static final byte FOLDED = 0; + private static final byte FOLDING = 1; + private static final byte UNFOLDED = 2; + private static final byte UNFOLDING = 3; + /** {@code MCH_Blade}: degrees a blade swings per tick while folding. */ + private static final float FOLD_SPEED = 2.0F; + private static final int FOLD_COOLDOWN = 40; + + private double currentSpeed = MIN_SPEED; + private float rotorAngle; + public float rotorAngleO; + /** Per rotor, per blade: where each blade points while they fold, and whether that rotor is folded. */ + private float[][] blades = new float[0][]; + private float[][] bladesO = new float[0][]; + private boolean[] rotorFolded = new boolean[0]; + private int foldCooldown; + private byte lastFold = -1; + + public HelicopterEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable HelicopterDefinition lookup(String name) { + return MCHeli.content().helicopter(name); + } + + @Override + public @Nullable HelicopterDefinition definition() { + return (HelicopterDefinition) super.definition(); + } + + @Override + public boolean steersWithMouse() { + return true; + } + + @Override + public float engineVolume() { + HelicopterDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return 0.2F + visibleThrottle() * 0.2F; + } + + /** Hovering or slow helicopters drop their gear sooner than ones passing over at speed. */ + @Override + protected float autoLowerGearThrottle() { + HelicopterDefinition def = definition(); + double dx = getX() - xo; + double dy = getY() - yo; + double dz = getZ() - zo; + float slow = def != null ? def.speed / 3.5F : 0.0F; + return dx * dx + dy * dy + dz * dz <= slow ? 0.8F : 0.3F; + } + + public float rotorAngle(float partialTicks) { + float prev = rotorAngleO; + if (rotorAngle - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - rotorAngle < -180.0F) { + prev += 360.0F; + } + return prev + (rotorAngle - prev) * partialTicks; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_FOLD, UNFOLDED); + } + + // Blade folding + + private byte foldState() { + return entityData.get(DATA_FOLD); + } + + private boolean hasFoldingRotors() { + HelicopterDefinition def = definition(); + return def != null && def.foldableBlades && !def.rotors.isEmpty(); + } + + /** {@code isFoldBlades}: folded, or on the way. */ + public boolean isFoldedBlades() { + return hasFoldingRotors() && foldState() == FOLDED; + } + + /** {@code canUseBlades}: the blades are out and none is still swinging into place. */ + public boolean canUseBlades() { + if (foldState() != UNFOLDED) { + return false; + } + for (int r = 0; r < rotorFolded.length; r++) { + if (rotorFolded[r]) { + return false; + } + } + return true; + } + + /** {@code canSwitchFoldBlades}: with the engine stopped and the last switch finished. */ + private boolean canSwitchFoldBlades() { + byte state = foldState(); + return hasFoldingRotors() && visibleThrottle() <= 0.01F && foldCooldown == 0 && (state == UNFOLDED || state == FOLDED); + } + + /** The fold key folds the blades of helicopters that can, and works the hatches otherwise. */ + @Override + public void toggleFold() { + if (canSwitchFoldBlades()) { + entityData.set(DATA_FOLD, foldState() == FOLDED ? UNFOLDING : FOLDING); + foldCooldown = FOLD_COOLDOWN; + return; + } + super.toggleFold(); + } + + /** + * {@code onUpdate_Rotor} with {@code MCH_Rotor.update} and {@code MCH_Blade}: spinning blades + * follow the rotor; folding ones swing one by one onto the tail, each a few degrees further + * than the one before, and swing out again until they meet the spinning rotor's angle. + */ + private void tickBlades(HelicopterDefinition def) { + int rotors = def.rotors.size(); + if (blades.length != rotors) { + blades = new float[rotors][]; + bladesO = new float[rotors][]; + rotorFolded = new boolean[rotors]; + for (int r = 0; r < rotors; r++) { + int count = Math.max(def.rotors.get(r).blades(), 1); + blades[r] = new float[count]; + bladesO[r] = new float[count]; + } + if (foldState() == FOLDED || foldState() == FOLDING) { + forceFold(def); + } + } + byte state = foldState(); + if (state != lastFold) { + lastFold = state; + foldCooldown = FOLD_COOLDOWN; + } else if (foldCooldown > 0) { + foldCooldown--; + } + boolean target = state == FOLDING || state == FOLDED; + boolean settled = true; + for (int r = 0; r < rotors; r++) { + RotorDefinition rotor = def.rotors.get(r); + boolean wanted = target && rotor.foldable(); + boolean done = true; + for (int b = 0; b < blades[r].length; b++) { + float base = b * Math.max(rotor.bladeStep(), 1); + float fold = 5.0F + b * 3.0F; + bladesO[r][b] = blades[r][b]; + if (!rotorFolded[r]) { + if (!wanted) { + blades[r][b] = wrap(rotorAngle + base); + done = false; + } else if (!foldBlade(r, b, fold)) { + done = false; + } + } else if (wanted) { + done = false; + } else if (!unfoldBlade(r, b, base)) { + done = false; + } + } + if (done) { + rotorFolded[r] = wanted; + } + if (rotorFolded[r] != wanted) { + settled = false; + } + } + if (settled && !level().isClientSide()) { + if (state == FOLDING) { + entityData.set(DATA_FOLD, FOLDED); + } else if (state == UNFOLDING) { + entityData.set(DATA_FOLD, UNFOLDED); + } + } + } + + /** {@code MCH_Blade.folding}: towards the nearer side of the tail. */ + private boolean foldBlade(int r, int b, float fold) { + float angle = blades[r][b]; + if (angle > fold && angle < 360.0F - fold) { + blades[r][b] = wrap(angle < 180.0F ? angle - FOLD_SPEED : angle + FOLD_SPEED); + return false; + } + return true; + } + + /** {@code MCH_Blade.unfolding}: back round until the blade meets its place on the turning rotor. */ + private boolean unfoldBlade(int r, int b, float base) { + float target = wrap(rotorAngle + base); + float before = blades[r][b]; + if (target <= 180.0F) { + blades[r][b] = wrap(before + FOLD_SPEED); + if (blades[r][b] >= target && before <= target) { + blades[r][b] = target; + return true; + } + } else { + blades[r][b] = wrap(before - FOLD_SPEED); + if (blades[r][b] <= target && before >= target) { + blades[r][b] = target; + return true; + } + } + return false; + } + + /** {@code forceFold}: a helicopter loaded with its blades folded starts with them folded. */ + private void forceFold(HelicopterDefinition def) { + for (int r = 0; r < blades.length; r++) { + if (!def.rotors.get(r).foldable()) { + continue; + } + for (int b = 0; b < blades[r].length; b++) { + float fold = 5.0F + b * 3.0F; + float angle = wrap(rotorAngle + b * Math.max(def.rotors.get(r).bladeStep(), 1)); + if (angle > fold && angle < 360.0F - fold) { + angle = angle < 180.0F ? fold : 360.0F - fold; + } + blades[r][b] = angle; + bladesO[r][b] = angle; + } + rotorFolded[r] = true; + } + } + + private static float wrap(float degrees) { + float d = degrees % 360.0F; + return d < 0.0F ? d + 360.0F : d; + } + + /** Where blade {@code blade} of rotor {@code rotor} points, for drawing. */ + public float bladeAngle(int rotor, int blade, int step, float partialTicks) { + if (rotor >= blades.length || blade >= blades[rotor].length) { + return rotorAngle(partialTicks) + blade * step; + } + float prev = bladesO[rotor][blade]; + float now = blades[rotor][blade]; + if (now - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - now < -180.0F) { + prev += 360.0F; + } + return prev + (now - prev) * partialTicks; + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + super.addAdditionalSaveData(output); + output.putBoolean("FoldBlade", foldState() == FOLDED || foldState() == FOLDING); + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + super.readAdditionalSaveData(input); + entityData.set(DATA_FOLD, input.getBooleanOr("FoldBlade", false) ? FOLDED : UNFOLDED); + } + + /** Pilot toggles hover mode. Only allowed while fairly level and not in gunner mode, as in the original. */ + public void toggleHovering() { + if (isHoveringMode()) { + setHovering(false); + } else if (!isGunnerMode() && isLevelEnough() && canUseBlades()) { + setHovering(true); + } + } + + private boolean isLevelEnough() { + return Math.abs(Mth.wrapDegrees(roll())) < 40.0F && Math.abs(Mth.wrapDegrees(getXRot())) < 40.0F; + } + + @Override + public boolean canSwitchGunnerMode() { + return super.canSwitchGunnerMode() && isLevelEnough() && canUseBlades(); + } + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + HelicopterDefinition def = definition(); + if (def == null) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + + float yaw = 0.0F; + float pitch = 0.0F; + float rollDelta = 0.0F; + if (!isNearGround(2)) { + yaw = clamp(x, 40.0F * def.mobilityYaw) * LOOK_SCALE; + } + // Standing on its skids or wheels a helicopter stays level; the mouse only tilts it in the air. + if (!isHovering() && !onGround()) { + pitch = clamp(y, 40.0F * def.mobilityPitch) * LOOK_SCALE; + rollDelta = clamp(x, 40.0F * def.mobilityRoll) * LOOK_SCALE; + } + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + // Keep yaw continuous so the pilot's own yaw never jumps across the +-180 seam. + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + @Override + protected void simulate() { + HelicopterDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + + updateThrottle(def, piloted); + updateAttitude(def); + updateMotion(def); + } + + /** {@code onUpdate_Control} and the hover/non-hover branches. */ + private void updateThrottle(HelicopterDefinition def, boolean piloted) { + if (piloted) { + Vec3 motion = getDeltaMovement(); + if (!canUseBlades()) { + // Folded blades give no lift; on the ground A/D taxi the helicopter round slowly. + throttle = Math.max(0.0, throttle - 0.02); + if (onGround() && isFoldedBlades() && !isDestroyed()) { + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + } + return; + } + if (!isHovering()) { + if (input.throttleUp) { + throttle = Math.min(1.0, throttle + 0.02 * def.throttleUpDown); + } else if (input.throttleDown) { + throttle = Math.max(0.0, throttle - (1.0 / 70.0) * def.throttleUpDown); + } + motion = strafe(motion, false, input.left, input.right, 0.02 * def.speed); + } else { + throttle = Math.min(1.0, throttle + 1.0 / 30.0); + motion = strafe(motion, input.throttleUp, input.left, input.right, 0.01 * def.speed); + if (input.throttleDown) { + motion = push(motion, getYRot() - 180.0F, 0.01 * def.speed); + } + } + setDeltaMovement(motion); + } else { + throttle = Math.max(0.0, throttle - 0.00125); + if (isHoveringMode() && !level().isClientSide()) { + setHovering(false); + } + } + } + + private Vec3 strafe(Vec3 motion, boolean forward, boolean left, boolean right, double amount) { + if (forward) { + motion = push(motion, getYRot(), amount); + } + if (left && !right) { + motion = push(motion, getYRot() - 90.0F, amount); + } else if (right && !left) { + motion = push(motion, getYRot() + 90.0F, amount); + } + return motion; + } + + /** Adds {@code amount} of horizontal motion towards {@code yaw}, Minecraft style (0 = south). */ + private static Vec3 push(Vec3 motion, float yaw, double amount) { + double rad = Math.toRadians(yaw); + return motion.add(-Math.sin(rad) * amount, 0.0, Math.cos(rad) * amount); + } + + /** Per-tick version of {@code onUpdateAngles}: roll levels out, A/D bank in the air. */ + private void updateAttitude(HelicopterDefinition def) { + float damping = 1.0F - (isHovering() ? ROLL_DAMPING_HOVER : ROLL_DAMPING); + if (Math.abs(roll) > 0.1F && Math.abs(roll) < 65.0F) { + roll *= damping; + } + if (getControllingPassenger() != null && !isNearGround(3)) { + if (input.left && !input.right) { + roll -= ROLL_RATE; + } else if (input.right && !input.left) { + roll += ROLL_RATE; + } + } + if (isHovering() && Math.abs(getXRot()) < 70.0F) { + setXRot(getXRot() * 0.95F); + } + if (def.limitRotation) { + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + /** {@code onUpdate_Server}: gravity, lift, forward thrust, speed limit and ground friction. */ + private void updateMotion(HelicopterDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + float groundPitch = def.onGroundPitch; + + if (!isHovering()) { + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + double yawRad = Math.toRadians(getYRot()); + float pitch = getXRot(); + if (isNearGround(3)) { + pitch -= groundPitch; + } + double thrust = 0.1 * currentSpeed * (pitch * pitch * pitch / 30000.0F) * throttle; + mx += Math.sin(yawRad) * -thrust; + mz += Math.cos(yawRad) * thrust; + + double tilt = (Math.abs(getXRot()) + Math.abs(roll)) * 0.6; + double lift = tilt <= 50.0 ? 1.0 - tilt / 50.0 : 0.0; + my += (lift * 0.025 + 0.03) * throttle; + } else { + levelOut(groundPitch); + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my *= 0.7; + } + my += 0.007; + } + } + } else { + if (random.nextInt(50) == 0) { + mx += (random.nextDouble() - 0.5) / 30.0; + } + if (random.nextInt(50) == 0) { + my += (random.nextDouble() - 0.5) / 50.0; + } + if (random.nextInt(50) == 0) { + mz += (random.nextDouble() - 0.5) / 30.0; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + double limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + levelOut(groundPitch); + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + private void levelOut(float groundPitch) { + if (Math.abs(getXRot()) < 40.0F) { + setXRot((getXRot() - groundPitch) * 0.9F + groundPitch); + } + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + } + + @Override + protected void tickAnimation() { + HelicopterDefinition def = definition(); + rotorAngleO = rotorAngle; + float spool = 1.0F - visibleThrottle(); + rotorAngle = (rotorAngle + (1.0F - spool * spool * spool) * def.rotorSpeed) % 360.0F; + tickBlades(def); + } +} From dc216ead8fa7ec38bca8997cbebce23cc8f6751b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:39 +0800 Subject: [PATCH 0976/1461] Add entity/NightVision.java to the 1.21.5 shared sources --- .../mcheli/entity/NightVision.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java new file mode 100644 index 00000000..f52c4227 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; + +/** + * Night vision through a sight, {@code MCH_Camera.updateViewer}: a short effect kept topped up while + * the viewer has it switched on, and taken away again when they switch it off or leave. + */ +public final class NightVision { + private static final int TICKS = 300; + + private NightVision() { + } + + /** Called every tick for each viewer. It never runs below ten seconds, where vanilla starts to flicker the view. */ + public static void topUp(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current == null || current.getDuration() < TICKS - 40) { + living.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, TICKS, 0, true, false, false)); + } + } + } + + /** Removes ours, leaving a longer one from a potion alone. */ + public static void end(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current != null && current.getDuration() <= TICKS) { + living.removeEffect(MobEffects.NIGHT_VISION); + } + } + } +} From c46c52b28a11f58e303fc3e44b75219b7ca92d78 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:39 +0800 Subject: [PATCH 0977/1461] Add entity/ParachuteEntity.java to the 1.21.5 shared sources --- .../mcheli/entity/ParachuteEntity.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java new file mode 100644 index 00000000..337f2816 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A parachute with someone hanging under it, {@code MCH_EntityParachute}. It sinks at about two + * blocks a second, the rider steers it with the forward key, and it folds away a moment after it + * touches the ground or the water. + */ +public class ParachuteEntity extends Entity { + /** What the parachute looks like and how it opens, the original's model type. */ + public enum Kind { + /** From the parachute item: open from the start. */ + ITEM, + /** An ejection seat: the rider falls in the seat and the canopy opens once it drops. */ + EJECTION_SEAT, + /** Troops dropped from a rack or a cargo door: open from the start, no seat. */ + DROP + } + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(ParachuteEntity.class, EntityDataSerializers.INT); + private static final double MAX_SPEED = 0.35; + private static final double MIN_STEER = 0.07; + + private final InterpolationHandler interpolation = new VehicleInterpolation(this); + private double steer = MIN_STEER; + private int onGroundCount; + /** Who boards on the next server tick; boarding in the same tick as the spawn can be lost. */ + private @Nullable Entity user; + + public ParachuteEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Opens a parachute above {@code user}, carrying over their speed, and puts them under it. + * Server only. + */ + public static ParachuteEntity open(Level level, Entity user, Kind kind) { + ParachuteEntity parachute = new ParachuteEntity(ModEntities.PARACHUTE.get(), level); + double lift = kind == Kind.ITEM ? 3.5 : 0.0; + parachute.setPos(user.getX(), user.getY() + lift, user.getZ()); + parachute.setYRot(user.getYRot()); + parachute.setDeltaMovement(user.getDeltaMovement()); + parachute.entityData.set(DATA_KIND, kind.ordinal()); + parachute.user = user; + user.resetFallDistance(); + level.addFreshEntity(parachute); + return parachute; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, Kind.ITEM.ordinal()); + } + + public Kind kind() { + Kind[] kinds = Kind.values(); + int i = entityData.get(DATA_KIND); + return i >= 0 && i < kinds.length ? kinds[i] : Kind.ITEM; + } + + /** An ejection seat only opens its canopy once it has started to fall. */ + public boolean isOpen() { + return kind() != Kind.EJECTION_SEAT || getDeltaMovement().y < -0.1; + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + if (level().isClientSide()) { + return; + } + if (user != null) { + if (!user.isPassenger() && user.isAlive()) { + user.startRiding(this, true); + setYRot(user.getYRot()); + } + user = null; + } + Entity rider = getFirstPassenger(); + if (rider == null && tickCount > 20 || isInWater()) { + ejectPassengers(); + discard(); + return; + } + if (rider != null) { + rider.resetFallDistance(); + } + simulate(rider); + } + + /** {@code onUpdateServer}. */ + private void simulate(@Nullable Entity rider) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevSpeed = Math.sqrt(mx * mx + mz * mz); + boolean seatLaunch = kind() == Kind.EJECTION_SEAT && tickCount < 20; + my -= onGround() || seatLaunch ? 0.01 : 0.03; + + if (isOpen()) { + if (rider instanceof ServerPlayer player && player.getLastClientInput().forward()) { + double yaw = Math.toRadians(rider.getYRot()); + mx += -Math.sin(yaw) * steer * 0.05; + mz += Math.cos(yaw) * steer * 0.05; + } + double speed = Math.sqrt(mx * mx + mz * mz); + if (speed > MAX_SPEED) { + mx *= MAX_SPEED / speed; + mz *= MAX_SPEED / speed; + speed = MAX_SPEED; + } + // Holding course builds up speed; letting go settles back to a slow drift. + if (speed > prevSpeed) { + steer = Math.min(steer + (MAX_SPEED - steer) / 35.0, MAX_SPEED); + } else { + steer = Math.max(steer - (steer - MIN_STEER) / 35.0, MIN_STEER); + } + } + + if (onGround() && ++onGroundCount > 5) { + ejectPassengers(); + discard(); + return; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + my *= seatLaunch ? 0.95 : 0.9; + double drag = isOpen() ? 0.95 : 0.99; + setDeltaMovement(mx * drag, my, mz * drag); + if (rider != null) { + setYRot(rider.getYRot()); + } + } + + /** + * The server moves the parachute; clients ease towards each position it sends over three + * ticks, as for the vehicles. Without this a slow descent steps from one update to the next. + */ + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + entityData.set(DATA_KIND, input.getIntOr("Kind", Kind.ITEM.ordinal())); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + output.putInt("Kind", kind().ordinal()); + } +} From f89cda99b329d3f0e644d7fa95ec12e17eb66ade Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:40 +0800 Subject: [PATCH 0978/1461] Add entity/PartState.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/entity/PartState.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/PartState.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/PartState.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/PartState.java new file mode 100644 index 00000000..54755d56 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/PartState.java @@ -0,0 +1,122 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; + +/** + * Open/closed state of a group of moving parts (canopy, landing gear, hatches, wings, VTOL nozzles). + * Ported from {@code MCH_Parts}. + * + *

The target state is one bit of a synched integer, so only the server changes it and every + * side sees the same value. The angle runs from 0 to {@link #max} at {@link #speed} degrees per tick + * and is advanced locally on every side, which keeps the animation smooth without sending angles. + */ +public final class PartState { + private final AircraftEntity owner; + private final int bit; + private final float max; + private final float speed; + private final Sounds sounds; + + private float angle; + private float prevAngle; + + PartState(AircraftEntity owner, int bit, float speed, Sounds sounds) { + this.owner = owner; + this.bit = bit; + this.max = 90.0F; + this.speed = speed; + this.sounds = sounds; + } + + /** Target state: {@code true} means open, folded, retracted or tilted, depending on the part. */ + public boolean status() { + return (owner.partBits() & (1 << bit)) != 0; + } + + /** Fully back in the default position. */ + public boolean isOff() { + return !status() && angle <= 0.02F; + } + + /** Fully in the switched position. */ + public boolean isOn() { + return status() && angle >= max - 0.02F; + } + + public float angle() { + return angle; + } + + public float angle(float partialTicks) { + return prevAngle + (angle - prevAngle) * partialTicks; + } + + /** Progress from 0 to 1. */ + public float factor() { + return angle / max; + } + + public float factor(float partialTicks) { + return angle(partialTicks) / max; + } + + /** Changes the target state. Server only; clients follow through synched data. */ + void set(boolean on, boolean playSound) { + if (owner.level().isClientSide() || status() == on) { + return; + } + owner.setPartBit(bit, on); + if (playSound) { + play(sounds.switching); + play(on ? sounds.startOn : sounds.startOff); + } + tick(); + } + + /** Jumps straight to {@code on} without animating, used for parts that start deployed. */ + void force(boolean on) { + if (!owner.level().isClientSide()) { + owner.setPartBit(bit, on); + } + angle = prevAngle = on ? max : 0.0F; + } + + void tick() { + prevAngle = angle; + if (status()) { + if (angle < max) { + angle = Math.min(angle + speed, max); + if (angle >= max) { + play(sounds.endOn); + } + } + } else if (angle > 0.0F) { + angle = Math.max(angle - speed, 0.0F); + if (angle <= 0.0F) { + play(sounds.endOff); + } + } + } + + private void play(Cue cue) { + if (cue != null && !owner.level().isClientSide()) { + ModSounds.playAt(owner, cue.sound(), 1.0F, cue.pitch()); + } + } + + /** One sound from {@code sounds.json} with its pitch. */ + record Cue(String sound, float pitch) { + } + + /** What plays when the part starts and finishes moving each way; {@code null} entries stay silent. */ + record Sounds(Cue startOn, Cue endOn, Cue startOff, Cue endOff, Cue switching) { + private static final Cue CLICK = new Cue("plane_cc", 1.0F); + private static final Cue CLICK_LOW = new Cue("plane_cc", 0.5F); + + static final Sounds HATCH = new Sounds(null, CLICK, null, CLICK, new Cue("plane_cv", 0.5F)); + static final Sounds CANOPY = new Sounds(null, CLICK, null, CLICK, null); + static final Sounds LANDING_GEAR = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.75F)); + static final Sounds NOZZLE = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.5F)); + static final Sounds WING = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, null); + } +} From 85ecc4fc60dba469867f7c849514e808a0df9a69 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:40 +0800 Subject: [PATCH 0979/1461] Add entity/PlaneEntity.java to the 1.21.5 shared sources --- .../mcheli/entity/PlaneEntity.java | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java new file mode 100644 index 00000000..b80171b8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java @@ -0,0 +1,516 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A fixed wing aircraft, ported from {@code MCP_EntityPlane}. + * + *

Throttle pushes the plane along its nose, tilted ten degrees up, and the vertical part of + * that push is what keeps it in the air: below full throttle it sinks. VTOL capable planes tilt + * their nozzles (and tilt rotors) towards straight down, which turns the push into lift. + * + *

The original applied its attitude rules once per rendered frame with the partial tick + * filtered to about 0.5. Here they run three times per tick with that value, which is what a + * 60 FPS client did, so the handling is the same without depending on the frame rate. + */ +public class PlaneEntity extends AircraftEntity implements EngineSoundSource { + private static final float LOOK_SCALE = 0.03F; + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private @Nullable PartState nozzle; + private @Nullable PartState wing; + + private double currentSpeed = MIN_SPEED; + private float keyRoll; + private float rotorAngle; + public float rotorAngleO; + private float soundLevel; + + public PlaneEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable PlaneDefinition lookup(String name) { + return MCHeli.content().plane(name); + } + + @Override + public @Nullable PlaneDefinition definition() { + return (PlaneDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + PlaneDefinition def = (PlaneDefinition) definition; + nozzle = def.hasNozzleState() ? addPart(PART_NOZZLE, 1.5F, PartState.Sounds.NOZZLE) : null; + wing = def.wings.isEmpty() ? null : addPart(PART_WING, 2.5F, PartState.Sounds.WING); + if (nozzle != null && def.defaultVtol && !nozzle.status()) { + nozzle.force(true); + } + } + + @Override + public boolean steersWithMouse() { + return true; + } + + public @Nullable PartState nozzle() { + return nozzle; + } + + public @Nullable PartState wing() { + return wing; + } + + public float nozzleAngle() { + return nozzle != null ? nozzle.angle() : 0.0F; + } + + @Override + public int vtolMode() { + if (nozzle == null) { + return 0; + } + if (!nozzle.status()) { + return nozzle.angle() <= 0.005F ? 0 : 1; + } + return nozzle.angle() >= 89.995F ? 2 : 1; + } + + public float rotorAngle(float partialTicks) { + return rotorAngleO + (rotorAngle - rotorAngleO) * partialTicks; + } + + // Pilot actions, server side + + public boolean canSwitchVtol() { + PlaneDefinition def = definition(); + if (def == null || !def.vtol || nozzle == null || modeSwitchCooldown > 0 || vtolMode() == 1) { + return false; + } + return Math.abs(roll) <= 30.0F && !(onGround() && def.defaultVtol); + } + + public void toggleVtol() { + if (canSwitchVtol()) { + modeSwitchCooldown = 20; + setVtol(!nozzle.status()); + } + } + + private void setVtol(boolean on) { + PlaneDefinition def = definition(); + if (nozzle == null || def == null || (def.defaultVtol && onGround() && !on)) { + return; + } + nozzle.set(on, true); + } + + public boolean canFoldWing() { + PlaneDefinition def = definition(); + if (wing == null || def == null || modeSwitchCooldown > 0) { + return false; + } + if (def.variableSweepWing) { + // Sweep back only when fast and well clear of the ground, spread again only when slow. + if (!onGround() && !hasBlockBelow(3, 20, true)) { + if (throttle < 0.7F) { + return false; + } + } else if (throttle > 0.1F) { + return false; + } + } else { + if (!onGround() && !hasBlockBelow(3, 3, true)) { + return false; + } + if (throttle > 0.01F) { + return false; + } + } + return wing.isOff(); + } + + public boolean canUnfoldWing() { + return wing != null && modeSwitchCooldown <= 0 && wing.isOn(); + } + + @Override + public void toggleFold() { + PlaneDefinition def = definition(); + if (def == null) { + return; + } + if (!def.hatches.isEmpty()) { + super.toggleFold(); + } else if (canFoldWing()) { + wing.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldWing()) { + wing.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Folded wings ground the plane; swept wings only below 20% throttle. */ + private boolean canUseWing(PlaneDefinition def) { + if (wing == null) { + return true; + } + if (def.variableSweepWing) { + return throttle >= 0.2 || wing.isOff(); + } + return wing.isOff(); + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void updatePartsServer(AircraftDefinition definition) { + super.updatePartsServer(definition); + PlaneDefinition def = (PlaneDefinition) definition; + // Swept wings spread out again when the plane slows down or gets close to the ground. + if (wing != null && def.variableSweepWing && wing.isOn() && throttle >= 0.2F + && (throttle < 0.5 || hasBlockBelow(1, 10, true))) { + wing.set(false, true); + } + if (nozzle != null && def.defaultVtol && onGround() && vtolMode() == 0) { + nozzle.set(true, true); + } + } + + // Mouse + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + PlaneDefinition def = definition(); + if (def == null || ticksAlive() < 30 || isPassenger() || hasBlockBelow(3, 2, true)) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + boolean vtol = vtolMode() > 0; + float yawFactor = (vtol ? def.vtolYaw : 1.0F) * 0.8F; + float pitchFactor = (vtol ? def.vtolPitch : 1.0F) * 0.8F; + float rollInput = vtolMode() == 0 ? x * 0.5F : x; + + float yaw = clamp(x, 40.0F * def.mobilityYaw) * yawFactor * LOOK_SCALE; + float pitch = clamp(y, 40.0F * def.mobilityPitch) * pitchFactor * LOOK_SCALE; + float rollDelta = clamp(rollInput, 40.0F * def.mobilityRoll) * yawFactor * LOOK_SCALE; + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + // Simulation + + @Override + protected void simulate() { + PlaneDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + if (def.targetDrone && !piloted) { + input.throttleUp = true; + } + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + } + updateMotion(def); + } + + /** + * Gunner mode needs a plane in level flight with throttle up and nothing solid within five + * blocks below, so the autopilot has room to circle. + */ + @Override + public boolean canSwitchGunnerMode() { + if (!super.canSwitchGunnerMode()) { + return false; + } + float r = Math.abs(Mth.wrapDegrees(roll())); + float p = Math.abs(Mth.wrapDegrees(getXRot())); + return r <= 40.0F && p <= 40.0F && visibleThrottle() > 0.6F && !hasBlockBelow(3, 5, false); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlNotHovering}. */ + private void updateThrottle(PlaneDefinition def, boolean piloted) { + reverse *= 0.8F; + boolean canFly = piloted && isCanopyClosed() && canUseWing(def); + if (canFly && isGunnerMode()) { + // The autopilot holds the throttle the pilot left it at. + return; + } + if (canFly || def.targetDrone) { + float upDown = def.throttleUpDown; + boolean turning = input.left != input.right; + boolean up = input.throttleUp; + if (turning && throttle < def.pivotTurnThrottle && !up && !input.throttleDown) { + up = true; + upDown *= 2.0F; + } + if (up) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * def.throttleUpDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}. In gunner mode the autopilot levels out and circles by {@code AutoPilotRot}. */ + private void updateAttitude(PlaneDefinition def, float pt) { + boolean gunner = isGunnerMode(); + if (gunner) { + setXRot(getXRot() * 0.95F); + setYRot(getYRot() + def.autoPilotRot * 0.2F); + if (Math.abs(roll) > 20.0F) { + roll *= 0.95F; + } + } + boolean flying = !hasBlockBelow(3, 3, true); + if (!flying || input.freeLook || gunner || (def.floats && waterDepth() > 0.0)) { + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.6F * yawRate * pt); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.6F * yawRate * pt); + } + } else { + // A/D bank the plane; in VTOL mode the keys do nothing in the air. + float rate = vtolMode() != 0 ? 0.0F : 0.2F; + if (input.left && !input.right) { + keyRoll -= rate * pt; + } else if (input.right && !input.left) { + keyRoll += rate * pt; + } + roll += keyRoll * 0.5F * def.mobilityRoll; + } + keyRoll *= 1.0F - 0.1F * pt; + if (!flying && Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.97F); + } + if (nozzleAngle() > 0.001F) { + setXRot(getXRot() * (1.0F - 0.03F * pt)); + roll *= 1.0F - 0.1F * pt; + } + roll = AircraftRotation.wrap(roll); + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), def.minRotationPitch, def.maxRotationPitch)); + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + private float maxSpeed(PlaneDefinition def) { + float extra = 0.0F; + if (def.variableSweepWing) { + if (wing != null) { + extra = (def.sweepWingSpeed - def.speed) * wing.factor(); + } else if (hatch != null) { + extra = (def.sweepWingSpeed - def.speed) * hatch.factor(); + } + } + return def.speed + extra; + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(PlaneDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + // The gunner mode autopilot holds its altitude. + boolean levelOff = isGunnerMode(); + + if (depth == 0.0) { + if (def.targetDrone) { + levelOff = targetDroneAutopilot(def) || levelOff; + } + if (!levelOff) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else { + my *= 0.8; + } + } else { + setXRot(getXRot() * 0.8F); + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + } + + double push = throttle / 10.0; + float nozzleAngle = nozzleAngle(); + Vec3 v; + if (nozzleAngle > 0.001F) { + setXRot(getXRot() * 0.95F); + v = direction(getYRot(), getXRot() - nozzleAngle); + if (nozzleAngle >= 90.0F) { + v = new Vec3(v.x * 0.8, v.y, v.z * 0.8); + } + } else { + v = direction(getYRot(), getXRot() - 10.0F); + } + if (!levelOff) { + my += v.y * push / (nozzleAngle <= 0.01F ? 2.0 : 8.0); + } + + boolean canMove = def.canMoveOnGround || !isOnSolidGround(); + if (canMove) { + if (def.enableBack && reverse > 0.0F) { + mx -= v.x * reverse; + mz -= v.z * reverse; + } else { + mx += v.x * push; + mz += v.z * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = maxSpeed(def); + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + if (Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.8F); + } + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** Target drones circle at a safe height on their own. Returns whether to hold altitude. */ + private boolean targetDroneAutopilot(PlaneDefinition def) { + if (hasBlockBelow(3, 40, true)) { + if (!hasBlockBelow(3, 5, true)) { + setYRot(getYRot() + def.autoPilotRot * 2.0F); + if (getXRot() > -20.0F) { + setXRot(getXRot() - 0.5F); + } + } + return false; + } + setYRot(getYRot() + def.autoPilotRot); + setXRot(getXRot() * 0.95F); + if (!level().isClientSide() && canRaiseLandingGear() && modeSwitchCooldown <= 0) { + toggleLandingGear(); + } + return true; + } + + /** {@code MCH_Lib.Rot2Vec3}: unit vector along a yaw and pitch, Minecraft conventions. */ + private static Vec3 direction(float yaw, float pitch) { + double y = Math.toRadians(yaw); + double p = Math.toRadians(pitch); + return new Vec3(-Math.sin(y) * Math.cos(p), -Math.sin(p), Math.cos(y) * Math.cos(p)); + } + + // Animation and sound + + @Override + protected void tickAnimation() { + PlaneDefinition def = definition(); + float throttle = visibleThrottle(); + rotorAngleO = rotorAngle; + rotorAngle += throttle * def.rotorSpeed; + if (rotorAngle > 360.0F) { + rotorAngle -= 360.0F; + rotorAngleO -= 360.0F; + } else if (rotorAngle < 0.0F) { + rotorAngle += 360.0F; + rotorAngleO += 360.0F; + } + + float target = throttle; + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + PlaneDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return 0.6F + visibleThrottle() * 0.4F; + } +} From 4a3e357171cce2fbe5266718ff0142486e3aec92 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:41 +0800 Subject: [PATCH 0980/1461] Add entity/RappelEntity.java to the 1.21.5 shared sources --- .../mcheli/entity/RappelEntity.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java new file mode 100644 index 00000000..6fdd2ab1 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Carries someone down a helicopter's rope, {@code MCH_EntityHide}. It stays under its hook while + * the helicopter drifts, slides down at a steady pace and lets its rider off at the ground or at + * the end of the rope. Pulling the ropes in drops whoever is still on them. + */ +public class RappelEntity extends Entity { + private static final EntityDataAccessor DATA_VEHICLE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_ROPE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + private final InterpolationHandler interpolation = new VehicleInterpolation(this); + + public RappelEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_VEHICLE, 0); + builder.define(DATA_ROPE, -1); + } + + public void attach(AircraftEntity vehicle, int rope) { + entityData.set(DATA_VEHICLE, vehicle.getId()); + entityData.set(DATA_ROPE, rope); + } + + private @Nullable AircraftEntity vehicle() { + return level().getEntity(entityData.get(DATA_VEHICLE)) instanceof AircraftEntity a && a.isAlive() ? a : null; + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + AircraftEntity vehicle = vehicle(); + Entity rider = getFirstPassenger(); + if (rider != null) { + rider.resetFallDistance(); + } + if (level().isClientSide()) { + return; + } + if (vehicle == null || !vehicle.isRepelling() || rider == null) { + arrive(); + return; + } + Vec3 hook = vehicle.ropePosition(entityData.get(DATA_ROPE)); + Vec3 m = getDeltaMovement(); + double fall = (m.y - (onGround() ? 0.01 : 0.03)) * 0.9; + setPos(hook.x, getY(), hook.z); + if (onGround()) { + arrive(); + return; + } + move(MoverType.SELF, new Vec3(0.0, fall, 0.0)); + setDeltaMovement(0.0, fall, 0.0); + if (Math.abs(getY() - hook.y) > Math.abs(vehicle.ropeLength()) + 5.0F) { + arrive(); + } + } + + private void arrive() { + ejectPassengers(); + discard(); + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + } +} From e3d10431a7a5eb564154a500b76aac6199e3c789 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:41 +0800 Subject: [PATCH 0981/1461] Add entity/TankEntity.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/entity/TankEntity.java | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java new file mode 100644 index 00000000..e729c7f5 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java @@ -0,0 +1,267 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.physics.GroundImpact; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Tanks and cars, ported from {@code MCH_EntityTank}. + * + *

The drive model is the plane's minus lift: throttle pushes along the hull, A/D turn it, + * S brakes and then reverses when {@code EnableBack} is set, and Space is a hand brake. The hull + * does not follow the mouse. Instead a {@link TerrainFollower} reads the terrain under the + * wheels and the hull eases towards that pitch and roll, while the crew look around freely and + * the turret follows their view. + */ +public class TankEntity extends AircraftEntity implements EngineSoundSource { + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private final TerrainFollower terrain = new TerrainFollower(this); + private double currentSpeed = MIN_SPEED; + /** + * Squared horizontal distance covered by last tick's move. The original read + * {@code posX - prevPosX} here, but vanilla resets {@code xo} before {@link #simulate()} runs, + * which left vehicles with {@code PivotTurnThrottle} unable to steer. + */ + private double lastStepSq; + private float soundLevel; + private float turnSound; + + public TankEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable TankDefinition lookup(String name) { + return MCHeli.content().tank(name); + } + + @Override + public @Nullable TankDefinition definition() { + return (TankDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + TankDefinition def = (TankDefinition) definition; + terrain.create(def.groundWheels, new Vector3(0.0, -0.35, def.weightedCenterZ)); + } + + /** Walls and long drops hurt tanks and cars, scaled by speed and height. */ + @Override + protected boolean takesGroundImpacts() { + return true; + } + + /** Tanks climb and tilt on slopes all the time; only rolling over damages them. */ + @Override + protected float crashTilt() { + return 91.0F; + } + + @Override + protected void simulate() { + TankDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), -90.0F, 90.0F)); + roll = Mth.clamp(roll, -90.0F, 90.0F); + } + } + updateMotion(def, piloted); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlSub}. */ + private void updateThrottle(TankDefinition def, boolean piloted) { + reverse *= 0.8F; + float upDown = def.throttleUpDown; + if (input.brake) { + reverse *= 0.5F; + throttle = Math.max(throttle - 0.02 * upDown, 0.0); + } + if ((piloted && isCanopyClosed()) || def.targetDrone) { + if (input.throttleUp || (def.targetDrone && !piloted)) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * upDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}: ease onto the terrain, steer with A/D. */ + private void updateAttitude(TankDefinition def, float pt) { + setXRot(getXRot() + (terrain.targetPitch - getXRot()) * pt); + roll += (terrain.targetRoll - roll) * pt; + + boolean flying = !hasBlockBelow(3, 3, true); + if (flying && !(def.floats && waterDepth() > 0.0)) { + return; + } + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + float pivot = def.pivotTurnThrottle; + double dist = lastStepSq; + // With PivotTurnThrottle set the vehicle has to be rolling before it can turn, like a car. + if (pivot <= 0.0F || throttle >= pivot || reverse >= pivot / 10.0F || dist > reverse * 0.01) { + float speedScale = pivot <= 0.0F ? 1.0F : (float) Math.sqrt(Math.min(dist, 1.0)); + float direction = !input.throttleUp && input.throttleDown && throttle < pivot + 0.05 ? -1.0F : 1.0F; + float step = 0.6F * yawRate * pt * direction * speedScale; + if (input.left && !input.right) { + setYRot(getYRot() - step); + } else if (input.right && !input.left) { + setYRot(getYRot() + step); + } + } + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(TankDefinition def, boolean piloted) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + + if (depth == 0.0) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double push = throttle / 10.0; + double yawRad = Math.toRadians(getYRot()); + double pitchRad = Math.toRadians(getXRot() - 10.0F); + double vx = -Math.sin(yawRad) * Math.cos(pitchRad); + double vy = -Math.sin(pitchRad); + double vz = Math.cos(yawRad) * Math.cos(pitchRad); + my += vy * push / 8.0; + + if (def.canMoveOnGround || !isOnSolidGround()) { + if (def.enableBack && reverse > 0.0F) { + mx -= vx * reverse; + mz -= vz * reverse; + } else { + mx += vx * push; + mz += vz * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + } + + if (!terrain.isEmpty()) { + Vec3 slide = terrain.move(mx, mz); + mx += slide.x; + mz += slide.z; + if (!piloted) { + // Nobody is easing the hull in per frame, so take the attitude straight away. + setXRot(terrain.targetPitch); + roll = terrain.targetRoll; + } + } + + double startX = getX(); + double startZ = getZ(); + move(MoverType.SELF, new Vec3(mx, my, mz)); + double stepX = getX() - startX; + double stepZ = getZ() - startZ; + lastStepSq = stepX * stepX + stepZ * stepZ; + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** The original's {@code applyOnGroundPitch} is a no-op for tanks; the terrain decides. */ + @Override + protected void applyOnGroundPitch(float factor) { + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void tickAnimation() { + float target = visibleThrottle(); + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + // Turning on the spot still revs the engine. + if (!input.left && !input.right && !input.throttleDown) { + turnSound *= 0.8F; + } else { + turnSound = Math.min(turnSound + 0.1F, 0.75F); + } + target = Math.max(target, turnSound); + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + TankDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return Math.max(0.5F + visibleThrottle() * 0.5F, 0.5F + turnSound * 0.5F); + } +} From 6181884b9373a616208b53863415db948c8a91bb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:41 +0800 Subject: [PATCH 0982/1461] Add entity/TerrainFollower.java to the 1.21.5 shared sources --- .../mcheli/entity/TerrainFollower.java | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java new file mode 100644 index 00000000..92e8e59e --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java @@ -0,0 +1,225 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * Works out the pitch and roll of a ground vehicle from where its wheels touch the terrain. + * Ported from {@code MCH_WheelManager}. + * + *

The original spawned an invisible entity per wheel and let each one fall onto the ground + * under its mount point. Here every wheel is a one block collision box moved with vanilla's + * collision code, which gives the same result without extra entities to sync. Each left/right + * pair spans a plane with the centre of gravity; the weighted average of their normals is the + * ground normal the hull leans towards. + */ +final class TerrainFollower { + private static final double STEP_HEIGHT = 1.5; + private static final double MAX_DRIFT = 2.0; + private static final double SNAP_DISTANCE = 8.0; + + private final AircraftEntity vehicle; + private Probe[] probes = new Probe[0]; + private Vector3 centre = Vector3.ZERO; + private double wheelBase = 1.0; + float targetPitch; + float targetRoll; + + TerrainFollower(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + /** + * @param wheels positive X wheel positions, front to back; the mirrored wheel is added for each + * @param centre centre of gravity in hull space + */ + void create(List wheels, Vector3 centre) { + this.centre = centre; + probes = new Probe[wheels.size() * 2]; + double minZ = Double.MAX_VALUE; + double maxZ = -Double.MAX_VALUE; + for (int i = 0; i < probes.length; i++) { + Vector3 w = wheels.get(i / 2); + Vector3 local = new Vector3(i % 2 == 0 ? w.x() : -w.x(), w.y(), w.z()); + Probe probe = new Probe(local, local.z() >= centre.z()); + Vec3 at = vehicle.toWorld(local); + probe.setPos(at.x, at.y + 1.0, at.z); + probes[i] = probe; + minZ = Math.min(minZ, local.z()); + maxZ = Math.max(maxZ, local.z()); + } + wheelBase = Math.max(maxZ - minZ, 1.0E-3); + } + + boolean isEmpty() { + return probes.length == 0; + } + + /** + * Drags the wheels along with the hull, lets them settle on the terrain and derives the target + * attitude from them. + * + * @return extra horizontal motion from sliding down steep slopes + */ + Vec3 move(double mx, double mz) { + for (Probe probe : probes) { + Vec3 target = vehicle.toWorld(probe.local); + if (target.distanceToSqr(probe.x, probe.y, probe.z) > SNAP_DISTANCE * SNAP_DISTANCE) { + // Teleported, or created before the vehicle was placed: start over under the hull. + probe.setPos(target.x, target.y + 1.0, target.z); + } + double dx = target.x - probe.x + mx; + double dy = (target.y - probe.y) * 0.15; + double dz = target.z - probe.z + mz; + probe.move(dx, dy, dz); + probe.move(0.0, -0.1, 0.0); + } + + int pairs = probes.length / 2; + // If neither half touches the ground, the outermost pair of that half carries the hull, + // which makes the vehicle tip over a crest instead of floating level. + int rear = pairs - 1; + for (int i = 0; i < pairs; i++) { + if (!probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + rear = -1; + break; + } + } + if (rear >= 0) { + probes[rear * 2].onGround = true; + probes[rear * 2 + 1].onGround = true; + } + int front = 0; + for (int i = pairs - 1; i >= 0; i--) { + if (probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + front = -1; + break; + } + } + if (front >= 0 && pairs > 0) { + probes[front * 2].onGround = true; + probes[front * 2 + 1].onGround = true; + } + + Vec3 wc = vehicle.toWorld(centre).subtract(vehicle.position()); + double cx = vehicle.getX() + wc.x; + double cy = vehicle.getY() + centre.y(); + double cz = vehicle.getZ() + wc.z; + Vec3 normal = Vec3.ZERO; + for (int i = 0; i < pairs; i++) { + Probe w1 = probes[i * 2]; + Probe w2 = probes[i * 2 + 1]; + Vec3 v1 = new Vec3(w1.x - cx, w1.y - cy, w1.z - cz); + Vec3 v2 = new Vec3(w2.x - cx, w2.y - cy, w2.z - cz); + Vec3 n = (w1.local.z() >= 0.0 ? v2.cross(v1) : v1.cross(v2)).normalize(); + double weight = w1.onGround || w2.onGround ? Math.abs(w1.local.z() / wheelBase) : 0.0; + normal = normal.add(n.scale(weight)); + } + normal = normal.normalize(); + + Vec3 slide = Vec3.ZERO; + if (normal.y > 0.01 && normal.y < 0.7) { + slide = new Vec3(normal.x / 50.0, 0.0, normal.z / 50.0); + } + + // Into the hull's frame, then read pitch and roll off the normal. + double yaw = Math.toRadians(vehicle.getYRot()); + double c = Math.cos(yaw); + double s = Math.sin(yaw); + double lx = normal.x * c + normal.z * s; + double lz = normal.z * c - normal.x * s; + float pitch = (float) (90.0 - Math.toDegrees(Math.atan2(normal.y, lz))); + float roll = (float) -(90.0 - Math.toDegrees(Math.atan2(normal.y, lx))); + if (normal.lengthSqr() < 1.0E-6) { + pitch = vehicle.getXRot(); + roll = vehicle.roll; + } + + var def = vehicle.definition(); + float pitchStep = def != null ? def.onGroundPitchFactor : 0.0F; + float rollStep = def != null ? def.onGroundRollFactor : 0.0F; + targetPitch = Mth.clamp(pitch, vehicle.getXRot() - pitchStep, vehicle.getXRot() + pitchStep); + targetRoll = Mth.clamp(roll, vehicle.roll - rollStep, vehicle.roll + rollStep); + + // Wheels that fell behind (stuck on a ledge) are pulled back under the hull. + for (Probe probe : probes) { + Vec3 v = vehicle.position().add(new Vec3(AircraftRotation.toWorld(vehicle.getYRot(), targetPitch, targetRoll, + probe.local.x(), probe.local.y(), probe.local.z()))); + boolean pulled = false; + double x = probe.x; + double z = probe.z; + if (x > v.x + MAX_DRIFT || x < v.x - MAX_DRIFT) { + x = Mth.clamp(x, v.x - MAX_DRIFT, v.x + MAX_DRIFT); + pulled = true; + } + if (z > v.z + MAX_DRIFT || z < v.z - MAX_DRIFT) { + z = Mth.clamp(z, v.z - MAX_DRIFT, v.z + MAX_DRIFT); + pulled = true; + } + if (pulled) { + probe.setPos(x, v.y + STEP_HEIGHT / 2.0, z); + } + } + return slide; + } + + /** One wheel: a one block box that falls onto the terrain and climbs steps up to 1.5 high. */ + private final class Probe { + final Vector3 local; + /** In front of the centre of gravity. */ + final boolean front; + double x; + double y; + double z; + boolean onGround; + + Probe(Vector3 local, boolean front) { + this.local = local; + this.front = front; + } + + void setPos(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + private AABB box() { + return new AABB(x - 0.5, y, z - 0.5, x + 0.5, y + 1.0, z + 0.5); + } + + void move(double dx, double dy, double dz) { + AABB box = box(); + Vec3 wanted = new Vec3(dx, dy, dz); + Vec3 moved = collide(wanted, box); + boolean blockedSideways = moved.x != dx || moved.z != dz; + boolean wasGrounded = onGround || (moved.y != dy && dy < 0.0); + if (blockedSideways && wasGrounded) { + // Same step-up rule as the original wheel entity: climb, move, settle back down. + Vec3 up = collide(new Vec3(0.0, STEP_HEIGHT, 0.0), box); + AABB raised = box.move(up); + Vec3 across = collide(new Vec3(dx, 0.0, dz), raised); + AABB shifted = raised.move(across); + Vec3 down = collide(new Vec3(0.0, -STEP_HEIGHT, 0.0), shifted); + Vec3 stepped = up.add(across).add(down); + if (stepped.horizontalDistanceSqr() > moved.horizontalDistanceSqr()) { + moved = stepped; + } + } + x += moved.x; + y += moved.y; + z += moved.z; + onGround = dy < 0.0 && moved.y != dy; + } + + private Vec3 collide(Vec3 movement, AABB box) { + return Entity.collideBoundingBox(vehicle, movement, box, vehicle.level(), List.of()); + } + } +} From 11452050c1414cada7a7a6bc567bca4bd230dc73 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:42 +0800 Subject: [PATCH 0983/1461] Save the station's item with the registries on 1.21.5 --- .../mcheli/entity/UavStationEntity.java | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java new file mode 100644 index 00000000..836e6d55 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java @@ -0,0 +1,339 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.UUIDUtil; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * The UAV station and the portable UAV controller, {@code MCH_EntityUavStation}. A player sits at + * it and puts a UAV in its slot: the drone appears at the station's launch offset and the player + * flies it from there. The portable controller only launches small UAVs and its lid opens and + * closes with Shift and right-click. + * + *

The original kept the player at the station and had the drone read its pilot from there. + * Here the pilot takes the drone's reserved seat for as long as they fly it, hidden and unhurt, + * so the drone uses the same controls, cameras, HUD and weapons as every other vehicle. Leaving + * the drone, or losing it, puts the pilot back beside the station. + */ +public class UavStationEntity extends Entity { + public static final int STATION = 1; + public static final int PORTABLE = 2; + /** Further out than this, measured flat, the link breaks and the drone is lost, as in the original. */ + public static final double RANGE = 123.0; + /** The launch offset goes up to this far from the station along each axis. */ + public static final int MAX_OFFSET = 50; + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_OPEN = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_OFFSET = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BLOCK_POS); + private static final EntityDataAccessor DATA_LINKED = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + + private final InterpolationHandler interpolation = new VehicleInterpolation(this); + private final SimpleContainer slot = new SimpleContainer(1); + private @Nullable UUID lastUav; + /** The lid of the portable controller, 0 shut to 1 open. */ + private float cover; + private float previousCover; + + public UavStationEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, (byte) STATION); + builder.define(DATA_OPEN, true); + builder.define(DATA_OFFSET, new BlockPos(12, 2, 12)); + builder.define(DATA_LINKED, false); + } + + public int kind() { + return entityData.get(DATA_KIND); + } + + public void setKind(int kind) { + entityData.set(DATA_KIND, (byte) (kind == PORTABLE ? PORTABLE : STATION)); + } + + public boolean isOpen() { + return entityData.get(DATA_OPEN); + } + + /** Where a launched drone appears, relative to the station. */ + public BlockPos offset() { + return entityData.get(DATA_OFFSET); + } + + public void setOffset(BlockPos offset) { + entityData.set(DATA_OFFSET, new BlockPos(Mth.clamp(offset.getX(), -MAX_OFFSET, MAX_OFFSET), + Mth.clamp(offset.getY(), -MAX_OFFSET, MAX_OFFSET), Mth.clamp(offset.getZ(), -MAX_OFFSET, MAX_OFFSET))); + } + + /** Whether a drone is flown from here right now; the screen lights up. */ + public boolean isLinked() { + return entityData.get(DATA_LINKED); + } + + public void setLinked(boolean linked) { + entityData.set(DATA_LINKED, linked); + } + + public SimpleContainer slot() { + return slot; + } + + public float cover(float partialTicks) { + return Mth.lerp(partialTicks, previousCover, cover); + } + + /** + * {@code initUavPostion}: the first launch offset is twelve blocks out on the diagonal the + * station faces, two up. + */ + public void initOffset() { + int quarter = (int) (Mth.positiveModulo(getYRot() + 45.0F, 360.0F) / 90.0F) % 4; + int x = quarter == 0 || quarter == 3 ? 12 : -12; + int z = quarter == 0 || quarter == 1 ? 12 : -12; + setOffset(new BlockPos(x, 2, z)); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + previousCover = cover; + cover = Mth.clamp(cover + (isOpen() ? 0.1F : -0.1F), 0.0F, 1.0F); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.03, 0.0); + move(MoverType.SELF, new Vec3(0.0, m.y, 0.0)); + setDeltaMovement(0.0, onGround() ? 0.0 : m.y * 0.96, 0.0); + + Entity user = getFirstPassenger(); + if (user instanceof ServerPlayer player && !slot.getItem(0).isEmpty()) { + launch(player, slot.getItem(0)); + } + } + + /** What the slot holds, if it is a vehicle at all. */ + public static @Nullable AircraftDefinition definitionOf(ItemStack stack) { + String name = AircraftItem.vehicleOf(stack); + return name != null ? MCHeli.content().any(name) : null; + } + + /** Whether this station can put up the vehicle in {@code stack}. */ + public boolean accepts(@Nullable AircraftDefinition def) { + return def != null && def.uav && (kind() != PORTABLE || def.smallUav); + } + + /** {@code handleItem}: puts up the drone in the slot and hands it to {@code player}. */ + private void launch(ServerPlayer player, ItemStack stack) { + AircraftDefinition def = definitionOf(stack); + if (!accepts(def) || !(level() instanceof ServerLevel server)) { + return; + } + if (ServerConfig.isDisabled(def.name())) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName()), true); + return; + } + AircraftEntity uav = ModEntities.typeFor(def).create(server, EntitySpawnReason.TRIGGERED); + if (uav == null) { + return; + } + BlockPos o = offset(); + double y = Math.max(getY() + o.getY(), server.getMinY() + 2.0); + uav.setType(def); + uav.snapTo(getX() + o.getX(), y, getZ() + o.getZ(), getYRot() - 180.0F, 0.0F); + if (!server.noCollision(uav, uav.getBoundingBox().deflate(0.1))) { + player.displayClientMessage(Component.translatable("message.mcheli.uav.blocked"), true); + slot.setItem(0, ItemStack.EMPTY); + Containers.dropItemStack(server, player.getX(), player.getY(), player.getZ(), stack); + return; + } + slot.setItem(0, ItemStack.EMPTY); + uav.setFuelLevel(def.targetDrone ? uav.maxFuel() : (int) (uav.maxFuel() * 0.05F)); + server.addFreshEntity(uav); + if (!def.targetDrone) { + player.closeContainer(); + control(player, uav); + } + } + + /** {@code controlLastAircraft}: takes the last drone flown from here again. */ + public boolean continueControl(ServerPlayer player) { + AircraftEntity uav = lastUav(); + if (uav == null || getFirstPassenger() != player || !uav.canTakeRemotePilot()) { + return false; + } + player.closeContainer(); + return control(player, uav); + } + + public @Nullable AircraftEntity lastUav() { + if (lastUav == null || !(level() instanceof ServerLevel server)) { + return null; + } + Entity e = server.getEntity(lastUav); + return e instanceof AircraftEntity uav && uav.isAlive() && !uav.isDestroyed() && inRange(uav) ? uav : null; + } + + public boolean inRange(Entity uav) { + double dx = uav.getX() - getX(); + double dz = uav.getZ() - getZ(); + return dx * dx + dz * dz <= RANGE * RANGE; + } + + private boolean control(ServerPlayer player, AircraftEntity uav) { + player.stopRiding(); + if (!uav.takeRemotePilot(player, this)) { + return false; + } + lastUav = uav.getUUID(); + setLinked(true); + player.setYRot(uav.getYRot()); + return true; + } + + /** Where a pilot comes back to when the drone is left or lost: the seat in front of the station. */ + public Vec3 returnPosition() { + float yaw = getYRot() * Mth.DEG_TO_RAD; + return new Vec3(getX() - Mth.sin(yaw) * 0.9, getY(), getZ() + Mth.cos(yaw) * 0.9); + } + + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + if (isVehicle()) { + return InteractionResult.PASS; + } + if (kind() == PORTABLE && player.isSecondaryUseActive()) { + if (!level().isClientSide()) { + entityData.set(DATA_OPEN, !isOpen()); + } + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive() || kind() == PORTABLE && !isOpen()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer server) { + player.setYRot(getYRot() - 180.0F); + if (player.startRiding(this)) { + server.openMenu(new SimpleMenuProvider((id, inventory, p) -> new UavStationMenu(id, inventory, this), + Component.translatable(kind() == PORTABLE ? "item.mcheli.uav_station2" : "item.mcheli.uav_station"))); + } + } + return InteractionResult.SUCCESS; + } + + /** The original sat the player down in front of the console, lower for the portable one. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + double y = kind() == PORTABLE ? -0.6 : 0.35; + return new Vec3(0.0, y, 0.9).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + if (isInvulnerableToBase(source) || isRemoved() || damage <= 0.0F) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + AircraftEntity uav = lastUav(); + if (uav != null) { + uav.releaseRemotePilot(); + } + Containers.dropContents(level, blockPosition(), slot); + if (!creative) { + spawnAtLocation(level, item()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), source.getEntity() instanceof Player ? "hit" : "helidmg", 1.0F, 1.0F); + discard(); + return true; + } + + private Item item() { + return kind() == PORTABLE ? ModItems.UAV_CONTROLLER.get() : ModItems.UAV_STATION.get(); + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith() { + return true; + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(item()); + } + + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + setKind(input.getIntOr("Kind", STATION)); + entityData.set(DATA_OPEN, input.getBooleanOr("Open", true)); + setOffset(new BlockPos(input.getIntOr("UavX", 12), input.getIntOr("UavY", 2), input.getIntOr("UavZ", 12))); + lastUav = input.read("LastUav", UUIDUtil.CODEC).orElse(null); + slot.setItem(0, input.read("Item", ItemStack.CODEC, registryAccess().createSerializationContext(NbtOps.INSTANCE)).orElse(ItemStack.EMPTY)); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + output.putInt("Kind", kind()); + output.putBoolean("Open", isOpen()); + BlockPos o = offset(); + output.putInt("UavX", o.getX()); + output.putInt("UavY", o.getY()); + output.putInt("UavZ", o.getZ()); + if (lastUav != null) { + output.store("LastUav", UUIDUtil.CODEC, lastUav); + } + if (!slot.getItem(0).isEmpty()) { + output.store("Item", ItemStack.CODEC, registryAccess().createSerializationContext(NbtOps.INSTANCE), slot.getItem(0)); + } + } +} From 031c4ce8d4af4561b6ca4f14c077c6894febed12 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:42 +0800 Subject: [PATCH 0984/1461] Keep the interpolation target when a packet carries only a rotation on 1.21.5 --- .../mcheli/entity/VehicleInterpolation.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleInterpolation.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleInterpolation.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleInterpolation.java new file mode 100644 index 00000000..5ba61eb5 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleInterpolation.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.phys.Vec3; + +/** + * Eases server position updates in, and keeps the target when one carries only a rotation. On this + * version the client answers those by interpolating to where the entity is right now, which would + * leave a turning vehicle short of its real position until the next full sync. + */ +public final class VehicleInterpolation extends InterpolationHandler { + private final Entity entity; + + public VehicleInterpolation(Entity entity) { + super(entity, 3); + this.entity = entity; + } + + @Override + public void interpolateTo(Vec3 position, float yRot, float xRot) { + boolean rotationOnly = hasActiveInterpolation() && position.equals(entity.position()); + super.interpolateTo(rotationOnly ? position() : position, yRot, xRot); + } +} From c9734892902f9e4407aeae13521101bf99079a9e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:43 +0800 Subject: [PATCH 0985/1461] Save the vehicle inventory with the registries on 1.21.5 --- .../mcheli/entity/VehicleInventory.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java new file mode 100644 index 00000000..7dd7045a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java @@ -0,0 +1,145 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.supply.SupplyRules; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +/** + * What a vehicle carries besides its crew, {@code MCH_AircraftInventory} and the container half of + * {@code W_EntityContainer}: the equipment slots of the vehicle screen (three fuel cans and a + * parachute per ejection seat) and the cargo chest of vehicles with an {@code InventorySize}. + * + *

The cargo chest always has room for the largest size, 54 slots, and the screen shows as many + * rows as the definition asks for. Items in rows a content reload took away are kept rather than + * lost, and still drop when the vehicle is destroyed. + */ +public final class VehicleInventory { + public static final int FUEL_SLOTS = 3; + public static final int PARACHUTE_SLOTS = 2; + public static final int EQUIPMENT_SLOTS = FUEL_SLOTS + PARACHUTE_SLOTS; + public static final int CARGO_SLOTS = 54; + /** How far from the vehicle's centre a player outside may keep a screen open, past its width. */ + private static final double REACH = 12.0; + + private final AircraftEntity vehicle; + private final SimpleContainer equipment = new GuardedContainer(EQUIPMENT_SLOTS); + private final SimpleContainer cargo = new GuardedContainer(CARGO_SLOTS); + + VehicleInventory(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + public SimpleContainer equipment() { + return equipment; + } + + public SimpleContainer cargo() { + return cargo; + } + + /** {@code isUseableByPlayer}: the crew, or someone standing next to the vehicle. */ + public boolean stillValid(Player player) { + if (vehicle.isRemoved() || vehicle.definition() == null) { + return false; + } + if (player.getVehicle() == vehicle) { + return true; + } + double reach = REACH + vehicle.definition().bodyWidth / 2.0; + return player.distanceToSqr(vehicle) <= reach * reach; + } + + // Fuel + + /** + * {@code updateFuel}: every half second, while the vehicle stands on something, each fuel can + * in a fuel slot pours up to 100 units into the tank. Server only. + * + * @return whether any fuel went in + */ + boolean pourFuelCans(int ticks) { + if (ticks % SupplyRules.FUEL_SLOT_INTERVAL != 0 || !vehicle.canSupply()) { + return false; + } + boolean poured = false; + for (int slot = 0; slot < FUEL_SLOTS; slot++) { + ItemStack can = equipment.getItem(slot); + if (!(can.getItem() instanceof FuelItem)) { + continue; + } + int amount = SupplyRules.fuelFromCan(vehicle.maxFuel() - vehicle.fuel(), FuelItem.contents(can)); + if (amount > 0) { + vehicle.refuel(amount); + can.setDamageValue(can.getDamageValue() + amount); + poured = true; + } + } + if (poured) { + equipment.setChanged(); + } + return poured; + } + + // Parachutes + + /** Parachute slots the vehicle screen offers: one per ejection seat. */ + public static int parachuteSlots(AircraftDefinition def) { + return Math.min(def.ejectionSeats(), PARACHUTE_SLOTS); + } + + /** + * {@code consumeParachute}: takes one parachute out of the parachute slots for a crew member + * who bails out. + * + * @return false when there is none left + */ + boolean takeParachute() { + for (int slot = FUEL_SLOTS; slot < EQUIPMENT_SLOTS; slot++) { + if (equipment.getItem(slot).getItem() instanceof ParachuteItem) { + equipment.removeItem(slot, 1); + return true; + } + } + return false; + } + + // Saving and dropping + + void save(CompoundTag output, HolderLookup.Provider registries) { + output.put("Equipment", ContainerHelper.saveAllItems(new CompoundTag(), equipment.getItems(), false, registries)); + output.put("Cargo", ContainerHelper.saveAllItems(new CompoundTag(), cargo.getItems(), false, registries)); + } + + void load(CompoundTag input, HolderLookup.Provider registries) { + equipment.clearContent(); + cargo.clearContent(); + input.getCompound("Equipment").ifPresent(in -> ContainerHelper.loadAllItems(in, equipment.getItems(), registries)); + input.getCompound("Cargo").ifPresent(in -> ContainerHelper.loadAllItems(in, cargo.getItems(), registries)); + } + + /** {@code setDead}: everything aboard falls out where the vehicle was. Server only. */ + void dropAll() { + Containers.dropContents(vehicle.level(), vehicle, equipment); + Containers.dropContents(vehicle.level(), vehicle, cargo); + } + + /** A container that only stays open for players {@link #stillValid(Player)} allows. */ + private final class GuardedContainer extends SimpleContainer { + GuardedContainer(int size) { + super(size); + } + + @Override + public boolean stillValid(Player player) { + return VehicleInventory.this.stillValid(player); + } + } +} From 0bbcc4730b27963c7eac16fe057ce62f13c344fd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:43 +0800 Subject: [PATCH 0986/1461] Add entity/VehicleParticles.java to the 1.21.5 shared sources --- .../mcheli/entity/VehicleParticles.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java new file mode 100644 index 00000000..46ed88a8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java @@ -0,0 +1,180 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.ParticleSplash; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; + +/** + * The spray, dust and exhaust a vehicle throws up, all client side: + *

    + *
  • {@code onUpdate_ParticleSandCloud}: a helicopter's downwash raises a ring of dust off the + * ground or water below it; boats and amphibians with {@code EnableSeaSurfaceParticle} raise + * one off the sea while under way.
  • + *
  • {@code onUpdate_ParticleSplash}: each {@code AddParticleSplash} point sprays while it moves + * through water.
  • + *
  • {@code onUpdate_ParticleNozzle}: jet nozzles smoke with the throttle, more when the blast + * hits the ground, as on a Harrier in the hover.
  • + *
  • {@code onUpdate_ParticleLandingGear}: a plane rolling fast kicks up bits of the ground.
  • + *
+ */ +final class VehicleParticles { + private static final int WHITE = 0xFFFFFF; + + private VehicleParticles() { + } + + static void tick(AircraftEntity vehicle, AircraftDefinition def) { + if (vehicle.isDestroyed()) { + return; + } + switch (vehicle) { + case HelicopterEntity heli -> sandCloud(heli, def, false); + case PlaneEntity plane -> { + if (def instanceof PlaneDefinition planeDef) { + nozzles(plane, planeDef); + } + landingGear(plane); + splashes(plane, def); + sandCloud(plane, def, true); + } + case TankEntity tank -> { + splashes(tank, def); + sandCloud(tank, def, true); + } + default -> { + } + } + } + + private static void sandCloud(AircraftEntity vehicle, AircraftDefinition def, boolean seaOnly) { + if (seaOnly && !def.seaSurfaceParticles || def.particlesScale <= 0.01F) { + return; + } + Level level = vehicle.level(); + RandomSource random = vehicle.getRandom(); + float scale = def.particlesScale * 3.0F * (seaOnly ? 2.0F : 1.0F); + double throttle = Math.min(vehicle.visibleThrottle() * 2.0, 1.0); + // A boat needs some water under it before it sprays; the original counted water blocks. + int count = seaOnly ? (int) (scale * 7.0F) : 0; + int range = (int) (scale * 10.0F) + 1; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + int bx = Mth.floor(vehicle.getX() + 0.5); + int by = Mth.floor(vehicle.getY() + 0.5); + int bz = Mth.floor(vehicle.getZ() + 0.5); + int y = 0; + double groundY = 0.0; + boolean found = false; + for (; y < range && !found; y++) { + for (int x = -1; x <= 1 && !found; x++) { + for (int z = -1; z <= 1 && !found; z++) { + BlockState state = level.getBlockState(pos.set(bx + x, by - y, bz + z)); + if (state.isAir()) { + continue; + } + if (seaOnly && state.getFluidState().is(FluidTags.WATER)) { + count--; + } + if (count <= 0) { + groundY = vehicle.getY() + 1.0 + scale / 5.0F - y; + found = true; + } + } + } + } + if (!found) { + return; + } + double density = (range - y + 1) / (5.0 * scale) / 2.0; + for (int k = 0; k < (int) (throttle * 6.0 * density); k++) { + float angle = random.nextFloat() * Mth.TWO_PI; + double dx = Mth.cos(angle); + double dz = Mth.sin(angle); + level.addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, scale * 5.0F, (int) (10.0F * scale) + 1), true, + vehicle.getX() + dx * scale * 3.0, groundY + (random.nextDouble() - 0.5) * scale, vehicle.getZ() + dz * scale * 3.0, + scale * dx * 0.3, scale * -0.02, scale * dz * 0.3); + } + } + + private static void splashes(AircraftEntity vehicle, AircraftDefinition def) { + if (def.splashes.isEmpty()) { + return; + } + double mx = vehicle.getX() - vehicle.xo; + double mz = vehicle.getZ() - vehicle.zo; + double dist = Math.min(mx * mx + mz * mz, 1.0); + RandomSource random = vehicle.getRandom(); + for (ParticleSplash splash : def.splashes) { + for (int i = 0; i < splash.count(); i++) { + if (dist > 0.03 + random.nextFloat() * 0.1) { + splash(vehicle, splash, -mx * splash.acceleration(), -mz * splash.acceleration(), splash.size() * (0.5 + dist * 0.5)); + } + } + } + } + + private static void splash(AircraftEntity vehicle, ParticleSplash splash, double mx, double mz, double size) { + RandomSource random = vehicle.getRandom(); + Vec3 at = vehicle.toWorld(splash.position()).add(random.nextDouble() - 0.5, (random.nextDouble() - 0.5) * 0.5, random.nextDouble() - 0.5); + BlockPos block = BlockPos.containing(at.x + 0.5, at.y, at.z + 0.5); + if (!vehicle.level().getFluidState(block).is(FluidTags.WATER)) { + return; + } + int age = splash.age() + (int) (random.nextFloat() * 0.5F * splash.age()); + vehicle.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, (float) size * (random.nextFloat() * 0.2F + 0.8F), age), true, + at.x, at.y, at.z, mx + (random.nextFloat() - 0.5) * 0.7, splash.motionY(), mz + (random.nextFloat() - 0.5) * 0.7); + } + + private static void nozzles(PlaneEntity plane, PlaneDefinition def) { + float throttle = plane.visibleThrottle(); + if (def.nozzles.isEmpty() || throttle <= 0.1F) { + return; + } + RandomSource random = plane.getRandom(); + PartState nozzle = plane.nozzle(); + float tilt = nozzle != null ? nozzle.angle() * Mth.DEG_TO_RAD : 0.0F; + // Out of the back, turned down as the nozzles swing towards the hover. + Vector3f blast = plane.toWorldOffset(new Vector3(0.0, -Mth.sin(tilt), -Mth.cos(tilt))); + for (MovingPart part : def.nozzles) { + if (random.nextFloat() > throttle * 1.5F) { + continue; + } + Vec3 at = plane.toWorld(part.position()).add(blast.x, blast.y, blast.z); + BlockPos ahead = BlockPos.containing(at.x + blast.x * 3.0, at.y + blast.y * 3.0, at.z + blast.z * 3.0); + float spread = plane.level().getBlockState(ahead).isAir() ? 0.7F : 2.0F; + plane.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, 5.0F * def.particlesScale, 30), true, at.x, at.y, at.z, + blast.x + (random.nextFloat() - 0.5F) * spread, blast.y, blast.z + (random.nextFloat() - 0.5F) * spread); + } + } + + private static void landingGear(PlaneEntity plane) { + double dx = plane.getX() - plane.xo; + double dz = plane.getZ() - plane.zo; + if (!plane.onGround() || dx * dx + dz * dz <= 0.01) { + return; + } + BlockPos below = BlockPos.containing(plane.getX(), plane.getY() - 0.5, plane.getZ()); + BlockState state = plane.level().getBlockState(below); + if (state.getRenderShape() == RenderShape.INVISIBLE) { + return; + } + RandomSource random = plane.getRandom(); + float width = plane.getBbWidth(); + plane.level().addParticle(new BlockParticleOption(ParticleTypes.BLOCK, state), + plane.getX() + (random.nextFloat() - 0.5) * width, plane.getBoundingBox().minY + 0.1, plane.getZ() + (random.nextFloat() - 0.5) * width, + -dx * 4.0, 1.5, -dz * 4.0); + } +} From 83d715b8397115d7ee8d81ce576779e871741e8c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:44 +0800 Subject: [PATCH 0987/1461] Add entity/projectile/A10Entity.java to the 1.21.5 shared sources --- .../mcheli/entity/projectile/A10Entity.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java new file mode 100644 index 00000000..8a695ef4 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java @@ -0,0 +1,159 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import org.jspecify.annotations.Nullable; + +/** + * The A-10 that answers a close air support call, {@code MCH_EntityA10}. It dives in for a second, + * strafes the target with forty GAU-8 rounds and climbs away. + */ +public class A10Entity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_DIRECTION = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.INT); + private static final int LIFETIME = 70; + + private int despawnCount; + private int shotCount; + public int power = 32; + public int explosionPower = 1; + public float acceleration = 4.0F; + private @Nullable Entity shooter; + private @Nullable Entity user; + + public A10Entity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_DIRECTION, 0); + } + + /** + * @param direction 0 heading south (+Z), then clockwise in steps of 90 degrees + */ + public void setup(String weapon, int direction, double mx, double mz, @Nullable Entity shooter, @Nullable Entity user) { + entityData.set(DATA_WEAPON, weapon); + entityData.set(DATA_DIRECTION, direction); + setYRot(90 * direction); + yRotO = getYRot(); + setDeltaMovement(mx, 0.0, mz); + this.shooter = shooter; + this.user = user; + } + + public int direction() { + return entityData.get(DATA_DIRECTION); + } + + /** Hidden during the first second, while it is still out of sight. */ + public boolean isVisible() { + return despawnCount > 20; + } + + @Override + public void tick() { + super.tick(); + despawnCount++; + if (!level().isClientSide()) { + if (despawnCount > LIFETIME) { + discard(); + return; + } + if (shotCount < 40) { + for (int i = 0; i < 2; i++) { + fireGau8(shotCount); + shotCount++; + } + if (shotCount == 38) { + ModSounds.playAt(this, "gau-8_snd", 150.0F, 1.0F); + } + } + } + if (despawnCount <= 20) { + setDeltaMovement(getDeltaMovement().x, -0.3, getDeltaMovement().z); + } else { + setPos(getX() + getDeltaMovement().x, getY() + getDeltaMovement().y, getZ() + getDeltaMovement().z); + setDeltaMovement(getDeltaMovement().add(0.0, 0.02, 0.0)); + } + } + + private void fireGau8(int count) { + int dir = direction(); + double x = getX(); + double z = getZ(); + double tx = random.nextDouble() - 0.5; + double ty = -2.6; + double tz = random.nextDouble() - 0.5; + switch (dir) { + case 0 -> { + tz += 10.0; + z += count * 0.6; + } + case 1 -> { + tx -= 10.0; + x -= count * 0.6; + } + case 2 -> { + tz -= 10.0; + z -= count * 0.6; + } + default -> { + tx += 10.0; + x += count * 0.6; + } + } + BulletEntity round = ModEntities.BULLET.get().create(level(), EntitySpawnReason.TRIGGERED); + if (round == null) { + return; + } + round.launch(x, getY(), z, tx, ty, tz, 90 * dir, 30.0F, acceleration); + round.setWeapon(entityData.get(DATA_WEAPON)); + round.explosionPower = shotCount % 4 == 0 ? explosionPower : 0; + round.power = power; + round.setOwners(shooter, user); + level().addFreshEntity(round); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 512.0 * 512.0; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From ea9b60f367ba80c25465a414cf3d8d400ed8df52 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:44 +0800 Subject: [PATCH 0988/1461] Add entity/projectile/AAMissileEntity.java to the 1.21.5 shared sources --- .../entity/projectile/AAMissileEntity.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java new file mode 100644 index 00000000..a9d41520 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** An air to air missile homing on its locked target, {@code MCH_EntityAAMissile}. */ +public class AAMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 3422500.0; + + public AAMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (age() > 4 && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize * 0.5F); + } + if (level().isClientSide() || w == null || isRemoved()) { + return; + } + Entity t = target(); + if (user() == null || t == null || t.isRemoved()) { + discard(); + return; + } + double d = distanceToSqr(t); + if (d > MAX_DISTANCE_SQ) { + discard(); + } else if (age() > w.rigidityTime) { + // The original compared the squared distance with the plain fuse distance. + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + setPos((t.getX() + getX()) / 2.0, (t.getY() + getY()) / 2.0, (t.getZ() + getZ()) / 2.0); + impact(new Impact(position(), t, null, Direction.UP), 1.0F); + } else { + guideTo(t.getX(), t.getY(), t.getZ()); + } + } + } + + @Override + public String defaultModel() { + return "aamissile"; + } +} From 321c9f5ebfb68d62601c9bdbf76fedb110907b65 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:45 +0800 Subject: [PATCH 0989/1461] Add entity/projectile/ASMissileEntity.java to the 1.21.5 shared sources --- .../entity/projectile/ASMissileEntity.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java new file mode 100644 index 00000000..e5f46c96 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java @@ -0,0 +1,91 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * An air to surface missile flying to the block the gunner aimed at, {@code MCH_EntityASMissile}. + * Powered ones climb for half a second first; gliding bombs only steer sideways. + */ +public class ASMissileEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + + public ASMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public float gravity() { + return isBomblet() ? -0.03F : super.gravity(); + } + + @Override + public float gravityInWater() { + return isBomblet() ? -0.03F : super.gravityInWater(); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && !isBomblet()) { + trail(w.trajectoryParticle, 3, 10.0F * w.smokeSize * 0.5F); + } + if (w != null && !level().isClientSide() && !isBomblet() && !isRemoved()) { + BlockPos aim = BlockPos.containing(targetX, targetY, targetZ); + if (!level().getBlockState(aim).isAir()) { + double dist = Math.sqrt(distanceToSqr(targetX, targetY, targetZ)); + if (dist < w.proximityFuseDist) { + if (w.bomblet > 0) { + for (int i = 0; i < w.bomblet; i++) { + sprinkleBomblet(); + } + } else { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + } + discard(); + } else if (gravity() == 0.0F) { + double up = age() < 10 ? 20.0 : 0.0; + aimAlong(targetX - getX(), targetY + up - getY(), targetZ - getZ()); + } else { + double dx = targetX - getX(); + double dy = (targetY - getY()) * 0.3; + double dz = targetZ - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + Vec3 m = getDeltaMovement(); + setDeltaMovement(dx * acceleration / d, m.y, dz * acceleration / d); + } + } + } + faceMotion(); + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + ASMissileEntity child = spawnChild(ModEntities.AS_MISSILE.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x * 0.5 + (random.nextFloat() - 0.5F) * spread, + m.y * 0.5 / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z * 0.5 + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From c14668ac021a5e059aeeedd7c7c2deb5a2fb531e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:45 +0800 Subject: [PATCH 0990/1461] Add entity/projectile/ATMissileEntity.java to the 1.21.5 shared sources --- .../entity/projectile/ATMissileEntity.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java new file mode 100644 index 00000000..d7031ecf --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java @@ -0,0 +1,84 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** + * An anti tank missile, {@code MCH_EntityATMissile}. In the default mode it flies straight at the + * locked target; in top attack mode it climbs 150 blocks above the shooter and dives. + */ +public class ATMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 2250000.0; + + /** 0 direct, 1 top attack. */ + public int guidanceType; + + public ATMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && tickCount >= w.trajectoryParticleStartTick) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + if (!level().isClientSide() && !isRemoved()) { + Entity t = target(); + if (user() != null && t != null && !t.isRemoved() && w != null) { + steer(w, t); + } else { + discard(); + } + } + faceMotion(); + } + + private void steer(WeaponDefinition w, Entity t) { + double x = t.getX() - getX(); + double y = t.getY() - getY(); + double z = t.getZ() - getZ(); + double d = x * x + y * y + z * z; + if (d > MAX_DISTANCE_SQ) { + discard(); + return; + } + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + return; + } + int rigidity = w.rigidityTime; + if (age() <= rigidity) { + return; + } + float af = age() < rigidity + w.trajectoryParticleStartTick ? 0.5F : 1.0F; + if (guidanceType == 1) { + Entity shooter = user(); + if (age() <= rigidity + 20) { + guideTo(t.getX(), shooter.getY() + 150.0, t.getZ()); + } else if (age() <= rigidity + 30) { + guideTo(t.getX(), shooter.getY(), t.getZ()); + } else { + if (age() == rigidity + 35) { + power = (int) (power * 1.2F); + if (explosionPower > 0) { + explosionPower++; + } + } + guideTo(t.getX(), t.getY(), t.getZ()); + } + } else { + d = Math.sqrt(d); + setDeltaMovement(x * acceleration / d * af, y * acceleration / d * af, z * acceleration / d * af); + } + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From dbafe55dcfd7a9990312f73d12ec93ef67a80581 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:45 +0800 Subject: [PATCH 0991/1461] Add entity/projectile/BombEntity.java to the 1.21.5 shared sources --- .../mcheli/entity/projectile/BombEntity.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java new file mode 100644 index 00000000..c758d75b --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** A free fall bomb, {@code MCH_EntityBomb}. Cluster bombs split into bomblets. */ +public class BombEntity extends ProjectileEntity { + public BombEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (!level().isClientSide() && w != null && !isRemoved()) { + Vec3 m = getDeltaMovement(); + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + float fuse = w.proximityFuseDist; + if (fuse > 0.1F && age() % 10 == 0) { + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(fuse), this::canHit)) { + if (entity instanceof LivingEntity) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + BombEntity child = spawnChild(ModEntities.BOMB.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + /** A cluster bomb that has not split yet is not drawn when it splits right away. */ + public boolean isVisible() { + WeaponDefinition w = weapon(); + return w == null || isBomblet() || w.bomblet <= 0 || w.bombletSTime > 0; + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From 9df52ac8fb9174fd2767ce4a27d136fe07103eb7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:46 +0800 Subject: [PATCH 0992/1461] Add entity/projectile/BulletEntity.java to the 1.21.5 shared sources --- .../entity/projectile/BulletEntity.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java new file mode 100644 index 00000000..b2b418ac --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** A gun round, {@code MCH_EntityBullet}. High explosive rounds may carry a proximity fuse. */ +public class BulletEntity extends ProjectileEntity { + public BulletEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return true; + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (isRemoved() || level().isClientSide() || age() <= 1 || w == null || explosionPower <= 0) { + return; + } + float fuse = w.proximityFuseDist; + if (fuse > 0.1F) { + fuse++; + float range = fuse + Math.abs(w.acceleration); + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(range), this::canHit)) { + if (entity.distanceToSqr(this) < fuse * fuse) { + setPos((entity.getX() + getX()) / 2.0, (entity.getY() + getY()) / 2.0, (entity.getZ() + getZ()) / 2.0); + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + + @Override + public String defaultModel() { + return "bullet"; + } +} From 2f2ab8a51460573e0443da2b6014819dc87193a6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:46 +0800 Subject: [PATCH 0993/1461] Add entity/projectile/DispensedItemEntity.java to the 1.21.5 shared sources --- .../projectile/DispensedItemEntity.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java new file mode 100644 index 00000000..69a2f674 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java @@ -0,0 +1,121 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseFireBlock; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A canister that uses an item on every block around where it lands, {@code MCH_EntityDispensedItem}: + * water bombers put out fires, crop dusters spread bone meal or seeds, and so on. + */ +public class DispensedItemEntity extends ProjectileEntity { + public DispensedItemEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize); + } + if (!level().isClientSide() && w != null) { + Vec3 m = getDeltaMovement(); + if (acceleration < 1.0E-4) { + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + } + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + } + tickBomblets(); + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + return; + } + WeaponDefinition w = weapon(); + Item item = w != null ? LegacyItems.item(w.dispenseItem, w.dispenseDamage) : null; + if (w != null && item != null && user() instanceof ServerPlayer player && !player.isRemoved()) { + BlockPos center = hit.block() != null ? hit.block() : blockPosition(); + int range = w.dispenseRange - 1; + for (int x = -range; x <= range; x++) { + for (int y = -range; y <= range; y++) { + for (int z = -range; z <= range; z++) { + int dist = x * x + y * y + z * z; + if (dist <= range * range && (dist <= 0.5 * range * range || random.nextInt(2) == 0)) { + useOn(server, center.offset(x, y, z), item); + } + } + } + } + } + discard(); + } + + private void useOn(ServerLevel server, BlockPos pos, Item item) { + if (pos.getY() < server.getMinY() || pos.getY() >= server.getMaxY()) { + return; + } + BlockState state = server.getBlockState(pos); + if (state.isAir()) { + return; + } + if (item == Items.WATER_BUCKET) { + // A water drop douses fire and quenches lava instead of placing water. + FluidState fluid = server.getFluidState(pos); + if (state.getBlock() instanceof BaseFireBlock) { + server.removeBlock(pos, false); + } else if (fluid.is(FluidTags.LAVA)) { + server.setBlockAndUpdate(pos, fluid.isSource() ? Blocks.OBSIDIAN.defaultBlockState() : Blocks.COBBLESTONE.defaultBlockState()); + } + return; + } + // Used from above with nobody holding it, the way a dispenser block would. + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(pos).add(0.0, 0.5, 0.0), Direction.UP, pos, false); + item.useOn(new UseOnContext(server, null, InteractionHand.MAIN_HAND, new ItemStack(item), hit)); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + DispensedItemEntity child = spawnChild(ModEntities.DISPENSED_ITEM.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From 51a98be5bda69144e82b2f6c4d74ad5c31a21815 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:47 +0800 Subject: [PATCH 0994/1461] Add entity/projectile/FlareEntity.java to the 1.21.5 shared sources --- .../mcheli/entity/projectile/FlareEntity.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java new file mode 100644 index 00000000..cfd73b74 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java @@ -0,0 +1,160 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A burning flare, or with a fuse a smoke grenade that bursts into a cloud. Port of + * {@code MCH_EntityFlare}. Flares only look the part: decoying happens in the dispenser. + */ +public class FlareEntity extends Entity { + private static final EntityDataAccessor DATA_FUSE = SynchedEntityData.defineId(FlareEntity.class, EntityDataSerializers.INT); + + private double gravity = -0.013; + private double airResistance = 0.992; + private float size = 6.0F; + + public FlareEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_FUSE, 0); + } + + /** + * @param fuse ticks until a smoke grenade bursts, 0 for a flare + * @param slow the long hanging type 4 flare + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float size, int fuse, boolean slow) { + snapTo(x, y, z, random.nextFloat() * 360.0F, random.nextFloat() * 360.0F); + setDeltaMovement(mx, my, mz); + this.size = size; + entityData.set(DATA_FUSE, fuse); + if (slow) { + gravity *= 0.6; + airResistance = 0.995; + } + } + + private int fuse() { + return entityData.get(DATA_FUSE); + } + + /** A smoke grenade rather than a burning flare. */ + public boolean isSmoke() { + return fuse() > 0; + } + + @Override + public void tick() { + if (fuse() > 0 && tickCount >= fuse()) { + // Removal bursts the grenade on the client. + discard(); + return; + } + boolean server = !level().isClientSide(); + if (server && (!level().isLoaded(blockPosition()) || tickCount > 300)) { + discard(); + return; + } + super.tick(); + Vec3 m = getDeltaMovement(); + if (server) { + HitResult hit = level().clip(new ClipContext(position(), position().add(m), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.MISS) { + discard(); + return; + } + } else { + trail(); + } + setPos(getX() + m.x, getY() + m.y, getZ() + m.z); + setDeltaMovement(m.x * airResistance, m.y + gravity, m.z * airResistance); + if (server && (isInWater() || onGround())) { + discard(); + } + } + + private void trail() { + double x = (getX() - xo) / 2.0; + double y = (getY() - yo) / 2.0; + double z = (getZ() - zo) / 2.0; + for (int i = 0; i < 2; i++) { + if (size < 5.0F && random.nextInt(2) == 0) { + continue; + } + level().addAlwaysVisibleParticle(fuse() > 0 ? ParticleTypes.CLOUD : ParticleTypes.LARGE_SMOKE, true, + xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + if (fuse() == 0) { + level().addAlwaysVisibleParticle(ParticleTypes.FLAME, true, xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + } + } + } + + /** A smoke grenade bursting into a cloud that hangs around for ten seconds or more. */ + private void burst() { + if (!level().isClientSide()) { + return; + } + for (int i = 0; i < 20; i++) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + getX() + (random.nextDouble() - 0.5) * 10.0, + getY() + (random.nextDouble() - 0.5) * 10.0, + getZ() + (random.nextDouble() - 0.5) * 10.0, + (random.nextDouble() - 0.5) * 0.045, 0.005, (random.nextDouble() - 0.5) * 0.045); + } + } + + @Override + public void remove(RemovalReason reason) { + if (level().isClientSide() && fuse() > 0) { + burst(); + entityData.set(DATA_FUSE, 0); + } + super.remove(reason); + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean isInvulnerable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From 9468526492493096c50f84675b107398f514497f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:47 +0800 Subject: [PATCH 0995/1461] Add entity/projectile/MarkerRocketEntity.java to the 1.21.5 shared sources --- .../entity/projectile/MarkerRocketEntity.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java new file mode 100644 index 00000000..eb723310 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java @@ -0,0 +1,139 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; + +/** + * A marking rocket, {@code MCH_EntityMarkerRocket}. Where it lands under open sky it pops red + * smoke, and five seconds later a stick of bombs falls on the mark. + */ +public class MarkerRocketEntity extends ProjectileEntity { + private static final EntityDataAccessor DATA_STATUS = SynchedEntityData.defineId(MarkerRocketEntity.class, EntityDataSerializers.BYTE); + + /** 0 inert, 1 flying, 2 marking. */ + private int countDown; + + public MarkerRocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_STATUS, (byte) 0); + } + + public void setStatus(int status) { + if (!level().isClientSide()) { + entityData.set(DATA_STATUS, (byte) status); + } + } + + public int status() { + return entityData.get(DATA_STATUS); + } + + @Override + public void tick() { + int status = status(); + if (level().isClientSide()) { + if (weapon() == null) { + super.tick(); + } + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && status != 0) { + if (status == 1) { + super.tick(); + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } else { + float gb = random.nextFloat() * 0.3F; + int rgb = (int) ((random.nextFloat() * 0.2F + 0.8F) * 255) << 16 | (int) (gb * 255) << 8 | (int) (gb * 255); + for (int i = 0; i < 5; i++) { + level().addAlwaysVisibleParticle(new DustParticleOptions(rgb, 4.0F), true, getX(), getY(), getZ(), + (random.nextFloat() - 0.5F) * 0.7F, 0.3F + random.nextFloat() * 0.3F, (random.nextFloat() - 0.5F) * 0.7F); + } + } + } + } else if (status == 0 || isInWater()) { + discard(); + } else if (status == 1) { + super.tick(); + } else if (countDown > 0) { + countDown--; + if (countDown == 40) { + dropBombs(); + } + } else { + discard(); + } + } + + private void dropBombs() { + int count = 6 + random.nextInt(2); + for (int i = 0; i < count; i++) { + BombEntity bomb = ModEntities.BOMB.get().create(level(), EntitySpawnReason.TRIGGERED); + if (bomb == null) { + continue; + } + bomb.launch(getX() + (random.nextFloat() - 0.5F) * 15.0F, 260.0F + random.nextFloat() * 10.0F + i * 30, + getZ() + (random.nextFloat() - 0.5F) * 15.0F, 0.0, -0.5, 0.0, 0.0F, 90.0F, 4.0); + bomb.setWeapon(weaponName()); + bomb.explosionPower = 3 + random.nextInt(2); + bomb.explosionPowerInWater = 0; + bomb.power = 30; + bomb.piercing = 0; + bomb.setOwners(shooter, user); + level().addFreshEntity(bomb); + } + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (level().isClientSide() || hit.entity() != null || hit.block() == null) { + return; + } + BlockPos mark = hit.block().relative(hit.side()); + if (!level().getBlockState(mark).isAir()) { + discard(); + return; + } + level().setBlockAndUpdate(mark, Blocks.FIRE.defaultBlockState()); + int solid = 0; + for (int y = mark.getY() + 1; y < level().getMaxY(); y++) { + if (!level().getBlockState(new BlockPos(mark.getX(), y, mark.getZ())).isAir() && ++solid >= 5) { + break; + } + } + if (solid < 5) { + setStatus(2); + setPos(mark.getX() + 0.5, mark.getY() + 1.1, mark.getZ() + 0.5); + xo = getX(); + yo = getY(); + zo = getZ(); + countDown = 100; + } else { + discard(); + } + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From 106678c4cd426dd854c433d38a23b7604697c6bb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:48 +0800 Subject: [PATCH 0996/1461] Add entity/projectile/ProjectileEntity.java to the 1.21.5 shared sources --- .../entity/projectile/ProjectileEntity.java | 789 ++++++++++++++++++ 1 file changed, 789 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java new file mode 100644 index 00000000..6ca981af --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java @@ -0,0 +1,789 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Combat; +import com.jnhprojects.mcheli.weapon.Explosions; +import com.jnhprojects.mcheli.weapon.ModDamageTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Everything a weapon fires: rounds, rockets, missiles, bombs and torpedoes. A port of + * {@code MCH_EntityBaseBullet}. + * + *

Projectiles fly on both sides, the client only for looks; hits, fuses and guidance are decided + * on the server. They are never saved: like the original, one found in a loaded chunk vanishes. + */ +public abstract class ProjectileEntity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TARGET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BOMBLET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.BOOLEAN); + /** Who fired, for clients: their rounds must not stop on their own vehicle. */ + private static final EntityDataAccessor DATA_SHOOTER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_USER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + + /** Beyond 5820 blocks from whoever fired it, a projectile is dropped. */ + private static final double MAX_RANGE_SQ = 3.38724E7; + + protected @Nullable Entity shooter; + protected @Nullable Entity user; + protected @Nullable Entity target; + private @Nullable WeaponDefinition weapon; + + public int explosionPower; + public int explosionPowerInWater; + public int power = 1; + public int piercing; + public int delayFuse; + public int sprinkleTime; + public double acceleration = 1.0; + public double accelerationFactor = 1.0; + private int age; + /** Client only: the round hit something here and waits, hidden, for the server to remove it. */ + private boolean spent; + + public Vec3 prevMotion = Vec3.ZERO; + /** Position two ticks ago, used to spread trail particles over the distance covered. */ + protected double xo2; + protected double yo2; + protected double zo2; + + protected ProjectileEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_TARGET, 0); + builder.define(DATA_BOMBLET, false); + builder.define(DATA_SHOOTER, 0); + builder.define(DATA_USER, 0); + } + + /** + * The second constructor of the original: place the projectile and point it along + * {@code (mx, my, mz)} at {@code acceleration} blocks per tick, capped at 3.9. + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float yaw, float pitch, double acceleration) { + snapTo(x, y, z, yaw, pitch); + xo2 = x; + yo2 = y; + zo2 = z; + double a = Math.min(acceleration, 3.9); + double d = Math.sqrt(mx * mx + my * my + mz * mz); + Vec3 motion = d > 0.0 ? new Vec3(mx * a / d, my * a / d, mz * a / d) : Vec3.ZERO; + setDeltaMovement(motion); + prevMotion = motion; + this.acceleration = a; + } + + // Weapon + + public void setWeapon(String name) { + WeaponDefinition def = MCHeli.content().weapon(name); + if (def == null) { + return; + } + weapon = def; + if (!level().isClientSide()) { + entityData.set(DATA_WEAPON, name); + } + if (def.bomblet > 0) { + sprinkleTime = def.bombletSTime; + } + piercing = def.piercing; + if (boostsAcceleration() && def.acceleration > 4.0F) { + accelerationFactor = def.acceleration / 4.0F; + } + } + + /** Rounds and rockets faster than 4 blocks a tick move several steps per tick instead. */ + protected boolean boostsAcceleration() { + return false; + } + + public @Nullable WeaponDefinition weapon() { + return weapon; + } + + /** Model drawn when the weapon names none, one of the original's {@code MCH_DefaultBulletModels}. */ + public abstract String defaultModel(); + + /** The model to draw: {@code ModelBomblet} for bomblets, else {@code ModelBullet}, else the default. */ + public String model() { + if (weapon == null) { + return defaultModel(); + } + String name = isBomblet() ? weapon.bombletModel : weapon.bulletModel; + return name.isEmpty() ? defaultModel() : name; + } + + public String weaponName() { + return entityData.get(DATA_WEAPON); + } + + /** {@code setParameterFromWeapon}: power and charges from the weapon file, and who to credit. */ + public void setParameters(WeaponDefinition def, @Nullable Entity shooter, @Nullable Entity user) { + explosionPower = def.explosion; + explosionPowerInWater = def.explosionInWater; + power = def.power; + piercing = def.piercing; + setOwners(shooter, user); + } + + /** The same, inherited from a parent projectile, for bomblets. */ + public void setParameters(ProjectileEntity parent) { + explosionPower = parent.explosionPower; + explosionPowerInWater = parent.explosionPowerInWater; + power = parent.power; + piercing = parent.piercing; + setOwners(parent.shooter, parent.user); + } + + public void setOwners(@Nullable Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + entityData.set(DATA_SHOOTER, shooter != null ? shooter.getId() : 0); + entityData.set(DATA_USER, user != null ? user.getId() : 0); + } + + public @Nullable Entity user() { + return user; + } + + public @Nullable Entity shooter() { + return shooter; + } + + public boolean isBomblet() { + return entityData.get(DATA_BOMBLET); + } + + public void setBomblet() { + sprinkleTime = 0; + entityData.set(DATA_BOMBLET, true); + } + + public void setTarget(@Nullable Entity entity) { + target = entity; + if (!level().isClientSide()) { + entityData.set(DATA_TARGET, entity != null ? entity.getId() : 0); + } + } + + public @Nullable Entity target() { + return target; + } + + /** A flare seduced this missile: it loses its target and is gone. */ + public void decoy() { + target = null; + discard(); + } + + public int age() { + return age; + } + + public float gravity() { + return weapon != null ? weapon.gravity : 0.0F; + } + + public float gravityInWater() { + return weapon != null ? weapon.gravityInWater : 0.0F; + } + + // Tick + + @Override + public void tick() { + if (spent) { + return; + } + if (level().isClientSide() && age == 0) { + int targetId = entityData.get(DATA_TARGET); + if (targetId > 0) { + target = level().getEntity(targetId); + } + } + Vec3 motion = getDeltaMovement(); + if (!motion.equals(prevMotion)) { + faceMotion(); + } + prevMotion = motion; + age++; + xo2 = xo; + yo2 = yo; + zo2 = zo; + super.tick(); + + if (weapon == null) { + if (age >= 2) { + discard(); + return; + } + setWeapon(weaponName()); + if (weapon == null) { + return; + } + } + + if (!level().isClientSide()) { + if (getY() <= level().getMaxY() && !level().isLoaded(blockPosition())) { + if (weapon.delayFuse <= 0) { + discard(); + return; + } + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + if (delayFuse > 0) { + delayFuse--; + if (delayFuse == 0) { + onTimeout(); + discard(); + return; + } + } + if (!isValid()) { + discard(); + return; + } + if (weapon.timeFuse > 0 && age > weapon.timeFuse) { + onTimeout(); + discard(); + return; + } + if (weapon.explosionAltitude > 0 && isGroundWithin(weapon.explosionAltitude)) { + impact(new Impact(position(), null, null, Direction.DOWN), 1.0F); + } + } + + motion = getDeltaMovement(); + setDeltaMovement(motion.x, motion.y + (isInWater() ? gravityInWater() : gravity()), motion.z); + if (!isRemoved()) { + checkCollisions(); + } + motion = getDeltaMovement().scale(accelerationFactor); + double nx = getX() + motion.x; + double ny = getY() + motion.y; + double nz = getZ() + motion.z; + if (level().isClientSide()) { + splash(nx, ny, nz); + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + level().addParticle(ParticleTypes.BUBBLE, nx - m.x * 0.25, ny - m.y * 0.25, nz - m.z * 0.25, m.x, m.y, m.z); + } + setPos(nx, ny, nz); + } + + /** Turns the nose along the flight path, the way every projectile class did after moving. */ + protected void faceMotion() { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + + /** {@code setMotion}: flies straight along {@code (dx, dy, dz)} at full speed. */ + protected void aimAlong(double dx, double dy, double dz) { + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d > 0.0) { + setDeltaMovement(dx * acceleration / d, dy * acceleration / d, dz * acceleration / d); + } + } + + /** {@code guidanceToTarget}: turns a seventh of the way towards the target each tick. */ + protected void guideTo(double tx, double ty, double tz) { + double dx = tx - getX(); + double dy = ty - getY(); + double dz = tz - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d <= 0.0) { + return; + } + Vec3 m = getDeltaMovement(); + setDeltaMovement((m.x * 6.0 + dx * acceleration / d) / 7.0, + (m.y * 6.0 + dy * acceleration / d) / 7.0, + (m.z * 6.0 + dz * acceleration / d) / 7.0); + faceMotion(); + } + + /** {@code checkValid}: whoever fired it still exists and is not too far away. */ + private boolean isValid() { + if (shooter == null && user == null) { + return false; + } + if (user != null && user.isRemoved()) { + return false; + } + Entity origin = user != null ? user : shooter; + double dx = getX() - origin.getX(); + double dz = getZ() - origin.getZ(); + return dx * dx + dz * dz < MAX_RANGE_SQ; + } + + private boolean isGroundWithin(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(Direction.DOWN); + if (!level().getBlockState(pos).isAir()) { + return true; + } + } + return false; + } + + /** Spawns {@code count} trail particles spread over the last two ticks of flight. */ + protected void trail(String name, int count, float size) { + if (!level().isClientSide() || name.isEmpty() || count < 1 || count > 50) { + return; + } + ParticleOptions particle = trailParticle(name); + double x = (getX() - xo) / count; + double y = (getY() - yo) / count; + double z = (getZ() - zo) / count; + double x2 = (xo - xo2) / count; + double y2 = (yo - yo2) / count; + double z2 = (zo - zo2) / count; + for (int i = 0; i < count; i++) { + double px = (xo + x * i + xo2 + x2 * i) / 2.0; + double py = (yo + y * i + yo2 + y2 * i) / 2.0; + double pz = (zo + z * i + zo2 + z2 * i) / 2.0; + level().addAlwaysVisibleParticle(particle, true, px, py, pz, 0.0, 0.0, 0.0); + } + } + + /** + * The original's own smoke particle ({@code explode}) was a large white puff; the cloud is the + * closest vanilla one. Other names are 1.7.10 particle names. + */ + private static ParticleOptions trailParticle(String name) { + return switch (name) { + case "explode", "cloud" -> ParticleTypes.CLOUD; + case "flame" -> ParticleTypes.FLAME; + case "largesmoke" -> ParticleTypes.LARGE_SMOKE; + case "heart" -> ParticleTypes.HEART; + case "fireworksspark" -> ParticleTypes.FIREWORK; + default -> ParticleTypes.SMOKE; + }; + } + + private void splash(double nx, double ny, double nz) { + if (weapon == null || weapon.power <= 0) { + return; + } + boolean wasDry = !level().getFluidState(BlockPos.containing(xo + 0.5, yo + 0.5, zo + 0.5)).is(FluidTags.WATER); + if (wasDry && level().getFluidState(BlockPos.containing(nx + 0.5, ny + 0.5, nz + 0.5)).is(FluidTags.WATER)) { + float pwr = Math.min(weapon.power, 20); + int n = random.nextInt(1 + (int) pwr / 3) + (int) pwr / 2 + 1; + for (int i = 0; i < n; i++) { + level().addParticle(ParticleTypes.SPLASH, nx, ny + 0.5, nz, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3, + pwr * 0.03 * (random.nextDouble() * 0.5 + 0.5) * 1.8, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3); + } + } + } + + /** {@code onUpdateTimeout}: the fuse ran out in mid air. */ + protected void onTimeout() { + if (!(level() instanceof ServerLevel server) || weapon == null) { + return; + } + if (isInWater()) { + if (explosionPowerInWater > 0) { + explode(server, getX(), getY(), getZ(), explosionPowerInWater, explosionPowerInWater, true); + } + } else if (explosionPower > 0) { + explode(server, getX(), getY(), getZ(), explosionPower, weapon.explosionBlock, false); + } else if (explosionPower < 0) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } + + /** {@code onUpdateBomblet}: once the sprinkle timer runs out the carrier splits up. */ + protected void tickBomblets() { + if (!level().isClientSide() && sprinkleTime > 0 && !isRemoved() && weapon != null) { + sprinkleTime--; + if (sprinkleTime == 0) { + for (int i = 0; i < weapon.bomblet; i++) { + sprinkleBomblet(); + } + discard(); + } + } + } + + protected void sprinkleBomblet() { + } + + /** Creates a copy of this projectile's kind for a bomblet, flying from here. */ + protected @Nullable T spawnChild(EntityType type, double mx, double my, double mz, float yaw, float pitch) { + T child = type.create(level(), EntitySpawnReason.TRIGGERED); + if (child == null) { + return null; + } + child.launch(getX(), getY(), getZ(), mx, my, mz, yaw, pitch, acceleration); + child.setWeapon(weaponName()); + child.setParameters(this); + return child; + } + + // Collision + + /** Where something was hit. {@code entity} is set for entity hits, {@code block} for block hits. */ + public record Impact(Vec3 location, @Nullable Entity entity, @Nullable BlockPos block, Direction side) { + } + + /** {@code onUpdateCollided}: the swept ray of this tick against blocks, then entities. */ + protected void checkCollisions() { + if (weapon == null) { + return; + } + Vec3 motion = getDeltaMovement().scale(accelerationFactor); + Vec3 from = position(); + Vec3 to = from.add(motion); + BlockHitResult blockHit = null; + for (int i = 0; i < 5; i++) { + blockHit = level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (blockHit.getType() != HitResult.Type.BLOCK) { + blockHit = null; + break; + } + BlockState state = level().getBlockState(blockHit.getBlockPos()); + if (user == null || level().isClientSide() || !Combat.isBulletBreakable(state)) { + break; + } + level().destroyBlock(blockHit.getBlockPos(), true, user); + } + + if (weapon.delayFuse > 0) { + if (blockHit != null) { + bounce(blockHit.getDirection()); + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + return; + } + + if (blockHit != null) { + to = blockHit.getLocation(); + } + Impact entityHit = findEntity(from, to, motion); + if (entityHit != null) { + // A vehicle applies the factor of the box struck itself, when it takes the damage. + impact(entityHit, 1.0F); + } else if (blockHit != null) { + impact(new Impact(blockHit.getLocation(), null, blockHit.getBlockPos(), blockHit.getDirection()), 1.0F); + } + } + + private @Nullable Impact findEntity(Vec3 from, Vec3 to, Vec3 motion) { + AABB area = getBoundingBox().expandTowards(motion).inflate(21.0); + Entity best = null; + Vec3 bestAt = null; + double bestDistance = 0.0; + List clipped = new ArrayList<>(); + for (Entity candidate : level().getEntities(this, area, this::canHit)) { + Optional at; + if (candidate instanceof AircraftEntity vehicle) { + at = vehicle.clipHitBoxes(from, to, 0.3); + clipped.add(vehicle); + } else { + at = candidate.getBoundingBox().inflate(0.3).clip(from, to); + } + if (at.isPresent()) { + double d = from.distanceTo(at.get()); + if (best == null || d < bestDistance) { + best = candidate; + bestAt = at.get(); + bestDistance = d; + } + } + } + // Only the vehicle actually hit keeps the factor of the box the ray struck. + for (AircraftEntity vehicle : clipped) { + if (vehicle != best) { + vehicle.consumeHitBoxFactor(); + } + } + return best != null ? new Impact(bestAt, best, null, Direction.UP) : null; + } + + /** Clients only learn who fired through synched data; look them up once it has arrived. */ + private void resolveOwners() { + if (shooter == null && entityData.get(DATA_SHOOTER) > 0) { + shooter = level().getEntity(entityData.get(DATA_SHOOTER)); + } + if (user == null && entityData.get(DATA_USER) > 0) { + user = level().getEntity(entityData.get(DATA_USER)); + } + } + + /** {@code canBeCollidedEntity}: never the shooter, its crew, or rounds fired by the same side. */ + public boolean canHit(Entity entity) { + if (level().isClientSide()) { + resolveOwners(); + } + if (!entity.isPickable() || entity == user || entity == shooter) { + return false; + } + if (entity instanceof ProjectileEntity other) { + if (level().isClientSide()) { + return false; + } + if (other.shooter != null && other.shooter == shooter || other.user != null && other.user == user) { + return false; + } + } + return !(shooter instanceof AircraftEntity vehicle) || !vehicle.hasPassenger(entity); + } + + /** {@code boundBullet}: a delayed fuse round bounces off what it hits. */ + private void bounce(Direction side) { + Vec3 m = getDeltaMovement(); + float bound = weapon != null ? weapon.bound : 0.0F; + switch (side) { + case DOWN -> { + if (m.y > 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case UP -> { + if (m.y < 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case NORTH -> { + if (m.z > 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case SOUTH -> { + if (m.z < 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case WEST -> { + if (m.x > 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + case EAST -> { + if (m.x < 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + } + } + + /** {@code onImpact}: damage what was hit, then explode or pierce through. */ + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + if (weapon != null && (weapon.explosion == 0 || weapon.modeNum >= 2) && hit.block() != null) { + debris(hit); + } + if (piercing <= 0) { + // The server decides what the hit does; until its removal arrives the round stays + // where it struck instead of flying on through the hull. + spent = true; + setPos(hit.location()); + setDeltaMovement(Vec3.ZERO); + } + return; + } + if (weapon == null) { + discard(); + return; + } + if (hit.entity() != null) { + hitEntity(hit.entity(), damageFactor); + piercing = 0; + } + float expPower = explosionPower * damageFactor; + float expPowerInWater = explosionPowerInWater * damageFactor; + Vec3 at = hit.location(); + if (piercing > 0) { + piercing--; + if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, 1.0F, 1.0F, false); + } + return; + } + if (expPowerInWater == 0.0F) { + if (weapon.fae) { + explodeFae(server, expPower); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } else if (hit.entity() != null) { + if (isInWater()) { + explode(server, at.x, at.y, at.z, expPowerInWater, expPowerInWater, true); + } else { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } + } else if (isInWater() || hit.block() != null && isWaterAround(hit.block())) { + BlockPos b = hit.block() != null ? hit.block() : blockPosition(); + explode(server, b.getX(), b.getY(), b.getZ(), expPowerInWater, expPowerInWater, true); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + discard(); + } + + /** {@code MCH_Lib.isBlockInWater}: water on any side of the block. */ + private boolean isWaterAround(BlockPos pos) { + for (Direction d : Direction.values()) { + if (level().getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return level().getFluidState(pos).is(FluidTags.WATER); + } + + /** Client side chips flying off the block a round struck. */ + private void debris(Impact hit) { + if (weapon == null || hit.block() == null) { + return; + } + BlockState state = level().getBlockState(hit.block()); + if (state.isAir()) { + return; + } + float p = weapon.power; + Vec3 m = getDeltaMovement(); + var particle = new BlockParticleOption(ParticleTypes.BLOCK, state); + for (int i = 0; i < p / 3.0F; i++) { + level().addParticle(particle, + hit.location().x + (random.nextFloat() - 0.5) * p / 10.0, + hit.location().y + 0.1, + hit.location().z + (random.nextFloat() - 0.5) * p / 10.0, + -m.x * p / 2.0, p / 2.0F, -m.z * p / 2.0); + } + } + + /** {@code onImpactEntity}. */ + protected void hitEntity(Entity entity, float damageFactor) { + if (!entity.isRemoved()) { + DamageSource source = ModDamageTypes.bullet(level(), this, user); + Combat.hurt(entity, source, power * damageFactor, weapon); + } + confirmHit(); + } + + /** {@code notifyHitBullet}: the shooter hears that a round connected. */ + protected void confirmHit() { + if (user instanceof ServerPlayer player) { + ModSounds.playTo(player, "hit", 1.0F, 1.0F); + } + } + + protected void explode(ServerLevel server, double x, double y, double z, float size, float blockSize, boolean inWater) { + if (weapon == null) { + return; + } + boolean destroyBlocks = !isBomblet() || random.nextInt(3) == 0; + Explosions.Params p = new Explosions.Params(this, user, size, blockSize, weapon.flaming, destroyBlocks, 0, inWater, weapon); + if (Explosions.explode(server, x, y, z, p)) { + confirmHit(); + } + } + + /** Fuel air explosives burn whatever they catch. */ + private void explodeFae(ServerLevel server, float size) { + Explosions.Params p = new Explosions.Params(this, user, size, weapon.explosionBlock, weapon.flaming, true, 15, false, weapon); + if (Explosions.explode(server, getX(), getY(), getZ(), p)) { + confirmHit(); + } + } + + // Entity plumbing + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + // Something thrown at a projectile sets it off, as in the original. + if (damage > 0.0F && source.is(DamageTypes.THROWN) && !isRemoved()) { + impact(new Impact(position().add(0.5, 0.5, 0.5), null, blockPosition(), Direction.UP), 1.0F); + return true; + } + return false; + } + + /** Client only: whether the round already hit something and should no longer be drawn. */ + public boolean isSpent() { + return spent; + } + + @Override + public boolean isPickable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + output.putString("WeaponName", weaponName()); + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From 5676ffbdc0ad69277ced8627aba4f0f18aaf3d5e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:48 +0800 Subject: [PATCH 0997/1461] Add entity/projectile/RocketEntity.java to the 1.21.5 shared sources --- .../entity/projectile/RocketEntity.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java new file mode 100644 index 00000000..c39fa6fe --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** An unguided rocket, {@code MCH_EntityRocket}, which may burst into bomblets. */ +public class RocketEntity extends ProjectileEntity { + public RocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return !isBomblet(); + } + + @Override + public void tick() { + super.tick(); + tickBomblets(); + WeaponDefinition w = weapon(); + if (!isBomblet() && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + RocketEntity child = spawnChild(ModEntities.ROCKET.get(), m.x, m.y, m.z, getYRot(), getXRot()); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(child.getDeltaMovement().add( + (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread)); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From 416f7b5c3a5b831c3baf0e37a4ba0b8e7adadc3c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:49 +0800 Subject: [PATCH 0998/1461] Add entity/projectile/ThrowableEntity.java to the 1.21.5 shared sources --- .../entity/projectile/ThrowableEntity.java | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java new file mode 100644 index 00000000..8466f9c6 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java @@ -0,0 +1,200 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.entity.VehicleInterpolation; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.Explosions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ARGB; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A thrown grenade, {@code MCH_EntityThrowable}. It flies under its definition's gravity, bounces + * off blocks losing speed, and when its fuse runs out either explodes or, for a smoke grenade, + * pours out coloured smoke until it disappears. + */ +public class ThrowableEntity extends Entity { + private static final EntityDataAccessor DATA_NAME = SynchedEntityData.defineId(ThrowableEntity.class, EntityDataSerializers.STRING); + + private final InterpolationHandler interpolation = new VehicleInterpolation(this); + private @Nullable Entity thrower; + private @Nullable ThrowableDefinition definition; + private int definitionGeneration = -1; + + public ThrowableEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Throws {@code def} from {@code thrower}'s eyes the way it looks. + * + * @param strength 0.2 for a flick up to 1.4 for a full draw, the original's acceleration + */ + public static ThrowableEntity thrown(Level level, Entity thrower, ThrowableDefinition def, float strength) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + Vec3 look = thrower.getLookAngle(); + entity.setPos(thrower.getX() + look.x * 0.3, thrower.getEyeY() - 0.1, thrower.getZ() + look.z * 0.3); + entity.setYRot(thrower.getYRot()); + entity.setDeltaMovement(look.scale(1.5 * strength).add(thrower.getDeltaMovement())); + entity.thrower = thrower; + entity.setDefinition(def); + return entity; + } + + /** One fired from a dispenser: no thrower, straight along {@code direction}. */ + public static ThrowableEntity dispensed(Level level, Vec3 at, Vec3 direction, ThrowableDefinition def) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + entity.setPos(at); + entity.setDeltaMovement(direction.normalize().scale(def.dispenseAcceleration)); + entity.setDefinition(def); + return entity; + } + + private void setDefinition(ThrowableDefinition def) { + entityData.set(DATA_NAME, def.name()); + definition = def; + definitionGeneration = MCHeli.content().generation(); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_NAME, ""); + } + + /** The definition by name, looked up again after a content reload. */ + public @Nullable ThrowableDefinition definition() { + if (definitionGeneration != MCHeli.content().generation()) { + definition = MCHeli.content().throwable(entityData.get(DATA_NAME)); + definitionGeneration = MCHeli.content().generation(); + } + return definition; + } + + public String definitionName() { + return entityData.get(DATA_NAME); + } + + @Override + public void tick() { + super.tick(); + interpolation.interpolate(); + ThrowableDefinition def = definition(); + if (def == null) { + // The original gave a missing definition ten ticks to arrive. + if (tickCount > 10 && !level().isClientSide()) { + discard(); + } + return; + } + if (level().isClientSide()) { + if (def.smokes() && tickCount >= def.timeFuse) { + smoke(def); + } + return; + } + if (tickCount >= def.timeFuse && !def.smokes()) { + Explosions.explode((ServerLevel) level(), getX(), getY(), getZ(), new Explosions.Params(this, thrower, + def.explosion, def.explosion, false, true, 0, isInWater(), null)); + discard(); + return; + } + if (tickCount >= def.aliveTime) { + discard(); + return; + } + fly(def); + } + + /** {@code onUpdate} and {@code boundBullet}: gravity of its own, and a bounce off whatever it hits. */ + private void fly(ThrowableDefinition def) { + boolean water = isInWater(); + Vec3 m = getDeltaMovement().add(0.0, water ? def.gravityInWater : def.gravity, 0.0); + Vec3 from = position(); + move(MoverType.SELF, m); + // Whichever axis came up short hit something. + Vec3 moved = position().subtract(from); + double mx = blocked(moved.x, m.x) ? -m.x * def.bound : m.x; + double my = m.y; + double mz = blocked(moved.z, m.z) ? -m.z * def.bound : m.z; + if (blocked(moved.y, m.y)) { + mx *= 0.9; + mz *= 0.9; + // Off the floor or the ceiling it keeps a share of its speed, unless that would be a crawl. + my = Math.abs(my) > 0.05 ? -my * def.bound : 0.0; + } + double drag = water ? 0.8 : 0.99; + setDeltaMovement(mx * drag, my * drag, mz * drag); + } + + private static boolean blocked(double moved, double wanted) { + return Math.abs(moved) < Math.abs(wanted) - 1.0E-6; + } + + private void smoke(ThrowableDefinition def) { + int color = def.smokeColor; + for (int i = 0; i < def.smokeNum; i++) { + // A little noise per puff, keeping greys grey. + float r = ARGB.redFloat(color) * 0.9F + random.nextFloat() * 0.1F; + float g = ARGB.greenFloat(color) * 0.9F + random.nextFloat() * 0.1F; + float b = ARGB.blueFloat(color) * 0.9F + random.nextFloat() * 0.1F; + if (ARGB.red(color) == ARGB.green(color)) { + g = r; + } + if (ARGB.red(color) == ARGB.blue(color)) { + b = r; + } + if (ARGB.green(color) == ARGB.blue(color)) { + b = g; + } + float size = def.smokeSize + random.nextFloat() * def.smokeSize / 3.0F; + level().addAlwaysVisibleParticle(new SmokeParticleOptions(ARGB.colorFromFloat(1.0F, r, g, b) & 0xFFFFFF, size), true, + getX(), getY() + 0.5, getZ(), + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2, + def.smokeVelocityVertical * random.nextFloat() * 0.2, + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2); + } + } + + /** The server moves it; clients ease towards each position it sends. */ + @Override + public InterpolationHandler getInterpolation() { + return interpolation; + } + + @Override + public boolean hurtServer(ServerLevel level, DamageSource source, float damage) { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From d0776a76bf90c62be0c5dc72039830f773dc95a3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:49 +0800 Subject: [PATCH 0999/1461] Add entity/projectile/TorpedoEntity.java to the 1.21.5 shared sources --- .../entity/projectile/TorpedoEntity.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java new file mode 100644 index 00000000..a89f7c01 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java @@ -0,0 +1,56 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A torpedo, {@code MCH_EntityTorpedo}. Once in the water it settles on its running speed and + * either holds course or, when guided, runs to the point the gunner aimed at. + */ +public class TorpedoEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + public double accelerationInWater = 2.0; + + public TorpedoEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + boolean guided = w != null && w.guidedTorpedo; + if (!level().isClientSide() && isInWater()) { + if (acceleration < accelerationInWater) { + acceleration += 0.1; + } else if (acceleration > accelerationInWater + 0.2F) { + acceleration -= 0.1; + } + if (guided) { + aimAlong(targetX - getX(), targetY - getY(), targetZ - getZ()); + } else { + Vec3 m = getDeltaMovement(); + aimAlong(m.x, m.y * 0.8F, m.z); + } + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + if (guided) { + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + } + + @Override + public String defaultModel() { + return "torpedo"; + } +} From 8bfc2954aa330faa0ce8263fac84c317e703d6df Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:50 +0800 Subject: [PATCH 1000/1461] Add entity/projectile/TvMissileEntity.java to the 1.21.5 shared sources --- .../entity/projectile/TvMissileEntity.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java new file mode 100644 index 00000000..a06fef1a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A wire guided missile flown by the gunner's view, {@code MCH_EntityTvMissile}. It steers while + * it is the vehicle's active TV missile; once another is fired it carries on straight. + */ +public class TvMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 1440000.0; + + public TvMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + Entity aimer = user(); + if (aimer != null) { + if (distanceToSqr(aimer) > MAX_DISTANCE_SQ) { + discard(); + } + if (!level().isClientSide() && !isRemoved()) { + steer(aimer); + } + } else if (!level().isClientSide()) { + discard(); + } + } + + private void steer(Entity aimer) { + if (aimer.isRemoved() || !(shooter() instanceof AircraftEntity vehicle) || vehicle.tvMissile() != this) { + return; + } + Vec3 dir = Vec3.directionFromRotation(aimer.getXRot(), aimer.getYRot()); + aimAlong(dir.x, dir.y, dir.z); + setYRot(aimer.getYRot()); + setXRot(aimer.getXRot()); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From 9992cf5067838fe2c99e7754cb865125612f158b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:50 +0800 Subject: [PATCH 1001/1461] Add item/AircraftItem.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/item/AircraftItem.java | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java new file mode 100644 index 00000000..c296bffa --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java @@ -0,0 +1,146 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.ChatFormatting; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.component.TooltipDisplay; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Places a vehicle, the same way a boat is placed. + * + *

There is one item per vehicle kind ({@code mcheli:helicopter}, {@code mcheli:plane} and so + * on). Which vehicle a stack places is stored in its {@link ModDataComponents#VEHICLE} component, + * and its look comes from the {@code item_model} component pointing at the model the import wrote + * for that vehicle. Items are registered once at startup, so this is what lets definitions be + * added or reloaded while the game runs. + */ +public class AircraftItem extends Item { + private final String kind; + + public AircraftItem(String kind, Properties properties) { + super(properties); + this.kind = kind; + } + + /** The vehicle kind this item holds, such as {@code helicopter}. */ + public String kind() { + return kind; + } + + /** + * A stack that places {@code def}. Vehicles from the import show the item model the import + * wrote for them; item models only load with the resources, so pack vehicles keep their kind's + * own model. + */ + public static ItemStack stackOf(AircraftDefinition def) { + ItemStack stack = new ItemStack(ModItems.forKind(def.kind())); + stack.set(ModDataComponents.VEHICLE.get(), def.name()); + if (def.pack().equals(ContentSource.ORIGINAL)) { + stack.set(DataComponents.ITEM_MODEL, ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, def.name())); + } + return stack; + } + + /** The definition name a stack places, or {@code null} if it is not a vehicle item or has none. */ + public static @Nullable String vehicleOf(ItemStack stack) { + return stack.getItem() instanceof AircraftItem item ? item.definitionName(stack) : null; + } + + protected @Nullable String definitionName(ItemStack stack) { + return stack.get(ModDataComponents.VEHICLE.get()); + } + + @Override + public Component getName(ItemStack stack) { + String name = definitionName(stack); + if (name == null) { + return super.getName(stack); + } + AircraftDefinition def = MCHeli.content().any(name); + // Vehicles added at runtime may bring no lang entry; their DisplayName stands in. + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + /** Who made a vehicle from a content pack. The import's vehicles have their credits in the docs. */ + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display, Consumer out, TooltipFlag flag) { + super.appendHoverText(stack, context, display, out, flag); + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + PackManifest credit = def != null ? def.credit() : null; + if (credit != null) { + out.accept((credit.author().isEmpty() + ? Component.translatable("item.mcheli.vehicle.from_pack", credit.name()) + : Component.translatable("item.mcheli.vehicle.from_pack_by", credit.name(), credit.author())) + .withStyle(ChatFormatting.GRAY)); + } + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + HitResult hit = getPlayerPOVHitResult(level, player, ClipContext.Fluid.ANY); + if (!(hit instanceof BlockHitResult blockHit) || hit.getType() != HitResult.Type.BLOCK) { + return InteractionResult.PASS; + } + + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + if (!level.isClientSide()) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.unknown", String.valueOf(name)), true); + } + return InteractionResult.FAIL; + } + if (!level.isClientSide() && ServerConfig.isDisabled(def.name())) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName()), true); + return InteractionResult.FAIL; + } + + AircraftEntity vehicle = ModEntities.typeFor(def).create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (vehicle == null) { + return InteractionResult.FAIL; + } + Vec3 at = blockHit.getLocation(); + vehicle.setType(def); + vehicle.snapTo(at.x, at.y, at.z, player.getYRot(), 0.0F); + if (!level.noCollision(vehicle, vehicle.getBoundingBox())) { + return InteractionResult.FAIL; + } + + if (!level.isClientSide()) { + level.addFreshEntity(vehicle); + level.gameEvent(player, GameEvent.ENTITY_PLACE, at); + stack.consume(1, player); + } + player.awardStat(Stats.ITEM_USED.get(this)); + return InteractionResult.SUCCESS; + } +} From 7ff820431a52144d6c101ff3965af390543ad910 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:50 +0800 Subject: [PATCH 1002/1461] Add item/ChainItem.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/item/ChainItem.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java new file mode 100644 index 00000000..0f89341b --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java @@ -0,0 +1,85 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.ChainEntity; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; + +import java.util.Map; +import java.util.UUID; +import java.util.WeakHashMap; + +/** + * The tow chain, {@code MCH_ItemChain}. Use it on both ends, in either order, two to sixteen + * blocks apart: the vehicle pulls, so one end must be a vehicle. Use it on either end of a chain + * to take the chain off, or on the hooked end again to let go of it. Vehicles and containers + * hand the click over here, and every step is told to the player above the hotbar. + */ +public class ChainItem extends Item { + private static final double MIN_DISTANCE = 2.0; + private static final double MAX_DISTANCE = 16.0; + + /** The first end each player has hooked, until they hook the second. Server only. */ + private static final Map HOOKED = new WeakHashMap<>(); + + public ChainItem(Properties properties) { + super(properties.stacksTo(1)); + } + + /** A click with the chain on {@code target}. */ + public static InteractionResult useOn(Entity target, Player player) { + if (!(player.level() instanceof ServerLevel level)) { + return InteractionResult.SUCCESS; + } + ChainEntity existing = ChainEntity.on(target); + if (existing != null) { + MCHeli.LOGGER.debug("Chain: {} takes the chain off {}", player.getName().getString(), target); + existing.disconnect(); + HOOKED.remove(player); + player.displayClientMessage(Component.translatable("message.mcheli.chain.removed"), true); + return InteractionResult.SUCCESS; + } + UUID firstId = HOOKED.get(player); + Entity first = firstId != null ? level.getEntity(firstId) : null; + // An end hooked far away was most likely forgotten: start over from this one. + if (first == null || first.isRemoved() || target.distanceTo(first) > MAX_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} hooks {}", player.getName().getString(), target); + HOOKED.put(player, target.getUUID()); + ModSounds.playAt(target, "chain", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.hooked", target.getDisplayName()), true); + return InteractionResult.SUCCESS; + } + if (first == target) { + MCHeli.LOGGER.debug("Chain: {} lets go of {}", player.getName().getString(), target); + HOOKED.remove(player); + player.displayClientMessage(Component.translatable("message.mcheli.chain.released"), true); + return InteractionResult.SUCCESS; + } + double distance = target.distanceTo(first); + if (distance < MIN_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} and {} are only {} blocks apart", first, target, distance); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.too_close", String.format("%.1f", distance)), true); + return InteractionResult.FAIL; + } + if (!ChainEntity.canTow(first) && !ChainEntity.canTow(target)) { + MCHeli.LOGGER.debug("Chain: neither {} nor {} can tow", first, target); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.no_vehicle"), true); + return InteractionResult.FAIL; + } + MCHeli.LOGGER.debug("Chain: {} chains {} and {}", player.getName().getString(), first, target); + ChainEntity chain = ChainEntity.connect(level, first, target); + HOOKED.remove(player); + Entity towed = chain.towed(); + Entity tow = chain.tow(); + player.displayClientMessage(Component.translatable("message.mcheli.chain.connected", + towed != null ? towed.getDisplayName() : first.getDisplayName(), tow != null ? tow.getDisplayName() : target.getDisplayName()), true); + return InteractionResult.SUCCESS; + } +} From bffd28b1f7881a56b8901e6ab94eb2908d34f79c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:51 +0800 Subject: [PATCH 1003/1461] Add item/FuelItem.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/item/FuelItem.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java new file mode 100644 index 00000000..efa8f295 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +/** + * The fuel can, {@code MCH_ItemFuel}. It holds 600 units; the durability bar shows how much is + * left. Using it refills it from the coal and charcoal in the inventory, as in the original. + * + *

A can in a fuel slot of the vehicle screen empties into the tank while the vehicle stands on + * something, as in the original. Not in the original: right-clicking a vehicle with the can pours + * up to 100 units at once, which saves opening the screen. + */ +public class FuelItem extends Item { + /** Units a full can holds, the original's maximum damage. */ + public static final int CAPACITY = 600; + /** Units poured into a vehicle per click: what the original moved per fuel slot per update. */ + private static final int POUR = 100; + private static final int PER_COAL = 100; + private static final int PER_CHARCOAL = 75; + + public FuelItem(Properties properties) { + super(properties.durability(CAPACITY)); + } + + public static int contents(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + /** Fills a vehicle's tank from the can. Server only. */ + public static InteractionResult pour(ItemStack stack, AircraftEntity vehicle, Player player) { + int added = vehicle.refuel(player.getAbilities().instabuild ? POUR : Math.min(contents(stack), POUR)); + if (added <= 0) { + return InteractionResult.FAIL; + } + if (!player.getAbilities().instabuild) { + stack.setDamageValue(stack.getDamageValue() + added); + } + vehicle.level().playSound(null, vehicle.getX(), vehicle.getY(), vehicle.getZ(), SoundEvents.BUCKET_EMPTY, SoundSource.NEUTRAL, 1.0F, 1.0F); + return InteractionResult.SUCCESS; + } + + /** Charcoal first, then coal, each one worth part of the can, until it is full. */ + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + if (!stack.isDamaged() || player.getAbilities().instabuild) { + return InteractionResult.PASS; + } + if (!level.isClientSide()) { + refill(stack, player.getInventory(), Items.CHARCOAL, PER_CHARCOAL); + refill(stack, player.getInventory(), Items.COAL, PER_COAL); + } + return InteractionResult.SUCCESS; + } + + private static void refill(ItemStack can, Inventory inventory, Item coal, int worth) { + for (int slot = 0; slot < inventory.getContainerSize() && can.isDamaged(); slot++) { + ItemStack item = inventory.getItem(slot); + while (item.is(coal) && !item.isEmpty() && can.isDamaged()) { + can.setDamageValue(Math.max(can.getDamageValue() - worth, 0)); + item.shrink(1); + } + } + } +} From 445e3733d7ca64407d68b65acddbfc78dbec29d3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:51 +0800 Subject: [PATCH 1004/1461] Add item/LauncherItem.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/item/LauncherItem.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java new file mode 100644 index 00000000..a83ff5bd --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; + +import java.util.function.Supplier; + +/** + * A shoulder launched missile, {@code MCH_ItemLightWeaponBase}: the FIM-92 Stinger and the + * FGM-148 Javelin. Held up it is a sight that locks onto targets; the attack key fires once + * locked. It holds one missile, and its bar shows whether it is loaded: an empty launcher reloads + * from a missile in the inventory after two seconds held but not raised. + */ +public class LauncherItem extends Item { + /** The original's durability: 0 is loaded, the maximum is empty. */ + private static final int EMPTY = 10; + + private final String weapon; + private final Supplier missile; + + /** + * @param weapon the weapon definition it fires, such as {@code fim92} + * @param missile the item it reloads from + */ + public LauncherItem(String weapon, Supplier missile, Properties properties) { + super(properties.durability(EMPTY)); + this.weapon = weapon; + this.missile = missile; + } + + public String weapon() { + return weapon; + } + + public Item missile() { + return missile.get(); + } + + public static boolean isLoaded(ItemStack stack) { + return stack.getDamageValue() < stack.getMaxDamage(); + } + + public static void setLoaded(ItemStack stack, boolean loaded) { + stack.setDamageValue(loaded ? 0 : stack.getMaxDamage()); + } + + /** Not from a vehicle seat or the GLTD, as in the original. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity) && !(player.getVehicle() instanceof GltdEntity); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResult.PASS; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.BOW; + } +} From ab2b2419f4e9df28de4657f9cd64a05db8598c2b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:52 +0800 Subject: [PATCH 1005/1461] Add item/LegacyAircraftItem.java to the 1.21.5 shared sources --- .../mcheli/item/LegacyAircraftItem.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java new file mode 100644 index 00000000..0a286838 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * A vehicle item as 1.0.0 saved it: one item per vehicle, such as {@code mcheli:ah-64}. + * + *

These are still registered for the vehicles imported at startup, so that stacks in 1.0.0 + * worlds load instead of vanishing. They work as before, are not listed anywhere, and turn into + * the generic item of their kind ({@link AircraftItem#stackOf}) as soon as they sit in a player's + * inventory. Stacks in chests convert once a player takes them out. + */ +public final class LegacyAircraftItem extends AircraftItem { + private final String definitionName; + + public LegacyAircraftItem(String kind, String definitionName, Properties properties) { + super(kind, properties); + this.definitionName = definitionName; + } + + @Override + protected @Nullable String definitionName(ItemStack stack) { + return definitionName; + } + + @Override + public void inventoryTick(ItemStack stack, ServerLevel level, Entity owner, @Nullable EquipmentSlot slot) { + if (!(owner instanceof Player player)) { + return; + } + AircraftDefinition def = MCHeli.content().any(definitionName); + if (def == null) { + return; + } + Inventory inventory = player.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + if (inventory.getItem(i) == stack) { + inventory.setItem(i, convert(stack, def)); + return; + } + } + } + + /** The generic stack for {@code def}, keeping count, custom name and anything else set on the old one. */ + static ItemStack convert(ItemStack legacy, AircraftDefinition def) { + ItemStack converted = AircraftItem.stackOf(def); + converted.applyComponents(legacy.getComponentsPatch()); + converted.set(ModDataComponents.VEHICLE.get(), def.name()); + converted.setCount(legacy.getCount()); + return converted; + } +} From 8a1bdc371c3d673d6211e84294dd071203943ebe Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:52 +0800 Subject: [PATCH 1006/1461] Add item/ParachuteItem.java to the 1.21.5 shared sources --- .../mcheli/item/ParachuteItem.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java new file mode 100644 index 00000000..2af7b79b --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; + +/** The parachute, {@code MCH_ItemParachute}: used in mid-air, it opens above the player. */ +public class ParachuteItem extends Item { + public ParachuteItem(Properties properties) { + super(properties.stacksTo(1)); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (player.isPassenger() || player.onGround()) { + return InteractionResult.PASS; + } + if (!level.isClientSide()) { + ParachuteEntity.open(level, player, ParachuteEntity.Kind.ITEM); + player.getItemInHand(hand).consume(1, player); + } + return InteractionResult.SUCCESS; + } +} From 97ffed04e97adb2914bad492263be35b6df83eef Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:53 +0800 Subject: [PATCH 1007/1461] Add item/PlacedEntityItem.java to the 1.21.5 shared sources --- .../mcheli/item/PlacedEntityItem.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java new file mode 100644 index 00000000..34c524d5 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java @@ -0,0 +1,62 @@ +package com.jnhprojects.mcheli.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; + +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Sets up a piece of equipment that lives in the world as an entity: the GLTD, the container, the + * UAV station. Like the originals' items it goes on top of the block clicked, facing the way the + * player looks, and only where it fits. + */ +public class PlacedEntityItem extends Item { + private final Supplier> type; + + public PlacedEntityItem(Supplier> type, Properties properties) { + super(properties); + this.type = type; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + Player player = context.getPlayer(); + BlockPos above = context.getClickedPos().above(); + Entity entity = type.get().create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (entity == null) { + return InteractionResult.FAIL; + } + entity.snapTo(above.getX() + 0.5, above.getY(), above.getZ() + 0.5, player != null ? player.getYRot() : 0.0F, 0.0F); + prepare(entity, player); + if (!level.noCollision(entity, entity.getBoundingBox().deflate(0.1)) || !level.getEntities(entity, entity.getBoundingBox()).isEmpty()) { + return InteractionResult.FAIL; + } + if (!level.isClientSide()) { + level.addFreshEntity(entity); + level.gameEvent(player, GameEvent.ENTITY_PLACE, entity.position()); + } + ItemStack stack = context.getItemInHand(); + if (player != null) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, player); + return InteractionResult.SUCCESS; + } + + /** Sets up the new entity before it is checked for room and spawned. */ + protected void prepare(Entity entity, @Nullable Player player) { + } +} From 77d04a276bd05ba8295d8db1460a2851bbaae59e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:53 +0800 Subject: [PATCH 1008/1461] Add item/RangefinderItem.java to the 1.21.5 shared sources --- .../mcheli/item/RangefinderItem.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java new file mode 100644 index 00000000..6a4fb996 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; + +/** + * The laser rangefinder, {@code MCH_ItemRangeFinder}. Held up to the eye it is a scope that reads + * the range to whatever it points at; the attack key spots targets for the team, using one of + * its ten charges. Crafting it with redstone charges it again. + */ +public class RangefinderItem extends Item { + public static final int CHARGES = 10; + + public RangefinderItem(Properties properties) { + super(properties.durability(CHARGES)); + } + + /** Not from a vehicle seat: the vehicle's own sight does that job. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity); + } + + /** Charges left, from none to {@link #CHARGES}. */ + public static int charges(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResult.PASS; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.SPYGLASS; + } +} From 3d1ab672dcac8bc8f0a6494e05c6163d65f168ca Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:53 +0800 Subject: [PATCH 1009/1461] Add item/ThrowableItem.java to the 1.21.5 shared sources --- .../mcheli/item/ThrowableItem.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java new file mode 100644 index 00000000..8976ab52 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java @@ -0,0 +1,123 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.Direction; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.dispenser.BlockSource; +import net.minecraft.core.dispenser.DefaultDispenseItemBehavior; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemUseAnimation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Hand grenades, {@code MCH_ItemThrowable}. Like the vehicles there is one item, and the + * {@link ModDataComponents#THROWABLE} component says which definition a stack throws, so pack + * throwables need no item of their own. Held like a bow: the longer the draw, the further it goes. + */ +public class ThrowableItem extends Item { + private static final int FULL_DRAW = 35; + + public ThrowableItem(Properties properties) { + super(properties); + } + + /** A stack of {@code def}, showing the item model the import wrote for it. */ + public static ItemStack stackOf(ThrowableDefinition def) { + ItemStack stack = new ItemStack(ModItems.THROWABLE.get()); + stack.set(ModDataComponents.THROWABLE.get(), def.name()); + stack.set(DataComponents.MAX_STACK_SIZE, def.stackSize); + if (def.pack().equals(ContentSource.ORIGINAL)) { + stack.set(DataComponents.ITEM_MODEL, ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, def.name())); + } + return stack; + } + + private static @Nullable ThrowableDefinition definition(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + return name != null ? MCHeli.content().throwable(name) : null; + } + + @Override + public Component getName(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + if (name == null) { + return super.getName(stack); + } + ThrowableDefinition def = MCHeli.content().throwable(name); + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (definition(player.getItemInHand(hand)) == null) { + return InteractionResult.FAIL; + } + player.startUsingItem(hand); + return InteractionResult.CONSUME; + } + + @Override + public int getUseDuration(ItemStack stack, LivingEntity user) { + return 72000; + } + + @Override + public ItemUseAnimation getUseAnimation(ItemStack stack) { + return ItemUseAnimation.BOW; + } + + @Override + public boolean releaseUsing(ItemStack stack, Level level, LivingEntity entity, int remainingTime) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return false; + } + int drawn = Math.max(getUseDuration(stack, entity) - remainingTime, 5); + float strength = drawn <= FULL_DRAW ? drawn / 25.0F : 1.0F; + level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), SoundEvents.ARROW_SHOOT, SoundSource.PLAYERS, + 0.5F, 0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)); + if (!level.isClientSide()) { + level.addFreshEntity(ThrowableEntity.thrown(level, entity, def, strength)); + } + if (entity instanceof Player player) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, entity); + return true; + } + + /** Dispensers throw them straight out, {@code MCH_ItemThrowableDispenseBehavior}. */ + public static class DispenseBehavior extends DefaultDispenseItemBehavior { + @Override + protected ItemStack execute(BlockSource source, ItemStack stack) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return super.execute(source, stack); + } + Direction facing = source.state().getValue(DispenserBlock.FACING); + Vec3 at = source.center().add(facing.getStepX() * 0.7, facing.getStepY() * 0.7, facing.getStepZ() * 0.7); + Vec3 direction = new Vec3(facing.getStepX(), facing.getStepY() + 0.1, facing.getStepZ()); + source.level().addFreshEntity(ThrowableEntity.dispensed(source.level(), at, direction, def)); + stack.shrink(1); + return stack; + } + } +} From ac43cde5297d4e015527f4a386df0ee120f9d450 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:54 +0800 Subject: [PATCH 1010/1461] Add item/UavStationItem.java to the 1.21.5 shared sources --- .../mcheli/item/UavStationItem.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java new file mode 100644 index 00000000..7e3bc1c8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java @@ -0,0 +1,33 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station and the portable UAV controller, {@code MCH_ItemUavStation}. Both place the same + * entity; the kind decides its model and which drones it launches. + */ +public class UavStationItem extends PlacedEntityItem { + private final int kind; + + public UavStationItem(int kind, Properties properties) { + super(ModEntities.UAV_STATION, properties); + this.kind = kind; + } + + /** Squared to the nearest quarter turn, facing the player, as the original placed it. */ + @Override + protected void prepare(Entity entity, @Nullable Player player) { + if (entity instanceof UavStationEntity station) { + station.setKind(kind); + float yaw = player != null ? player.getYRot() : 0.0F; + int quarter = Math.floorMod(Math.round(Mth.wrapDegrees(yaw) / 90.0F), 4); + station.setYRot(quarter * 90.0F - 180.0F); + station.initOffset(); + } + } +} From 07ee499e0c051863e8d5b63aea3d355035587b94 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:54 +0800 Subject: [PATCH 1011/1461] Add item/VehicleIngredient.java to the 1.21.5 shared sources --- .../mcheli/item/VehicleIngredient.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java new file mode 100644 index 00000000..05c610e1 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java @@ -0,0 +1,67 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Matches one vehicle, such as an AH-6 in the recipe that turns it into an AH-6X. Every vehicle of + * a kind shares an item, so a plain item ingredient would accept any of them. In JSON: + *

{"type": "mcheli:vehicle", "vehicle": "ah-6"}
+ * with the type also under {@code neoforge:ingredient_type} and {@code fabric:type}, since each + * loader names custom ingredients its own way. + * + *

This is the loader-independent half; each loader module wraps it in its own ingredient class. + */ +public final class VehicleIngredient { + public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "vehicle"); + + private VehicleIngredient() { + } + + /** The JSON fields, given a way to build the loader's ingredient from a vehicle name. */ + public static MapCodec codec(Function create, Function vehicle) { + return Codec.STRING.fieldOf("vehicle").xmap(create, vehicle); + } + + public static StreamCodec streamCodec(Function create, Function vehicle) { + return ByteBufCodecs.STRING_UTF8.map(create, vehicle).cast(); + } + + /** The items a vehicle can be: the generic item of each kind. */ + @SuppressWarnings("deprecation") + public static HolderSet kindItems() { + return HolderSet.direct(Item::builtInRegistryHolder, + ModItems.HELICOPTER.get(), ModItems.PLANE.get(), ModItems.TANK.get(), ModItems.VEHICLE.get()); + } + + public static Stream> items() { + return kindItems().stream(); + } + + public static boolean matches(String vehicle, @Nullable ItemStack stack) { + return stack != null && vehicle.equals(AircraftItem.vehicleOf(stack)); + } + + /** What recipe books show: the vehicle itself, or nothing while it is not loaded. */ + public static SlotDisplay display(String vehicle) { + AircraftDefinition def = MCHeli.content().any(vehicle); + return def != null ? new SlotDisplay.ItemStackSlotDisplay(AircraftItem.stackOf(def)) + : SlotDisplay.Empty.INSTANCE; + } +} From 200832a1000e34698117d6d1ce9e12c45bf29696 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:55 +0800 Subject: [PATCH 1012/1461] Add item/WrenchItem.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/item/WrenchItem.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java new file mode 100644 index 00000000..627b94c0 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +/** + * The wrench, {@code MCH_ItemWrench}. Right-clicking a damaged vehicle repairs 10 hit points, once + * a second, at one point of the wrench's durability. Sneaking switches the vehicle to its next + * paint scheme. + * + *

The original repaired while the use key was held; a click per repair works the same with + * today's item use, which stops as soon as the cursor leaves the hull. + */ +public class WrenchItem extends Item { + /** An iron tool's durability, the original's tool material. */ + private static final int DURABILITY = 250; + private static final int REPAIR = 10; + private static final int COOLDOWN = 20; + + public WrenchItem(Properties properties) { + super(properties.durability(DURABILITY)); + } + + /** Server only. */ + public static InteractionResult useOn(ItemStack stack, AircraftEntity vehicle, Player player, InteractionHand hand) { + if (player.isSecondaryUseActive()) { + return vehicle.nextTexture() ? InteractionResult.SUCCESS : InteractionResult.FAIL; + } + if (player.getCooldowns().isOnCooldown(stack) || !vehicle.repair(REPAIR)) { + return InteractionResult.FAIL; + } + player.getCooldowns().addCooldown(stack, COOLDOWN); + stack.hurtAndBreak(1, player, LivingEntity.getSlotForHand(hand)); + ModSounds.playAt(vehicle, "wrench", 1.0F, 0.9F + vehicle.getRandom().nextFloat() * 0.2F); + return InteractionResult.SUCCESS; + } +} From 2c9043542ff6ca676a372b937a4907a8b2403843 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:55 +0800 Subject: [PATCH 1013/1461] Add menu/DraftingTableMenu.java to the 1.21.5 shared sources --- .../mcheli/menu/DraftingTableMenu.java | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java new file mode 100644 index 00000000..c38c9530 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java @@ -0,0 +1,148 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.ResultContainer; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; + +import java.util.List; + +/** + * The drafting table, {@code MCH_DraftingTableGuiContainer}: the player's inventory and one output + * slot. The screen browses the MCHeli recipes in the player's recipe book and asks for one by its + * display id; the server finds the recipe, takes the ingredients from the inventory and puts the + * result in the output slot. + */ +public class DraftingTableMenu extends AbstractContainerMenu { + public static final int OUTPUT_X = 178; + public static final int OUTPUT_Y = 90; + + private final ContainerLevelAccess access; + private final ResultContainer output = new ResultContainer(); + private final int outputSlot; + + /** The client side, from the open screen packet. */ + public DraftingTableMenu(int containerId, Inventory inventory) { + this(containerId, inventory, ContainerLevelAccess.NULL); + } + + public DraftingTableMenu(int containerId, Inventory inventory, ContainerLevelAccess access) { + super(ModMenus.DRAFTING_TABLE.get(), containerId); + this.access = access; + addStandardInventorySlots(inventory, 30, 140); + outputSlot = slots.size(); + addSlot(new Slot(output, 0, OUTPUT_X, OUTPUT_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + } + + /** + * {@code createRecipeItem}: makes one of the recipe behind {@code id}, if it is ours, the output + * slot is free and the player carries the ingredients. Creative players need neither. + */ + public void craft(ServerPlayer player, RecipeDisplayId id) { + boolean creative = player.getAbilities().instabuild; + Slot slot = slots.get(outputSlot); + if (slot.hasItem() && !creative) { + return; + } + RecipeManager.ServerDisplayInfo info = player.level().getServer().getRecipeManager().getRecipeFromDisplay(id); + if (info == null) { + return; + } + RecipeHolder holder = info.parent(); + if (!holder.id().location().getNamespace().equals(MCHeli.MOD_ID) || !(holder.value() instanceof CraftingRecipe recipe)) { + return; + } + List ingredients = recipe.placementInfo().ingredients(); + if (!creative && !take(player.getInventory(), ingredients, true)) { + return; + } + if (!creative) { + take(player.getInventory(), ingredients, false); + } + slot.set(recipe.assemble(CraftingInput.EMPTY, player.registryAccess())); + broadcastChanges(); + } + + /** + * Finds, and unless {@code dryRun} removes, one item per ingredient. A stack serves several + * ingredients as far as its count goes. + */ + private static boolean take(Inventory inventory, List ingredients, boolean dryRun) { + int[] used = new int[inventory.getContainerSize()]; + for (Ingredient ingredient : ingredients) { + int found = -1; + for (int i = 0; i < inventory.getContainerSize() && found < 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && stack.getCount() > used[i] && ingredient.test(stack)) { + found = i; + } + } + if (found < 0) { + return false; + } + used[found]++; + } + if (!dryRun) { + for (int i = 0; i < used.length; i++) { + if (used[i] > 0) { + inventory.removeItem(i, used[i]); + } + } + } + return true; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(access, player, ModBlocks.DRAFTING_TABLE.get()); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + if (index != outputSlot) { + return ItemStack.EMPTY; + } + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (!moveItemStackTo(stack, 0, outputSlot, true)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** What is left in the output goes back to the player, as in the original. */ + @Override + public void removed(Player player) { + super.removed(player); + if (!player.level().isClientSide()) { + clearContainer(player, output); + } + } +} From 0205d4cd38ff69df08ae377ced9021b6d45a6da4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:56 +0800 Subject: [PATCH 1014/1461] Add menu/UavStationMenu.java to the 1.21.5 shared sources --- .../mcheli/menu/UavStationMenu.java | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java new file mode 100644 index 00000000..4c5f7bd8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java @@ -0,0 +1,154 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_ContainerUavStation}: one slot for the drone to put up, the + * launch offset with its -10, -1, +1 and +10 buttons per axis, and Continue to take the last drone + * again. The player sits at the station while it is open, so the client finds it as their vehicle. + */ +public class UavStationMenu extends AbstractContainerMenu { + /** Button ids: axis times four plus the step, then Continue. */ + public static final int[] STEPS = {-10, -1, 1, 10}; + public static final int BUTTON_CONTINUE = 12; + + private static final int DATA_SIZE = 4; + private static final int DATA_CAN_CONTINUE = 3; + + private final @Nullable UavStationEntity station; + private final ContainerData data; + + /** The client side, from the open screen packet. */ + public UavStationMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof UavStationEntity s ? s : null, + new SimpleContainer(1), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public UavStationMenu(int containerId, Inventory inventory, UavStationEntity station) { + this(containerId, inventory, station, station.slot(), new ServerData(station)); + } + + private UavStationMenu(int containerId, Inventory inventory, @Nullable UavStationEntity station, Container slot, ContainerData data) { + super(ModMenus.UAV_STATION.get(), containerId); + this.station = station; + this.data = data; + checkContainerSize(slot, 1); + addSlot(new Slot(slot, 0, 20, 20) { + @Override + public boolean mayPlace(ItemStack stack) { + return AircraftItem.vehicleOf(stack) != null; + } + + @Override + public int getMaxStackSize() { + return 1; + } + }); + addStandardInventorySlots(inventory, 8, 84); + addDataSlots(data); + } + + public @Nullable UavStationEntity station() { + return station; + } + + /** The launch offset along {@code axis} (0 x, 1 y, 2 z), as the server last reported it. */ + public int offset(int axis) { + return (short) data.get(axis); + } + + public boolean canContinue() { + return data.get(DATA_CAN_CONTINUE) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (station == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CONTINUE) { + return station.continueControl(server); + } + if (id < 0 || id >= 12) { + return false; + } + int[] pos = {station.offset().getX(), station.offset().getY(), station.offset().getZ()}; + pos[id / 4] += STEPS[id % 4]; + station.setOffset(new BlockPos(pos[0], pos[1], pos[2])); + return true; + } + + @Override + public boolean stillValid(Player player) { + return station != null && station.isAlive() && player.getVehicle() == station; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (index == 0) { + if (!moveItemStackTo(stack, 1, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (AircraftItem.vehicleOf(stack) == null || !moveItemStackTo(stack, 0, 1, false)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** Whatever is left in the slot goes back to the player, as with a crafting table. */ + @Override + public void removed(Player player) { + super.removed(player); + if (station != null && !player.level().isClientSide()) { + clearContainer(player, station.slot()); + } + } + + private record ServerData(UavStationEntity station) implements ContainerData { + @Override + public int get(int index) { + BlockPos o = station.offset(); + return switch (index) { + case 0 -> o.getX(); + case 1 -> o.getY(); + case 2 -> o.getZ(); + default -> station.lastUav() != null ? 1 : 0; + }; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } +} From 489c0c8c0e1ffc7ce3744520093a872dc5e50ae3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:56 +0800 Subject: [PATCH 1015/1461] Add menu/VehicleMenu.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/menu/VehicleMenu.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java new file mode 100644 index 00000000..b2dba1e4 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java @@ -0,0 +1,217 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.weapon.Resupply; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGuiContainer}: fuel slots, a parachute slot per ejection + * seat, reloading the weapons from the crew member's own items and the way into the cargo chest. + * + *

Only crew open it, so the client finds the vehicle as the one its player rides. The weapon + * rounds and which weapons can be reloaded right now come from the server through data slots, + * since a client only hears about the ammunition of the seat it sits in. + */ +public class VehicleMenu extends AbstractContainerMenu { + /** Weapons the screen can show; more than any vehicle in the original has. */ + public static final int MAX_WEAPONS = 32; + /** Button ids from zero up reload that weapon; this one opens the cargo chest. */ + public static final int BUTTON_CARGO = 1000; + + public static final int FUEL_X = 8; + public static final int FUEL_Y = 30; + public static final int PARACHUTE_X = 8; + public static final int PARACHUTE_Y = 66; + public static final int INVENTORY_Y = 102; + + private static final int DATA_RELOADABLE = MAX_WEAPONS; + private static final int DATA_SIZE = MAX_WEAPONS + 2; + + private final @Nullable AircraftEntity vehicle; + private final Container equipment; + private final ContainerData data; + private final int parachuteSlots; + private boolean supplied; + + /** The client side, from the open screen packet. */ + public VehicleMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof AircraftEntity v ? v : null, + new SimpleContainer(VehicleInventory.EQUIPMENT_SLOTS), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public VehicleMenu(int containerId, Inventory inventory, AircraftEntity vehicle) { + this(containerId, inventory, vehicle, vehicle.inventory().equipment(), new ServerData(vehicle, inventory.player)); + } + + private VehicleMenu(int containerId, Inventory inventory, @Nullable AircraftEntity vehicle, Container equipment, ContainerData data) { + super(ModMenus.VEHICLE.get(), containerId); + this.vehicle = vehicle; + this.equipment = equipment; + this.data = data; + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + parachuteSlots = def != null ? VehicleInventory.parachuteSlots(def) : 0; + checkContainerSize(equipment, VehicleInventory.EQUIPMENT_SLOTS); + + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + addSlot(new Slot(equipment, i, FUEL_X + i * 18, FUEL_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() instanceof FuelItem; + } + }); + } + for (int i = 0; i < VehicleInventory.PARACHUTE_SLOTS; i++) { + boolean active = i < parachuteSlots; + addSlot(new Slot(equipment, VehicleInventory.FUEL_SLOTS + i, PARACHUTE_X + i * 18, PARACHUTE_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return active && stack.getItem() instanceof ParachuteItem; + } + + @Override + public boolean isActive() { + return active; + } + }); + } + addStandardInventorySlots(inventory, 8, INVENTORY_Y); + addDataSlots(data); + } + + public @Nullable AircraftEntity vehicle() { + return vehicle; + } + + /** Parachute slots in use: one per ejection seat. */ + public int parachuteSlots() { + return parachuteSlots; + } + + /** Magazine and reserve of a weapon together, as the server last reported them. */ + public int rounds(int weapon) { + return weapon >= 0 && weapon < MAX_WEAPONS ? data.get(weapon) : 0; + } + + /** Whether the server would reload this weapon now: the vehicle is down and the items are there. */ + public boolean canReload(int weapon) { + if (weapon < 0 || weapon >= MAX_WEAPONS) { + return false; + } + int bits = (data.get(DATA_RELOADABLE) & 0xFFFF) | (data.get(DATA_RELOADABLE + 1) & 0xFFFF) << 16; + return (bits & 1 << weapon) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (vehicle == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CARGO) { + return vehicle.openCargo(server); + } + if (id >= 0 && id < MAX_WEAPONS && Resupply.supply(server, vehicle, id)) { + supplied = true; + return true; + } + return false; + } + + @Override + public boolean stillValid(Player player) { + return vehicle != null && equipment.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + int equipmentEnd = VehicleInventory.EQUIPMENT_SLOTS; + if (index < equipmentEnd) { + if (!moveItemStackTo(stack, equipmentEnd, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof FuelItem) { + if (!moveItemStackTo(stack, 0, VehicleInventory.FUEL_SLOTS, false)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof ParachuteItem && parachuteSlots > 0) { + int first = VehicleInventory.FUEL_SLOTS; + if (!moveItemStackTo(stack, first, first + parachuteSlots, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** As in the original, closing the screen after a resupply loads every magazine from its reserve. */ + @Override + public void removed(Player player) { + super.removed(player); + if (supplied && vehicle != null && vehicle.weapons() != null && !player.level().isClientSide()) { + vehicle.weapons().reloadAll(); + } + } + + /** Rounds per weapon and the reload bits, worked out on the server whenever the menu syncs. */ + private record ServerData(AircraftEntity vehicle, Player player) implements ContainerData { + @Override + public int get(int index) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons == null) { + return 0; + } + if (index < MAX_WEAPONS) { + WeaponSet set = weapons.set(index); + // Data slots travel as shorts. + return set != null ? Math.min(set.ammo() + set.restAmmo(), Short.MAX_VALUE) : 0; + } + int bits = 0; + if (player instanceof ServerPlayer server) { + for (int i = 0; i < Math.min(weapons.sets().size(), MAX_WEAPONS); i++) { + if (Resupply.canSupply(server, vehicle, i)) { + bits |= 1 << i; + } + } + } + return index == DATA_RELOADABLE ? bits & 0xFFFF : bits >>> 16; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } +} From cb9fad68cf9ab12c9f78e6b8928c56da35050375 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:57 +0800 Subject: [PATCH 1016/1461] Add network/BoardVehiclePacket.java to the 1.21.5 shared sources --- .../mcheli/network/BoardVehiclePacket.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java new file mode 100644 index 00000000..0f7ccf72 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.Vec3; + +/** + * Client to server: the player right-clicked a vehicle's hull outside its main hit box. The + * server repeats the reach check against the hull boxes, then treats it as an ordinary click. + * + * @param location where on the hull the click landed, in world coordinates + */ +public record BoardVehiclePacket(int entityId, Vec3 location) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("board_vehicle"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(BoardVehiclePacket::encode, BoardVehiclePacket::decode); + + /** The same slack vanilla allows on top of the player's reach for a click on an entity. */ + private static final double REACH_SLACK = 3.0; + + public static void encode(BoardVehiclePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeDouble(msg.location.x); + buf.writeDouble(msg.location.y); + buf.writeDouble(msg.location.z); + } + + public static BoardVehiclePacket decode(FriendlyByteBuf buf) { + return new BoardVehiclePacket(buf.readVarInt(), new Vec3(buf.readDouble(), buf.readDouble(), buf.readDouble())); + } + + public static void handle(BoardVehiclePacket msg, ServerPlayer sender) { + if (sender == null || !(sender.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) || vehicle.isRemoved()) { + return; + } + Vec3 eye = sender.getEyePosition(); + double reach = sender.entityInteractionRange() + REACH_SLACK; + // The click must land on the hull, and the hull must be within reach. + if (eye.distanceToSqr(msg.location) > reach * reach) { + return; + } + Vec3 through = eye.add(msg.location.subtract(eye).scale(1.05)); + boolean onHull = vehicle.clipHitBoxes(eye, through, 0.5).isPresent(); + vehicle.consumeHitBoxFactor(); + if (onHull) { + vehicle.interactAt(sender, msg.location.subtract(vehicle.position()), InteractionHand.MAIN_HAND); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 67ed587b7c99d8a762225ab04a55544223d654c3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:57 +0800 Subject: [PATCH 1017/1461] Add network/CrewActionPacket.java to the 1.21.5 shared sources --- .../mcheli/network/CrewActionPacket.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java new file mode 100644 index 00000000..1b565e21 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java @@ -0,0 +1,164 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A one-off request from a crew member: lower the gear, fold the wings, change seats and so on. + * + *

The client only asks; the server checks that the sender sits in that vehicle, in a seat + * allowed to do it, and then applies the same conditions the original checked before switching. + * The result reaches every client through synched data. + */ +public record CrewActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("crew_action"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(CrewActionPacket::encode, CrewActionPacket::decode); + + public enum Action { + /** Raise or lower the landing gear. Pilot only. */ + LANDING_GEAR, + /** Fold or unfold hatches, wings or swept wings. Pilot only. */ + FOLD, + /** Tilt the nozzles for vertical flight. Pilot only. */ + VTOL, + /** Eject from seat zero or one. */ + EJECT, + /** Move to the next free crew seat. */ + SWITCH_SEAT, + /** Load nearby cargo onto the racks, or drive onto a carrier's rack. Pilot only. */ + RACK_LOAD, + /** Drop the last cargo, or leave the rack this vehicle rides on. Pilot only. */ + RACK_UNLOAD, + /** Select the next weapon this seat may use. */ + NEXT_WEAPON, + /** Select the previous weapon this seat may use. */ + PREVIOUS_WEAPON, + /** Cycle the fire mode of the selected weapon. */ + WEAPON_MODE, + /** Release flares. Pilot and first crew seat. */ + FLARE, + /** Open the vehicle screen: fuel and parachute slots, reloading, the way to the cargo chest. */ + VEHICLE_SCREEN, + /** Hand the controls to the autopilot and man the weapons, or take them back. Pilot only. */ + GUNNER_MODE, + /** Turn night vision on for the sender, in vehicles with {@code EnableNightVision}. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF, + /** Let the troops down: ropes from a hover, a parachute drop, or out on the ground. Pilot only. */ + DROP_CREW, + /** Slide down a rope or jump with a parachute. Crew other than the pilot. */ + JUMP_OUT, + /** Turn the search light on or off. Pilot and first crew seat. */ + SEARCH_LIGHT + } + + public static void encode(CrewActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static CrewActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown crew action " + ordinal); + } + return new CrewActionPacket(entityId, actions[ordinal]); + } + + public static void handle(CrewActionPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + boolean pilot = vehicle.getControllingPassenger() == sender; + switch (msg.action) { + case LANDING_GEAR -> { + if (pilot) { + vehicle.toggleLandingGear(); + } + } + case FOLD -> { + if (pilot) { + vehicle.toggleFold(); + } + } + case VTOL -> { + if (pilot && vehicle instanceof PlaneEntity plane) { + plane.toggleVtol(); + } + } + case GUNNER_MODE -> { + if (pilot) { + vehicle.setGunnerMode(!vehicle.isGunnerMode()); + } + } + case EJECT -> vehicle.eject(sender); + case SWITCH_SEAT -> vehicle.switchSeat(sender); + case RACK_LOAD -> { + if (pilot) { + if (!vehicle.boardNearbyRack()) { + vehicle.loadNearbyCargo(); + } + } + } + case RACK_UNLOAD -> { + if (pilot) { + if (vehicle.isPassenger()) { + vehicle.leaveRack(); + } else { + vehicle.unloadCargo(); + } + } + } + case NEXT_WEAPON, PREVIOUS_WEAPON -> { + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons != null && !vehicle.isDestroyed()) { + int next = weapons.next(seat, msg.action == Action.NEXT_WEAPON ? 1 : -1); + if (next >= 0) { + weapons.switchTo(sender, seat, next); + } + } + } + case WEAPON_MODE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.switchMode(vehicle.seatOf(sender)); + } + } + case FLARE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null && vehicle.seatOf(sender) <= 1) { + weapons.flares.use(); + } + } + case VEHICLE_SCREEN -> vehicle.openScreen(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> vehicle.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + case DROP_CREW -> { + if (pilot && !vehicle.dropCrew()) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + case SEARCH_LIGHT -> vehicle.toggleSearchLight(sender); + case JUMP_OUT -> { + if (!pilot && !vehicle.jumpOut(sender)) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 3b0e360335f61e138af49fd89100752eb8b82d7a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:58 +0800 Subject: [PATCH 1018/1461] Add network/DraftingTablePacket.java to the 1.21.5 shared sources --- .../mcheli/network/DraftingTablePacket.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java new file mode 100644 index 00000000..46294aa3 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; + +/** + * The Create button of the drafting table, {@code MCH_DraftingTableCreatePacket}: make one of the + * recipe the player's recipe book knows by this display id. + */ +public record DraftingTablePacket(RecipeDisplayId recipe) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("drafting_table"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(DraftingTablePacket::encode, DraftingTablePacket::decode); + + public static void encode(DraftingTablePacket msg, FriendlyByteBuf buf) { + RecipeDisplayId.STREAM_CODEC.encode(buf, msg.recipe); + } + + public static DraftingTablePacket decode(FriendlyByteBuf buf) { + return new DraftingTablePacket(RecipeDisplayId.STREAM_CODEC.decode(buf)); + } + + public static void handle(DraftingTablePacket msg, ServerPlayer sender) { + if (sender != null && sender.containerMenu instanceof DraftingTableMenu menu && menu.stillValid(sender)) { + menu.craft(sender, msg.recipe); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 1e64e88472599cc672b6524a0d6c589edee4f410 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:58 +0800 Subject: [PATCH 1019/1461] Add network/EquipmentActionPacket.java to the 1.21.5 shared sources --- .../mcheli/network/EquipmentActionPacket.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java new file mode 100644 index 00000000..76090f2e --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A request from a player using a piece of equipment they sit at, such as the GLTD. Like + * {@link CrewActionPacket} the client only asks; the equipment checks that the sender is the one + * using it. + */ +public record EquipmentActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("equipment_action"); + public static final StreamCodec STREAM_CODEC = + StreamCodec.ofMember(EquipmentActionPacket::encode, EquipmentActionPacket::decode); + + public enum Action { + /** Mark the spot under the sight and call in the A-10. */ + CALL_STRIKE, + /** Turn night vision on for the sender. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF + } + + public static void encode(EquipmentActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static EquipmentActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown equipment action " + ordinal); + } + return new EquipmentActionPacket(entityId, actions[ordinal]); + } + + public static void handle(EquipmentActionPacket msg, ServerPlayer sender) { + if (sender == null || sender.getVehicle() == null || sender.getVehicle().getId() != msg.entityId) { + return; + } + if (sender.getVehicle() instanceof GltdEntity gltd) { + switch (msg.action) { + case CALL_STRIKE -> gltd.callStrike(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> gltd.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 7beec5c338248d637aeba1cb056f478f00e68734 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:59 +0800 Subject: [PATCH 1020/1461] Add network/ExplosionEffectPacket.java to the 1.21.5 shared sources --- .../mcheli/network/ExplosionEffectPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java new file mode 100644 index 00000000..279b1511 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.ExplosionEffect; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to nearby players: draw an MCHeli explosion here. The original's + * {@code MCH_PacketEffectExplosion}, plus a flag for the bigger fireball of a crashing vehicle. + */ +public record ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater, boolean crash) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("explosion_effect"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(ExplosionEffectPacket::encode, ExplosionEffectPacket::decode); + + public ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater) { + this(x, y, z, size, inWater, false); + } + + public static void encode(ExplosionEffectPacket msg, FriendlyByteBuf buf) { + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.size); + buf.writeBoolean(msg.inWater); + buf.writeBoolean(msg.crash); + } + + public static ExplosionEffectPacket decode(FriendlyByteBuf buf) { + return new ExplosionEffectPacket(buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readBoolean(), + buf.readBoolean()); + } + + public static void handle(ExplosionEffectPacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player == null || !Float.isFinite(msg.size)) { + return; + } + float size = Math.min(msg.size, 50.0F); + if (msg.crash) { + ExplosionEffect.crash(player.level(), msg.x, msg.y, msg.z, Math.min(size, ExplosionEffect.MAX_CRASH_SIZE)); + } else { + ExplosionEffect.play(player.level(), msg.x, msg.y, msg.z, size, msg.inWater); + } + } + + @Override + public Type type() { + return TYPE; + } +} From a2f12cf4200a683c8e84b14bd7caca8a59885eec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:59 +0800 Subject: [PATCH 1021/1461] Add network/FireWeaponPacket.java to the 1.21.5 shared sources --- .../mcheli/network/FireWeaponPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java new file mode 100644 index 00000000..0056dcc0 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.ServerFiring; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Client to server: the crew member's selected weapon fired on their side, so fire it for real. + * The client already checked readiness, ammunition and lock; the server checks again with its own + * timers and only trusts the two option values, a locked entity and a fire mode. + * + * @param weapon the weapon the client fired, ignored unless it is still the seat's selection + */ +public record FireWeaponPacket(int entityId, int weapon, int option1, int option2) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("fire_weapon"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(FireWeaponPacket::encode, FireWeaponPacket::decode); + + public static void encode(FireWeaponPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.option1); + buf.writeVarInt(msg.option2); + } + + public static FireWeaponPacket decode(FriendlyByteBuf buf) { + return new FireWeaponPacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(FireWeaponPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons == null || weapons.selected(seat) != msg.weapon || weapons.operatorSeat(msg.weapon) != seat) { + return; + } + Shot shot = new Shot(vehicle, sender); + shot.option1 = msg.option1; + shot.option2 = msg.option2; + weapons.use(sender, seat, shot, ServerFiring.INSTANCE); + } + + @Override + public Type type() { + return TYPE; + } +} From 452f6778df84434fc2e943b8c3b454e99f2bde51 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:03:59 +0800 Subject: [PATCH 1022/1461] Add network/LauncherPacket.java to the 1.21.5 shared sources --- .../mcheli/network/LauncherPacket.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java new file mode 100644 index 00000000..0f93dc7a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java @@ -0,0 +1,119 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +/** + * The player with a launcher in hand, {@code MCH_PacketLightWeaponPlayerControl}: fire at the + * locked target, or finish a reload. The client locks and times the reload; the server checks + * the launcher, the target and the missile in the inventory. + * + * @param target the locked entity, for {@link Action#FIRE} + * @param mode the Javelin's attack mode: 0 direct, 1 top attack + */ +public record LauncherPacket(Action action, int target, int mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("launcher"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LauncherPacket::encode, LauncherPacket::decode); + + /** Locks further than this are refused, with some slack over the lock range for lag. */ + private static final double MAX_TARGET_DISTANCE = 260.0; + + public enum Action { + FIRE, + RELOAD + } + + public static void encode(LauncherPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeVarInt(msg.target); + buf.writeVarInt(msg.mode); + } + + public static LauncherPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown launcher action " + ordinal); + } + return new LauncherPacket(actions[ordinal], buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(LauncherPacket msg, ServerPlayer sender) { + ItemStack stack = sender == null ? ItemStack.EMPTY : sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof LauncherItem launcher) || !LauncherItem.canUse(sender)) { + return; + } + switch (msg.action) { + case FIRE -> fire(sender, stack, launcher, msg.target, msg.mode); + case RELOAD -> reload(sender, stack, launcher); + } + } + + private static void fire(ServerPlayer player, ItemStack stack, LauncherItem launcher, int targetId, int mode) { + WeaponDefinition def = MCHeli.content().weapon(launcher.weapon()); + ServerLevel level = player.serverLevel(); + Entity target = level.getEntity(targetId); + if (def == null || !LauncherItem.isLoaded(stack) || target == null || target.isRemoved() + || target.distanceToSqr(player) > MAX_TARGET_DISTANCE * MAX_TARGET_DISTANCE) { + return; + } + ProjectileEntity missile = def.type == WeaponType.AT_MISSILE + ? ModEntities.AT_MISSILE.get().create(level, EntitySpawnReason.TRIGGERED) + : ModEntities.AA_MISSILE.get().create(level, EntitySpawnReason.TRIGGERED); + if (missile == null) { + return; + } + float yaw = player.getYRot(); + float pitch = player.getXRot(); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + Vec3 at = player.getEyePosition().add(dir); + missile.launch(at.x, at.y, at.z, dir.x, dir.y, dir.z, yaw, pitch, def.acceleration); + missile.setWeapon(launcher.weapon()); + missile.setParameters(def, player, player); + missile.setTarget(target); + if (missile instanceof ATMissileEntity atMissile) { + atMissile.guidanceType = Math.clamp(mode, 0, Math.max(def.modeNum - 1, 0)); + } + level.addFreshEntity(missile); + if (def.sound != null) { + ModSounds.playAt(player, def.sound, def.soundVolume, def.soundPitch); + } + LauncherItem.setLoaded(stack, false); + } + + private static void reload(ServerPlayer player, ItemStack stack, LauncherItem launcher) { + if (LauncherItem.isLoaded(stack)) { + return; + } + if (!player.getAbilities().instabuild) { + int slot = player.getInventory().findSlotMatchingItem(new ItemStack(launcher.missile())); + if (slot < 0) { + return; + } + player.getInventory().removeItem(slot, 1); + } + LauncherItem.setLoaded(stack, true); + } + + @Override + public Type type() { + return TYPE; + } +} From f54db4d41a37bb1673ec01e2f3857f69965670b8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:00 +0800 Subject: [PATCH 1023/1461] Add network/LockWarningPacket.java to the 1.21.5 shared sources --- .../mcheli/network/LockWarningPacket.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java new file mode 100644 index 00000000..97d2cbf4 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java @@ -0,0 +1,44 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +/** + * Client to server: this player is locking on to an entity. If it is a vehicle with flares, or a + * crew member of one, its crew hears the warning. The original's {@code MCH_PacketNotifyLock}. + */ +public record LockWarningPacket(int targetId) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("lock_warning"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LockWarningPacket::encode, LockWarningPacket::decode); + + public static void encode(LockWarningPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.targetId); + } + + public static LockWarningPacket decode(FriendlyByteBuf buf) { + return new LockWarningPacket(buf.readVarInt()); + } + + public static void handle(LockWarningPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity)) { + return; + } + Entity target = sender.level().getEntity(msg.targetId); + if (target != null && target.distanceToSqr(sender) < 400.0 * 400.0) { + AircraftEntity vehicle = target instanceof AircraftEntity a ? a : target.getVehicle() instanceof AircraftEntity a ? a : null; + if (vehicle != null) { + vehicle.warnLocked(); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From f8131f5517dadd1f79192c2b57512d81d8070729 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:00 +0800 Subject: [PATCH 1024/1461] Add network/MarkerPacket.java to the 1.21.5 shared sources --- .../mcheli/network/MarkerPacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java new file mode 100644 index 00000000..1de0bfc3 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to client: the team spotted these entities for {@code ticks}, or marked {@code point}, + * {@code MCH_PacketNotifySpotedEntity} and {@code MCH_PacketNotifyMarkPoint}. Only the spotter and + * their teammates get it. + */ +public record MarkerPacket(int[] entities, int ticks, BlockPos point) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("marker"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(MarkerPacket::encode, MarkerPacket::decode); + /** Spotting sends no point; this stands in for none. */ + private static final BlockPos NO_POINT = new BlockPos(0, Integer.MIN_VALUE, 0); + private static final int MAX_ENTITIES = 256; + + public static MarkerPacket spotted(int[] entities, int ticks) { + return new MarkerPacket(entities, ticks, NO_POINT); + } + + public static MarkerPacket point(BlockPos point) { + return new MarkerPacket(new int[0], 0, point); + } + + public static void encode(MarkerPacket msg, FriendlyByteBuf buf) { + buf.writeVarIntArray(msg.entities); + buf.writeVarInt(msg.ticks); + buf.writeBlockPos(msg.point); + } + + public static MarkerPacket decode(FriendlyByteBuf buf) { + return new MarkerPacket(buf.readVarIntArray(MAX_ENTITIES), buf.readVarInt(), buf.readBlockPos()); + } + + public static void handle(MarkerPacket msg) { + if (msg.entities.length > 0) { + Markers.spotted(msg.entities, msg.ticks); + } + if (!msg.point.equals(NO_POINT)) { + Markers.point(msg.point); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 865472573238425767980e3f3953f4ceef3b71b7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:01 +0800 Subject: [PATCH 1025/1461] Add network/ModNetwork.java to the 1.21.5 shared sources --- .../mcheli/network/ModNetwork.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java new file mode 100644 index 00000000..74ee6247 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java @@ -0,0 +1,90 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Our packets. Each one is a custom payload with its own id and the same encoding on every loader, + * so a client on one loader can talk to a server on another. The loader module registers the + * lists below with its own networking API; handlers always run on the main thread. + */ +public final class ModNetwork { + /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ + public static final int PROTOCOL_VERSION = 11; + + /** Client to server. The handler gets the sending player. */ + public static final List> SERVERBOUND = List.of( + new Serverbound<>(PilotStatePacket.TYPE, PilotStatePacket.STREAM_CODEC, PilotStatePacket::handle), + new Serverbound<>(CrewActionPacket.TYPE, CrewActionPacket.STREAM_CODEC, CrewActionPacket::handle), + new Serverbound<>(EquipmentActionPacket.TYPE, EquipmentActionPacket.STREAM_CODEC, EquipmentActionPacket::handle), + new Serverbound<>(SpotPacket.TYPE, SpotPacket.STREAM_CODEC, SpotPacket::handle), + new Serverbound<>(LauncherPacket.TYPE, LauncherPacket.STREAM_CODEC, LauncherPacket::handle), + new Serverbound<>(DraftingTablePacket.TYPE, DraftingTablePacket.STREAM_CODEC, DraftingTablePacket::handle), + new Serverbound<>(TeamPacket.TYPE, TeamPacket.STREAM_CODEC, TeamPacket::handle), + new Serverbound<>(TestFlightPacket.TYPE, TestFlightPacket.STREAM_CODEC, TestFlightPacket::handle), + new Serverbound<>(FireWeaponPacket.TYPE, FireWeaponPacket.STREAM_CODEC, FireWeaponPacket::handle), + new Serverbound<>(BoardVehiclePacket.TYPE, BoardVehiclePacket.STREAM_CODEC, BoardVehiclePacket::handle), + new Serverbound<>(LockWarningPacket.TYPE, LockWarningPacket.STREAM_CODEC, LockWarningPacket::handle), + new Serverbound<>(PackRequestPacket.TYPE, PackRequestPacket.STREAM_CODEC, PackRequestPacket::handle), + new Serverbound<>(PackSubmitPacket.TYPE, PackSubmitPacket.STREAM_CODEC, PackSubmitPacket::handle)); + + /** Server to client. Handlers touch client classes, so they only ever run on a client. */ + public static final List> CLIENTBOUND = List.of( + new Clientbound<>(WeaponStatePacket.TYPE, WeaponStatePacket.STREAM_CODEC, WeaponStatePacket::handle), + new Clientbound<>(ExplosionEffectPacket.TYPE, ExplosionEffectPacket.STREAM_CODEC, ExplosionEffectPacket::handle), + new Clientbound<>(PackListPacket.TYPE, PackListPacket.STREAM_CODEC, PackListPacket::handle), + new Clientbound<>(PackDataPacket.TYPE, PackDataPacket.STREAM_CODEC, PackDataPacket::handle), + new Clientbound<>(PackSoundPacket.TYPE, PackSoundPacket.STREAM_CODEC, PackSoundPacket::handle), + new Clientbound<>(MarkerPacket.TYPE, MarkerPacket.STREAM_CODEC, MarkerPacket::handle)); + + private ModNetwork() { + } + + static CustomPacketPayload.Type type(String name) { + return new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, name)); + } + + public static void sendToServer(CustomPacketPayload message) { + Platform.get().sendToServer(message); + } + + public static void sendToPlayer(ServerPlayer player, CustomPacketPayload message) { + Platform.get().sendToPlayer(player, message); + } + + public static void sendToAll(MinecraftServer server, CustomPacketPayload message) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + sendToPlayer(player, message); + } + } + + /** Sends to every player in {@code level} within {@code range} blocks of the point. */ + public static void sendNear(ServerLevel level, double x, double y, double z, double range, CustomPacketPayload message) { + for (ServerPlayer player : level.players()) { + if (player.distanceToSqr(x, y, z) < range * range) { + sendToPlayer(player, message); + } + } + } + + public record Serverbound(CustomPacketPayload.Type type, + StreamCodec codec, + BiConsumer handler) { + } + + public record Clientbound(CustomPacketPayload.Type type, + StreamCodec codec, + Consumer handler) { + } +} From ccefce0116e5a5ceb0739abd1c577bba0b8fabb4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:01 +0800 Subject: [PATCH 1026/1461] Add network/PackDataPacket.java to the 1.21.5 shared sources --- .../mcheli/network/PackDataPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java new file mode 100644 index 00000000..89c1644d --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: one chunk of a pack's zip. Chunks of a pack arrive in order, so + * {@code offset} only lets the client check that nothing went missing. + * + * @param total length of the whole zip, as in the {@link PackListPacket} + */ +public record PackDataPacket(String hash, int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_data"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackDataPacket::encode, PackDataPacket::decode); + + /** Well under the 1 MiB limit of a clientbound custom payload. */ + public static final int CHUNK_SIZE = 128 * 1024; + + public static void encode(PackDataPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.hash, 64); + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackDataPacket decode(FriendlyByteBuf buf) { + return new PackDataPacket(buf.readUtf(64), buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackDataPacket msg) { + ClientAccess.packs().onPackData(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 4e3ebf2fc33e94269f1b75ca1b81c17b3cd847da Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:02 +0800 Subject: [PATCH 1027/1461] Add network/PackListPacket.java to the 1.21.5 shared sources --- .../mcheli/network/PackListPacket.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java new file mode 100644 index 00000000..ad7f0c85 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +import java.util.ArrayList; +import java.util.List; + +/** + * Server to player: the content packs the server uses, in load order. Sent on join and after + * {@code /mcheli reload}. The client requests the packs it has not cached ({@link PackRequestPacket}) + * and then rebuilds its content from exactly this list, so both sides fly the same definitions. + * The imported 1.0.3 content is never listed: every player imports their own copy. + */ +public record PackListPacket(List packs) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_list"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackListPacket::encode, PackListPacket::decode); + + public static final int MAX_PACKS = 256; + + /** + * @param size length of the pack's zip in bytes, which is what {@link PackDataPacket} sends + */ + public record Entry(String id, String version, String hash, int size) { + } + + public static void encode(PackListPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.packs.size()); + for (Entry entry : msg.packs) { + buf.writeUtf(entry.id, 64); + buf.writeUtf(entry.version, 32); + buf.writeUtf(entry.hash, 64); + buf.writeVarInt(entry.size); + } + } + + public static PackListPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > MAX_PACKS) { + throw new DecoderException("Too many content packs: " + count); + } + List packs = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + packs.add(new Entry(buf.readUtf(64), buf.readUtf(32), buf.readUtf(64), buf.readVarInt())); + } + return new PackListPacket(List.copyOf(packs)); + } + + public static void handle(PackListPacket msg) { + ClientAccess.packs().onPackList(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From bd7373d66540c65ba9f856beb1199e8be6a80cbd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:02 +0800 Subject: [PATCH 1028/1461] Add network/PackRequestPacket.java to the 1.21.5 shared sources --- .../mcheli/network/PackRequestPacket.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java new file mode 100644 index 00000000..50f10f74 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.network; + +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Player to server: send me these packs, by content hash. The server only answers for hashes in + * the list it sent, so a client cannot ask for anything else. + */ +public record PackRequestPacket(List hashes) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_request"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackRequestPacket::encode, PackRequestPacket::decode); + + public static void encode(PackRequestPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.hashes.size()); + for (String hash : msg.hashes) { + buf.writeUtf(hash, 64); + } + } + + public static PackRequestPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > PackListPacket.MAX_PACKS) { + throw new DecoderException("Too many content packs requested: " + count); + } + List hashes = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + hashes.add(buf.readUtf(64)); + } + return new PackRequestPacket(List.copyOf(hashes)); + } + + public static void handle(PackRequestPacket msg, ServerPlayer sender) { + if (sender != null) { + PackSync.request(sender, msg.hashes); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 4acaea7ec21fa7ff6e8c90eeae962a2854f0890d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:03 +0800 Subject: [PATCH 1029/1461] Add network/PackSoundPacket.java to the 1.21.5 shared sources --- .../mcheli/network/PackSoundPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java new file mode 100644 index 00000000..8bac5034 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: play a sound a content pack brought, once, at a point. The game's own sound + * packet ends at the sound manager, which only knows the sounds in {@code sounds.json}, and a pack + * has none: it would be silent. See {@code ModSounds}. + */ +public record PackSoundPacket(String sound, double x, double y, double z, float volume, float pitch) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_sound"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSoundPacket::encode, PackSoundPacket::decode); + + private static final int MAX_NAME = 256; + + public static void encode(PackSoundPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.sound, MAX_NAME); + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.volume); + buf.writeFloat(msg.pitch); + } + + public static PackSoundPacket decode(FriendlyByteBuf buf) { + return new PackSoundPacket(buf.readUtf(MAX_NAME), buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readFloat()); + } + + public static void handle(PackSoundPacket msg) { + ClientAccess.packs().onPackSound(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From ca2770314a018cb501c787350d2828d6c9481368 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:03 +0800 Subject: [PATCH 1030/1461] Add network/PackSubmissions.java to the 1.21.5 shared sources --- .../mcheli/network/PackSubmissions.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java new file mode 100644 index 00000000..1bc4d037 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java @@ -0,0 +1,185 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.Util; +import org.jspecify.annotations.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Players offering a server the vehicles they built, for its operators to approve. + *

+ *  Submit in the workshop   ── PackSubmitPacket(chunk) ──▶  until the whole zip is here
+ *  checked off the main thread, kept in mcheli/pending, operators told
+ *  /mcheli approve <pack>    moves it to mcheli/packs and reloads, so every player gets it
+ * 
+ * Nothing a client sends is trusted. Submissions are off until the server's owner turns them on, one + * zip can be no larger than the server allows for a pack, a player sends one at a time with a pause + * in between, chunks out of order end the transfer, and a few transfers run at once at most. The + * bytes stay in memory only while they arrive; {@link PendingPacks} checks them as it would any pack. + * + *

Everything here runs on the server thread except the check, which reads the whole pack. + */ +public final class PackSubmissions { + private static final int MAX_UPLOADS = 4; + private static final long COOLDOWN_MILLIS = 30_000L; + + private static final Map UPLOADS = new HashMap<>(); + private static final Map LAST_SUBMISSION = new HashMap<>(); + + private PackSubmissions() { + } + + static void receive(ServerPlayer player, PackSubmitPacket packet) { + UUID id = player.getUUID(); + if (packet.offset() == 0) { + UPLOADS.remove(id); + Component refusal = refusal(player, packet.total()); + if (refusal != null) { + player.sendSystemMessage(refusal); + return; + } + UPLOADS.put(id, new ByteArrayOutputStream()); + } + ByteArrayOutputStream upload = UPLOADS.get(id); + // No upload means it was refused above and the rest of its chunks are still on their way. + if (upload == null) { + return; + } + long end = (long) upload.size() + packet.data().length; + if (packet.offset() != upload.size() || packet.total() > ServerConfig.maxPackBytes() || end > packet.total()) { + UPLOADS.remove(id); + player.sendSystemMessage(Component.translatable("message.mcheli.submit.failed", "chunks arrived out of order")); + return; + } + upload.write(packet.data(), 0, packet.data().length); + if (end == packet.total()) { + UPLOADS.remove(id); + LAST_SUBMISSION.put(id, System.currentTimeMillis()); + check(player, upload.toByteArray()); + } + } + + private static @Nullable Component refusal(ServerPlayer player, int total) { + if (!ServerConfig.allowSubmissions()) { + return Component.translatable("message.mcheli.submit.off"); + } + if (total <= 0 || total > ServerConfig.maxPackBytes()) { + return Component.translatable("message.mcheli.submit.too_large", ServerConfig.maxPackBytes() >> 20); + } + if (UPLOADS.size() >= MAX_UPLOADS) { + return Component.translatable("message.mcheli.submit.busy"); + } + long now = System.currentTimeMillis(); + LAST_SUBMISSION.values().removeIf(time -> now - time >= COOLDOWN_MILLIS); + return LAST_SUBMISSION.containsKey(player.getUUID()) ? Component.translatable("message.mcheli.submit.busy") : null; + } + + private static void check(ServerPlayer player, byte[] zip) { + MinecraftServer server = player.level().getServer(); + UUID id = player.getUUID(); + String name = player.getScoreboardName(); + CompletableFuture.supplyAsync(() -> { + try { + return MCHeli.packs().pending().store(zip, name, id); + } catch (IOException | PackException | RuntimeException e) { + throw new java.util.concurrent.CompletionException(e); + } + }, Util.nonCriticalIoPool()).whenComplete((submission, failure) -> server.execute(() -> { + if (failure != null) { + Throwable cause = failure.getCause() != null ? failure.getCause() : failure; + MCHeli.LOGGER.info("{} sent a pack that was refused: {}", name, cause.toString()); + tell(server, id, Component.translatable("message.mcheli.submit.failed", cause.getMessage() != null ? cause.getMessage() : cause.toString())); + } else { + arrived(server, submission); + } + })); + } + + private static void arrived(MinecraftServer server, PendingPacks.Submission submission) { + MCHeli.LOGGER.info("{} submitted content pack {} {} ({})", submission.submitter(), submission.id(), + submission.manifest().version(), ContentPack.shortHash(submission.hash())); + // Even with approval off, a pack taking over one that is in use waits for a person. + boolean replaces = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (!ServerConfig.requireApproval() && !replaces) { + try { + install(server, submission); + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not install submitted content pack {}", submission.id(), e); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.failed", e.getMessage())); + } + return; + } + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.waiting", submission.id())); + Component note = Component.translatable("message.mcheli.submit.to_operators", submission.submitter(), submission.id(), submission.manifest().version()); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (PackSync.seesProblems(player)) { + player.sendSystemMessage(note); + } + } + } + + /** + * Puts a waiting pack in use: into {@code mcheli/packs}, then a reload that sends every player + * the new list. + * + * @return whether the pack is in use now; the server's allow list or size limits may leave it out + */ + public static boolean install(MinecraftServer server, PendingPacks.Submission submission) throws IOException, PackException { + MCHeli.packs().pending().install(submission, MCHeli.packs().folder()); + ContentReload.reload(server); + MCHeli.LOGGER.info("Content pack {} from {} approved", submission.id(), submission.submitter()); + boolean live = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (live) { + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.approved", submission.id())); + } + return live; + } + + /** Throws a waiting pack away and tells the player who sent it, if they are here. */ + public static void reject(MinecraftServer server, PendingPacks.Submission submission) throws IOException { + MCHeli.packs().pending().remove(submission); + MCHeli.LOGGER.info("Content pack {} from {} rejected", submission.id(), submission.submitter()); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.rejected", submission.id())); + } + + private static void tell(MinecraftServer server, UUID player, Component message) { + ServerPlayer online = server.getPlayerList().getPlayer(player); + if (online != null) { + online.sendSystemMessage(message); + } + } + + /** An operator joining hears what is waiting for them. */ + static void onLogin(ServerPlayer player) { + if (!PackSync.seesProblems(player)) { + return; + } + try { + List waiting = MCHeli.packs().pending().list(); + if (!waiting.isEmpty()) { + player.sendSystemMessage(Component.translatable("message.mcheli.submit.waiting_for_you", waiting.size())); + } + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not list the submitted content packs", e); + } + } + + static void onLogout(ServerPlayer player) { + UPLOADS.remove(player.getUUID()); + } +} From 1ba62cf66862daad3449a229b3cf6b805ed09e87 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:04 +0800 Subject: [PATCH 1031/1461] Add network/PackSubmitPacket.java to the 1.21.5 shared sources --- .../mcheli/network/PackSubmitPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java new file mode 100644 index 00000000..d142e700 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Player to server: one chunk of a pack the player made, offered to the server's operators. See + * {@link PackSubmissions}, which treats all of this as untrusted. + * + * @param offset where this chunk goes; 0 starts a new submission + * @param total length of the whole zip + */ +public record PackSubmitPacket(int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_submit"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSubmitPacket::encode, PackSubmitPacket::decode); + + /** Under the 32 KiB limit of a serverbound custom payload, which is far lower than the other way. */ + public static final int CHUNK_SIZE = 30 * 1024; + + public static void encode(PackSubmitPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackSubmitPacket decode(FriendlyByteBuf buf) { + return new PackSubmitPacket(buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackSubmitPacket msg, ServerPlayer sender) { + PackSubmissions.receive(sender, msg); + } + + @Override + public Type type() { + return TYPE; + } +} From cdc7cecf531756b03c3cd9f0bbf453a34c7c6232 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:04 +0800 Subject: [PATCH 1032/1461] Add network/PackSync.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/network/PackSync.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSync.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSync.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSync.java new file mode 100644 index 00000000..e522f525 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PackSync.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * The server half of the content pack sync. + *

+ *  player joins          ── PackListPacket(id, version, hash, size per pack) ──▶ client
+ *                        ◀── PackRequestPacket(hashes it has not cached) ────────
+ *  every tick            ── PackDataPacket(chunk) ──▶  until each pack is sent
+ * 
+ * Each player gets {@link #CHUNKS_PER_TICK} chunks a tick (about 5 MB a second), one pack after + * the other, so a download never crowds out the game's own packets. A pack's bytes are read when + * its turn comes and dropped once sent. Everything here runs on the server thread. + */ +public final class PackSync { + private static final int CHUNKS_PER_TICK = 2; + + private static final Map> TRANSFERS = new HashMap<>(); + + private PackSync() { + } + + /** Sends a joining player the pack list, and operators any pack problems. */ + public static void onLogin(ServerPlayer player) { + ModNetwork.sendToPlayer(player, list()); + if (seesProblems(player)) { + MCHeli.packs().reportProblems(player::sendSystemMessage); + } + PackSubmissions.onLogin(player); + } + + public static void onLogout(ServerPlayer player) { + TRANSFERS.remove(player.getUUID()); + PackSubmissions.onLogout(player); + } + + /** The packs the server uses now, as the client should load them. */ + public static PackListPacket list() { + List entries = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + try { + entries.add(new PackListPacket.Entry(pack.id(), pack.manifest().version(), pack.hash(), (int) pack.archiveSize())); + } catch (IOException e) { + MCHeli.LOGGER.error("Content pack {} cannot be sent to players", pack, e); + } + } + return new PackListPacket(List.copyOf(entries)); + } + + /** After a reload: drops transfers in progress and sends every player the new list. */ + public static void sendListToAll(MinecraftServer server) { + TRANSFERS.clear(); + ModNetwork.sendToAll(server, list()); + } + + /** Operators, and the owner of a single player or LAN world, are told what is wrong with the packs. */ + public static boolean seesProblems(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + return server.isSingleplayerOwner(player.getGameProfile()) + || player.createCommandSourceStack().hasPermission(Commands.LEVEL_GAMEMASTERS); + } + + static void request(ServerPlayer player, List hashes) { + Deque queue = TRANSFERS.computeIfAbsent(player.getUUID(), id -> new ArrayDeque<>()); + for (String hash : hashes) { + ContentPack pack = find(hash); + if (pack == null) { + MCHeli.LOGGER.debug("{} asked for content pack {}, which the server does not use", player.getScoreboardName(), hash); + continue; + } + if (queue.stream().noneMatch(t -> t.pack == pack)) { + queue.add(new Transfer(pack)); + } + } + } + + private static @Nullable ContentPack find(String hash) { + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.hash().equals(hash)) { + return pack; + } + } + return null; + } + + public static void pump(MinecraftServer server) { + Iterator>> it = TRANSFERS.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey()); + Deque queue = entry.getValue(); + if (player == null || queue.isEmpty()) { + it.remove(); + continue; + } + for (int sent = 0; sent < CHUNKS_PER_TICK && !queue.isEmpty(); sent++) { + Transfer transfer = queue.peek(); + PackDataPacket chunk = transfer.next(); + if (chunk == null) { + queue.poll(); + continue; + } + ModNetwork.sendToPlayer(player, chunk); + if (transfer.done()) { + queue.poll(); + } + } + } + } + + /** One pack on its way to one player. */ + private static final class Transfer { + private final ContentPack pack; + private byte @Nullable [] data; + private int offset; + + Transfer(ContentPack pack) { + this.pack = pack; + } + + /** The next chunk, or {@code null} when the pack could not be read. */ + @Nullable PackDataPacket next() { + if (data == null) { + try { + data = pack.archive(); + } catch (IOException e) { + MCHeli.LOGGER.error("Could not read content pack {} to send it", pack, e); + return null; + } + } + int end = Math.min(offset + PackDataPacket.CHUNK_SIZE, data.length); + PackDataPacket chunk = new PackDataPacket(pack.hash(), offset, data.length, Arrays.copyOfRange(data, offset, end)); + offset = end; + return chunk; + } + + boolean done() { + return data != null && offset >= data.length; + } + } +} From 803da588455f77c8d30e246140457dba33e1b5e2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:04 +0800 Subject: [PATCH 1033/1461] Add network/PilotStatePacket.java to the 1.21.5 shared sources --- .../mcheli/network/PilotStatePacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java new file mode 100644 index 00000000..6426f060 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Sent by the pilot every tick. Position, yaw and pitch already travel through the vanilla + * vehicle move packet; this carries the parts vanilla does not know about. + * + * @param keys the pilot's key state as packed by {@link AircraftEntity.ControlInput#write()}, so + * other players see wheels steer and tracks turn + */ +public record PilotStatePacket(int entityId, float throttle, float reverse, float roll, boolean hovering, byte keys) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pilot_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PilotStatePacket::encode, PilotStatePacket::decode); + + public static void encode(PilotStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeFloat(msg.throttle); + buf.writeFloat(msg.reverse); + buf.writeFloat(msg.roll); + buf.writeBoolean(msg.hovering); + buf.writeByte(msg.keys); + } + + public static PilotStatePacket decode(FriendlyByteBuf buf) { + return new PilotStatePacket(buf.readVarInt(), buf.readFloat(), buf.readFloat(), buf.readFloat(), buf.readBoolean(), buf.readByte()); + } + + public static void handle(PilotStatePacket msg, ServerPlayer sender) { + if (sender == null) { + return; + } + // Only the player actually flying this aircraft may drive it. + if (sender.getVehicle() instanceof AircraftEntity aircraft + && aircraft.getId() == msg.entityId + && aircraft.getControllingPassenger() == sender + && Float.isFinite(msg.throttle) + && Float.isFinite(msg.reverse) + && Float.isFinite(msg.roll)) { + aircraft.acceptPilotState(msg.throttle, msg.reverse, msg.roll, msg.hovering, msg.keys); + } + } + + @Override + public Type type() { + return TYPE; + } +} From dcd91d1205d302b9a6d8370f03d31c9cfb9950f8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:05 +0800 Subject: [PATCH 1034/1461] Add network/SpotPacket.java to the 1.21.5 shared sources --- .../mcheli/network/SpotPacket.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java new file mode 100644 index 00000000..1ee72286 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.weapon.Spotting; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; + +/** + * The attack key through a rangefinder, {@code MCH_PacketIndSpotEntity}: spot players and + * vehicles, spot mobs, or mark a point for the team. Spotting uses a charge; marking is free. + */ +public record SpotPacket(Mode mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("spot"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(SpotPacket::encode, SpotPacket::decode); + + /** The rangefinder's modes, in the order its mode key steps through them. */ + public enum Mode { + PLAYERS_AND_VEHICLES(Spotting.PLAYERS | Spotting.GROUND_VEHICLES | Spotting.HELICOPTERS | Spotting.PLANES), + MOBS(Spotting.OTHERS | Spotting.MONSTERS), + MARK_POINT(0); + + final int filter; + + Mode(int filter) { + this.filter = filter; + } + } + + public static void encode(SpotPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.mode.ordinal()); + } + + public static SpotPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Mode[] modes = Mode.values(); + if (ordinal < 0 || ordinal >= modes.length) { + throw new IllegalArgumentException("Unknown spot mode " + ordinal); + } + return new SpotPacket(modes[ordinal]); + } + + public static void handle(SpotPacket msg, ServerPlayer sender) { + if (sender == null || !RangefinderItem.canUse(sender)) { + return; + } + ItemStack stack = sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + stack = sender.getItemInHand(InteractionHand.OFF_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + return; + } + } + boolean done; + if (msg.mode == Mode.MARK_POINT) { + done = Spotting.markPoint(sender); + } else if (RangefinderItem.charges(stack) > 0) { + if (!sender.getAbilities().instabuild) { + // An empty rangefinder stays: it only needs redstone, unlike a worn out tool. + stack.setDamageValue(stack.getDamageValue() + 1); + } + done = Spotting.spot(sender, msg.mode.filter); + } else { + done = false; + } + ModSounds.playAt(sender, done ? "pi" : "ng", 1.0F, 1.0F); + } + + @Override + public Type type() { + return TYPE; + } +} From 90e6e9d0c7dc7ac4818690cf731bc9f2b133480f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:05 +0800 Subject: [PATCH 1035/1461] Add network/TeamPacket.java to the 1.21.5 shared sources --- .../mcheli/network/TeamPacket.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java new file mode 100644 index 00000000..14b2db42 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A button of the team manager, {@code MCH_PacketIndMultiplayCommand}. Operators only; the + * server checks again. + * + * @param name the new team's name, for {@link Action#CREATE_TEAM} + * @param color the new team's colour, as a vanilla team colour id + */ +public record TeamPacket(Action action, String name, int color, boolean friendlyFire) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("team"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TeamPacket::encode, TeamPacket::decode); + + public enum Action { + PVP, + SHUFFLE, + CREATE_TEAM, + JUMP_TO_SPAWN, + DESTROY_ALL + } + + public TeamPacket(Action action) { + this(action, "", 0, false); + } + + public static void encode(TeamPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeUtf(msg.name, 16); + buf.writeVarInt(msg.color); + buf.writeBoolean(msg.friendlyFire); + } + + public static TeamPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown team action " + ordinal); + } + return new TeamPacket(actions[ordinal], buf.readUtf(16), buf.readVarInt(), buf.readBoolean()); + } + + public static void handle(TeamPacket msg, ServerPlayer sender) { + if (sender == null || !TeamManager.mayManage(sender)) { + return; + } + switch (msg.action) { + case PVP -> TeamManager.togglePvp(sender); + case SHUFFLE -> TeamManager.shuffle(sender); + case CREATE_TEAM -> TeamManager.create(sender, msg.name, msg.color, msg.friendlyFire); + case JUMP_TO_SPAWN -> TeamManager.jumpToSpawn(sender); + case DESTROY_ALL -> TeamManager.destroyAll(sender); + } + } + + @Override + public Type type() { + return TYPE; + } +} From bcc24de5cbbdacc0ce8fbee24d85e4ab095192d4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:06 +0800 Subject: [PATCH 1036/1461] Add network/TestFlightPacket.java to the 1.21.5 shared sources --- .../mcheli/network/TestFlightPacket.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java new file mode 100644 index 00000000..db12a390 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.workshop.TestFlight; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** The workshop's Test flight button: fly the vehicle just saved. The server checks who is asking. */ +public record TestFlightPacket(String vehicle) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("test_flight"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TestFlightPacket::encode, TestFlightPacket::decode); + private static final int MAX_NAME = 64; + + public static void encode(TestFlightPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.vehicle, MAX_NAME); + } + + public static TestFlightPacket decode(FriendlyByteBuf buf) { + return new TestFlightPacket(buf.readUtf(MAX_NAME)); + } + + public static void handle(TestFlightPacket msg, ServerPlayer sender) { + if (sender != null) { + TestFlight.start(sender, msg.vehicle); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 6ceddc3a58eabdf20fc9cf91c10f8c826cf23824 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:06 +0800 Subject: [PATCH 1037/1461] Add network/WeaponStatePacket.java to the 1.21.5 shared sources --- .../mcheli/network/WeaponStatePacket.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java new file mode 100644 index 00000000..292f643e --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to crew: the magazine and reserve of one weapon after it fired, reloaded or was + * resupplied. The original's {@code MCH_PacketNotifyAmmoNum}. + */ +public record WeaponStatePacket(int entityId, int weapon, int ammo, int rest) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("weapon_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(WeaponStatePacket::encode, WeaponStatePacket::decode); + + public static void encode(WeaponStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.ammo); + buf.writeVarInt(msg.rest); + } + + public static WeaponStatePacket decode(FriendlyByteBuf buf) { + return new WeaponStatePacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(WeaponStatePacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player != null && player.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.applyAmmo(msg.weapon, msg.ammo, msg.rest); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 5d19fbb587ab3487dddffec4994c3f0d32ff8f87 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:07 +0800 Subject: [PATCH 1038/1461] Add particle/GlowParticleOptions.java to the 1.21.5 shared sources --- .../mcheli/particle/GlowParticleOptions.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java new file mode 100644 index 00000000..63a3577f --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A soft ball of light blended on top of the scene: the flash of an explosion, a burning wreck, an + * ember. Spawned on the client only, but the codecs let {@code /particle mcheli:glow} show it too. + * + * @param color RGB of the light + * @param size radius in blocks at its largest + * @param lifetime ticks until it has faded out + * @param flicker whether it wavers like a fire rather than fading smoothly like a flash + */ +public record GlowParticleOptions(int color, float size, int lifetime, boolean flicker) implements ParticleOptions { + /** White hot at the centre of a blast. */ + public static final int FLASH = 0xFFE2B0; + /** Burning fuel and the fire inside the smoke. */ + public static final int FIRE = 0xFF8A2E; + /** Embers and the deeper red at the edge of a fireball. */ + public static final int EMBER = 0xFF5A1E; + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.RGB_COLOR_CODEC.fieldOf("color").forGetter(GlowParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(GlowParticleOptions::size), + ExtraCodecs.POSITIVE_INT.fieldOf("lifetime").forGetter(GlowParticleOptions::lifetime), + Codec.BOOL.optionalFieldOf("flicker", false).forGetter(GlowParticleOptions::flicker)) + .apply(i, GlowParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, GlowParticleOptions::color, + ByteBufCodecs.FLOAT, GlowParticleOptions::size, + ByteBufCodecs.VAR_INT, GlowParticleOptions::lifetime, + ByteBufCodecs.BOOL, GlowParticleOptions::flicker, + GlowParticleOptions::new); + + public static GlowParticleOptions flash(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, false); + } + + public static GlowParticleOptions fire(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, true); + } + + @Override + public ParticleType getType() { + return ModParticles.GLOW.get(); + } +} From f92d0960b6a439066933f905da3c8fede2136683 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:07 +0800 Subject: [PATCH 1039/1461] Add particle/SmokeParticleOptions.java to the 1.21.5 shared sources --- .../mcheli/particle/SmokeParticleOptions.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java new file mode 100644 index 00000000..29a0b0ef --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java @@ -0,0 +1,42 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A puff of coloured smoke that swells and pales as it drifts, {@code MCH_EntityParticleSmoke} + * with {@code toWhite} set. The smoke grenades use it to mark a spot. + * + * @param color RGB the puff starts with + * @param size the original's particle size; a puff ends up about {@code size / 5} blocks across + * @param lifetime ticks the puff lasts, or 0 for the grenade smoke's three to five seconds + */ +public record SmokeParticleOptions(int color, float size, int lifetime) implements ParticleOptions { + public SmokeParticleOptions(int color, float size) { + this(color, size, 0); + } + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + ExtraCodecs.RGB_COLOR_CODEC.fieldOf("color").forGetter(SmokeParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(SmokeParticleOptions::size), + ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("lifetime", 0).forGetter(SmokeParticleOptions::lifetime)) + .apply(i, SmokeParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, SmokeParticleOptions::color, + ByteBufCodecs.FLOAT, SmokeParticleOptions::size, + ByteBufCodecs.VAR_INT, SmokeParticleOptions::lifetime, + SmokeParticleOptions::new); + + @Override + public ParticleType getType() { + return ModParticles.SMOKE.get(); + } +} From f64d57f06b50b935d2ca322e0a844eff693ea129 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:08 +0800 Subject: [PATCH 1040/1461] Add platform/Platform.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/platform/Platform.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Platform.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Platform.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Platform.java new file mode 100644 index 00000000..22892ad7 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Platform.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.function.BiFunction; + +/** + * What the shared code needs from the mod loader. Each loader module installs its implementation + * first thing during mod construction, before anything else of ours runs. + * + *

Kept small on purpose: registration and events go the other way, with the loader module + * calling into the shared code (see {@code Registration} and the {@code *Hooks} classes). + */ +public interface Platform { + + static Platform get() { + Platform platform = Holder.instance; + if (platform == null) { + throw new IllegalStateException("No mod loader platform installed yet"); + } + return platform; + } + + static void install(Platform platform) { + if (Holder.instance != null) { + throw new IllegalStateException("Platform already installed: " + Holder.instance.loaderName()); + } + Holder.instance = Objects.requireNonNull(platform); + } + + /** {@code forge}, {@code neoforge} or {@code fabric}, for logs and {@code /mcheli status}. */ + String loaderName(); + + /** The game directory, which holds the {@code mcheli} folder. */ + Path gameDir(); + + /** The config directory, which holds the client config. */ + Path configDir(); + + /** Our version as the loader read it from the mod metadata. */ + String modVersion(); + + /** Whether this is a physical client, as opposed to a dedicated server. */ + boolean isClient(); + + void sendToServer(CustomPacketPayload payload); + + void sendToPlayer(ServerPlayer player, CustomPacketPayload payload); + + /** + * A builder for one of our creative tabs, placed after {@code after}. Vanilla's builder is not + * public on every loader and only Forge and NeoForge can order tabs. + */ + CreativeModeTab.Builder creativeTab(ResourceKey after); + + /** A block entity type; vanilla's constructor is private on 1.21.5. */ + BlockEntityType blockEntityType(BiFunction factory, Block block); + + final class Holder { + private static volatile Platform instance; + + private Holder() { + } + } +} From 3de495195c37dfef4d8fc84e3cc94c9e19e96656 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:08 +0800 Subject: [PATCH 1041/1461] Add platform/Registered.java to the 1.21.5 shared sources --- .../mcheli/platform/Registered.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Registered.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Registered.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Registered.java new file mode 100644 index 00000000..e90f274f --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Registered.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Supplier; + +/** + * A registry entry the shared code declares and the loader module registers, like Forge's + * {@code RegistryObject}. {@link #get()} works once the loader has registered it. + */ +public final class Registered implements Supplier { + private final ResourceLocation id; + private volatile T value; + + Registered(ResourceLocation id) { + this.id = id; + } + + public ResourceLocation id() { + return id; + } + + @Override + public T get() { + T v = value; + if (v == null) { + throw new IllegalStateException("Registry entry " + id + " is not registered yet"); + } + return v; + } + + void bind(T value) { + this.value = value; + } +} From 137956367d99eddccc7e44dc0caf1b5a950db503 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:08 +0800 Subject: [PATCH 1042/1461] Add platform/Registration.java to the 1.21.5 shared sources --- .../mcheli/platform/Registration.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Registration.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Registration.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Registration.java new file mode 100644 index 00000000..67d899aa --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/platform/Registration.java @@ -0,0 +1,80 @@ +package com.jnhprojects.mcheli.platform; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.ResourceKey; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Every vanilla registry entry the mod adds. The shared registry classes declare their entries + * here during mod construction; the loader module then registers them its own way: Forge and + * NeoForge from their register event, Fabric straight away. + */ +public final class Registration { + private static final List> PENDING = new ArrayList<>(); + private static final Set>> REGISTRIES = new LinkedHashSet<>(); + + private Registration() { + } + + /** + * Declares an entry. + * + * @param factory makes the value; it gets the entry's key for {@code Item.Properties#setId} + * and the like + */ + public static synchronized Registered register(ResourceKey> registry, String name, + Function, T> factory) { + ResourceLocation id = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, name); + Registered entry = new Registered<>(id); + PENDING.add(new Pending<>(registry, entry, factory)); + REGISTRIES.add(registry); + return entry; + } + + /** The registries with entries of ours, in the order they were first used. */ + public static synchronized List>> registries() { + return List.copyOf(REGISTRIES); + } + + /** + * A key from {@link #registries()} with its element type filled in, for the loader's generic + * register calls. The type is whatever the caller needs; entries are only ever handed to the + * registry they were declared for. + */ + @SuppressWarnings("unchecked") + public static ResourceKey> typed(ResourceKey> registry) { + return (ResourceKey>) registry; + } + + /** + * Creates our entries for one registry, in declaration order, and hands each to the loader. + * Called once per registry. + */ + public static synchronized void registerAll(ResourceKey> registry, BiConsumer sink) { + for (Pending pending : PENDING) { + if (pending.registry.equals(registry)) { + @SuppressWarnings("unchecked") + Pending ours = (Pending) pending; + ours.create(sink); + } + } + } + + private record Pending(ResourceKey> registry, Registered entry, + Function, T> factory) { + @SuppressWarnings("unchecked") + void create(BiConsumer sink) { + T value = factory.apply(ResourceKey.create((ResourceKey>) registry, entry.id())); + entry.bind(value); + sink.accept(entry.id(), value); + } + } +} From ead5203a366e14095e16467c0e40124843d25964 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:09 +0800 Subject: [PATCH 1043/1461] Add registry/ModBlockEntities.java to the 1.21.5 shared sources --- .../mcheli/registry/ModBlockEntities.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java new file mode 100644 index 00000000..00d40aed --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java @@ -0,0 +1,21 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; + +/** Our block entities: the drafting table's, which draws its model. */ +public final class ModBlockEntities { + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.BLOCK_ENTITY_TYPE, + "drafting_table", key -> Platform.get().blockEntityType(DraftingTableBlockEntity::new, ModBlocks.DRAFTING_TABLE.get())); + + private ModBlockEntities() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 601c2e67f37249ba5e2c027306960dc46a15d430 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:09 +0800 Subject: [PATCH 1044/1461] Add registry/ModBlocks.java to the 1.21.5 shared sources --- .../mcheli/registry/ModBlocks.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java new file mode 100644 index 00000000..19995a92 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockBehaviour; + +/** Our blocks: the drafting table, lit or not. */ +public final class ModBlocks { + public static final Registered DRAFTING_TABLE = Registration.register(Registries.BLOCK, "drafting_table", + key -> new DraftingTableBlock(BlockBehaviour.Properties.of().setId(key) + .strength(0.2F) + .sound(SoundType.METAL) + .noOcclusion() + .lightLevel(state -> state.getValue(DraftingTableBlock.LIT) ? 15 : 0))); + + private ModBlocks() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From a38e32a3ebe8924d652498a3bc6ff50bec65935f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:10 +0800 Subject: [PATCH 1045/1461] Add registry/ModCreativeTabs.java to the 1.21.5 shared sources --- .../mcheli/registry/ModCreativeTabs.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java new file mode 100644 index 00000000..870aa8b8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java @@ -0,0 +1,72 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** + * One tab per vehicle kind, listing a stack for each loaded definition of that kind, and one for + * the equipment and throwables. Those lists are read from the content registry whenever a tab is + * built, so reloaded content shows up. + */ +public final class ModCreativeTabs { + public static final Registered HELICOPTERS = register("helicopters", "helicopter", "ah-64", + CreativeModeTabs.SPAWN_EGGS); + public static final Registered PLANES = register("planes", "plane", "f-35b", key(HELICOPTERS)); + public static final Registered TANKS = register("tanks", "tank", "m1a2", key(PLANES)); + public static final Registered VEHICLES = register("vehicles", "vehicle", "mk15", key(TANKS)); + public static final Registered EQUIPMENT = Registration.register(Registries.CREATIVE_MODE_TAB, "equipment", + key -> Platform.get().creativeTab(key(VEHICLES)) + .title(Component.translatable("itemGroup.mcheli.equipment")) + .icon(() -> new ItemStack(ModItems.WRENCH.get())) + .displayItems((params, output) -> { + ModItems.EQUIPMENT.forEach(item -> output.accept(item.get())); + MCHeli.content().throwables().forEach(def -> output.accept(ThrowableItem.stackOf(def))); + }) + .build()); + + private ModCreativeTabs() { + } + + private static ResourceKey key(Registered tab) { + return ResourceKey.create(Registries.CREATIVE_MODE_TAB, tab.id()); + } + + private static Registered register(String name, String kind, String icon, ResourceKey after) { + return Registration.register(Registries.CREATIVE_MODE_TAB, name, key -> Platform.get().creativeTab(after) + .title(Component.translatable("itemGroup.mcheli." + name)) + .icon(() -> { + AircraftDefinition def = MCHeli.content().any(icon); + return def != null ? AircraftItem.stackOf(def) : new ItemStack(ModItems.forKind(kind)); + }) + .displayItems((params, output) -> { + for (AircraftDefinition def : MCHeli.content().all()) { + if (def.kind().equals(kind)) { + output.accept(AircraftItem.stackOf(def)); + } + } + }) + .build()); + } + + /** Our tabs, for rebuilding their contents after a content reload. */ + public static List all() { + return List.of(HELICOPTERS.get(), PLANES.get(), TANKS.get(), VEHICLES.get(), EQUIPMENT.get()); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 27548645685581e2e1d1e583768a0506f170674f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:10 +0800 Subject: [PATCH 1046/1461] Add registry/ModDataComponents.java to the 1.21.5 shared sources --- .../mcheli/registry/ModDataComponents.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java new file mode 100644 index 00000000..377a5783 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.Codec; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.codec.ByteBufCodecs; + +public final class ModDataComponents { + /** + * The definition a vehicle item places, such as {@code ah-64}. Saved with the stack and sent + * to clients; the item itself only says which kind of vehicle it is. + */ + public static final Registered> VEHICLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "vehicle", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + /** The definition a throwable item throws, such as {@code m18_red}. */ + public static final Registered> THROWABLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "throwable", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + private ModDataComponents() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 34f0e9e171368a0400e76249e25255c8e2042071 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:11 +0800 Subject: [PATCH 1047/1461] Add registry/ModEntities.java to the 1.21.5 shared sources --- .../mcheli/registry/ModEntities.java | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java new file mode 100644 index 00000000..b6686ccd --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java @@ -0,0 +1,170 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.entity.ContainerEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.RappelEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; + +/** + * One entity type per vehicle kind. The concrete vehicle is stored on the entity, the same approach + * the original took, so adding content never changes the entity registry. + */ +public final class ModEntities { + public static final Registered> HELICOPTER = register("helicopter", HelicopterEntity::new); + public static final Registered> PLANE = register("plane", PlaneEntity::new); + public static final Registered> TANK = register("tank", TankEntity::new); + public static final Registered> VEHICLE = register("vehicle", GroundVehicleEntity::new); + + public static final Registered> BULLET = projectile("bullet", BulletEntity::new); + public static final Registered> ROCKET = projectile("rocket", RocketEntity::new); + public static final Registered> BOMB = projectile("bomb", BombEntity::new); + public static final Registered> AA_MISSILE = projectile("aa_missile", AAMissileEntity::new); + public static final Registered> AS_MISSILE = projectile("as_missile", ASMissileEntity::new); + public static final Registered> AT_MISSILE = projectile("at_missile", ATMissileEntity::new); + public static final Registered> TV_MISSILE = projectile("tv_missile", TvMissileEntity::new); + public static final Registered> TORPEDO = projectile("torpedo", TorpedoEntity::new); + public static final Registered> MARKER_ROCKET = projectile("marker_rocket", MarkerRocketEntity::new); + public static final Registered> DISPENSED_ITEM = projectile("dispensed_item", DispensedItemEntity::new); + public static final Registered> FLARE = Registration.register(Registries.ENTITY_TYPE, "flare", key -> EntityType.Builder + .of(FlareEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(2) + .fireImmune() + .noSave() + .build(key)); + public static final Registered> A10 = Registration.register(Registries.ENTITY_TYPE, "a10", key -> EntityType.Builder + .of(A10Entity::new, MobCategory.MISC) + .sized(5.0F, 3.0F) + .clientTrackingRange(32) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + + public static final Registered> PARACHUTE = Registration.register(Registries.ENTITY_TYPE, "parachute", key -> EntityType.Builder + .of(ParachuteEntity::new, MobCategory.MISC) + .sized(1.5F, 0.6F) + .clientTrackingRange(10) + .updateInterval(1) + .build(key)); + + public static final Registered> THROWABLE = Registration.register(Registries.ENTITY_TYPE, "throwable", key -> EntityType.Builder + .of(ThrowableEntity::new, MobCategory.MISC) + .sized(0.25F, 0.25F) + .clientTrackingRange(8) + .updateInterval(2) + .noSave() + .build(key)); + + public static final Registered> GLTD = Registration.register(Registries.ENTITY_TYPE, "gltd", key -> EntityType.Builder + .of(GltdEntity::new, MobCategory.MISC) + .sized(0.5F, 0.5F) + .clientTrackingRange(10) + .updateInterval(3) + .build(key)); + + public static final Registered> CONTAINER = Registration.register(Registries.ENTITY_TYPE, "container", key -> EntityType.Builder + .of(ContainerEntity::new, MobCategory.MISC) + .sized(2.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key)); + + public static final Registered> CHAIN = Registration.register(Registries.ENTITY_TYPE, "chain", key -> EntityType.Builder + .of(ChainEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(2) + .fireImmune() + .build(key)); + + public static final Registered> RAPPEL = Registration.register(Registries.ENTITY_TYPE, "rappel", key -> EntityType.Builder + .of(RappelEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + + public static final Registered> UAV_STATION = Registration.register(Registries.ENTITY_TYPE, "uav_station", key -> EntityType.Builder + .of(UavStationEntity::new, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key)); + + private ModEntities() { + } + + /** Projectiles are a metre cube like the original's, and never saved. */ + private static Registered> projectile(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + } + + private static Registered> register(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .build(key)); + } + + /** The entity type that hosts a definition of this kind. */ + public static EntityType typeFor(AircraftDefinition def) { + if (def instanceof HelicopterDefinition) { + return HELICOPTER.get(); + } + if (def instanceof PlaneDefinition) { + return PLANE.get(); + } + if (def instanceof TankDefinition) { + return TANK.get(); + } + return VEHICLE.get(); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 27d8b41fb31e9f78f161beb2e8d5620d31484e6e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:11 +0800 Subject: [PATCH 1048/1461] Add registry/ModItems.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/registry/ModItems.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java new file mode 100644 index 00000000..e7415dfd --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java @@ -0,0 +1,102 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.item.LegacyAircraftItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.item.PlacedEntityItem; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.item.UavStationItem; +import com.jnhprojects.mcheli.item.WrenchItem; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; + +import java.util.List; +import java.util.Map; + +/** + * One item per vehicle kind; the stack says which vehicle (see {@link AircraftItem}). The 1.0.0 + * items, one per vehicle, are kept for the content present at startup so old worlds keep them. The + * equipment (fuel can, wrench, parachute and so on) has one item each, and the throwables share one. + */ +public final class ModItems { + public static final Registered HELICOPTER = kind("helicopter"); + public static final Registered PLANE = kind("plane"); + public static final Registered TANK = kind("tank"); + public static final Registered VEHICLE = kind("vehicle"); + + public static final Registered FUEL = Registration.register(Registries.ITEM, "fuel", key -> new FuelItem(new Item.Properties().setId(key))); + public static final Registered WRENCH = Registration.register(Registries.ITEM, "wrench", key -> new WrenchItem(new Item.Properties().setId(key))); + public static final Registered PARACHUTE = Registration.register(Registries.ITEM, "parachute", key -> new ParachuteItem(new Item.Properties().setId(key))); + public static final Registered GLTD = Registration.register(Registries.ITEM, "gltd", + key -> new PlacedEntityItem(ModEntities.GLTD, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered RANGEFINDER = Registration.register(Registries.ITEM, "rangefinder", key -> new RangefinderItem(new Item.Properties().setId(key))); + public static final Registered CONTAINER = Registration.register(Registries.ITEM, "container", + key -> new PlacedEntityItem(ModEntities.CONTAINER, new Item.Properties().setId(key))); + public static final Registered CHAIN = Registration.register(Registries.ITEM, "chain", key -> new ChainItem(new Item.Properties().setId(key))); + public static final Registered STINGER_MISSILE = Registration.register(Registries.ITEM, "fim92_bullet", key -> new Item(new Item.Properties().setId(key).stacksTo(16))); + public static final Registered STINGER = Registration.register(Registries.ITEM, "fim92", + key -> new LauncherItem("fim92", () -> ModItems.STINGER_MISSILE.get(), new Item.Properties().setId(key))); + public static final Registered JAVELIN_MISSILE = Registration.register(Registries.ITEM, "fgm148_bullet", key -> new Item(new Item.Properties().setId(key).stacksTo(16))); + public static final Registered JAVELIN = Registration.register(Registries.ITEM, "fgm148", + key -> new LauncherItem("fgm148", () -> ModItems.JAVELIN_MISSILE.get(), new Item.Properties().setId(key))); + public static final Registered UAV_STATION = Registration.register(Registries.ITEM, "uav_station", + key -> new UavStationItem(UavStationEntity.STATION, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered UAV_CONTROLLER = Registration.register(Registries.ITEM, "uav_station2", + key -> new UavStationItem(UavStationEntity.PORTABLE, new Item.Properties().setId(key).stacksTo(1))); + public static final Registered DRAFTING_TABLE = Registration.register(Registries.ITEM, "drafting_table", + key -> new BlockItem(ModBlocks.DRAFTING_TABLE.get(), new Item.Properties().setId(key).useBlockDescriptionPrefix())); + /** Every throwable; the stack says which (see {@link ThrowableItem}). */ + public static final Registered THROWABLE = Registration.register(Registries.ITEM, "throwable", key -> new ThrowableItem(new Item.Properties().setId(key))); + + /** The equipment tab, in the order it lists them. */ + public static final List> EQUIPMENT = List.of(FUEL, WRENCH, PARACHUTE, GLTD, RANGEFINDER, CONTAINER, CHAIN, + STINGER, STINGER_MISSILE, JAVELIN, JAVELIN_MISSILE, UAV_STATION, UAV_CONTROLLER, DRAFTING_TABLE); + + private static final Map> BY_KIND = + Map.of("helicopter", HELICOPTER, "plane", PLANE, "tank", TANK, "vehicle", VEHICLE); + + private ModItems() { + } + + private static Registered kind(String kind) { + return Registration.register(Registries.ITEM, kind, key -> new AircraftItem(kind, new Item.Properties().setId(key).stacksTo(1))); + } + + /** + * Declares the entries, with a 1.0.0 item for each original vehicle in {@code content}; the + * loader module registers them. + */ + public static void init(ContentRegistry content) { + for (AircraftDefinition def : content.all()) { + String name = def.name(); + // Only the imported content existed in 1.0.0; pack vehicles never had their own item. + if (!def.pack().equals(ContentSource.ORIGINAL) || BY_KIND.containsKey(name) || EQUIPMENT.stream().anyMatch(e -> e.id().getPath().equals(name))) { + // 1.0.0 could not have had such an item either: it would have clashed the same way. + continue; + } + String kind = def.kind(); + Registration.register(Registries.ITEM, name, key -> new LegacyAircraftItem(kind, name, + new Item.Properties().setId(key).stacksTo(1))); + } + } + + /** The generic item of a vehicle kind ({@code helicopter}, {@code plane}, {@code tank} or {@code vehicle}). */ + public static Item forKind(String kind) { + Registered item = BY_KIND.get(kind); + if (item == null) { + throw new IllegalArgumentException("Unknown vehicle kind " + kind); + } + return item.get(); + } +} From ad19ae160ba10b5113487501a1e9cf1b5da64135 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:12 +0800 Subject: [PATCH 1049/1461] Add registry/ModMenus.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/registry/ModMenus.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java new file mode 100644 index 00000000..0dac0052 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; + +/** Our screens with slots. The cargo chest uses vanilla's chest menus and needs no entry here. */ +public final class ModMenus { + public static final Registered> VEHICLE = Registration.register(Registries.MENU, "vehicle", + key -> new MenuType<>(VehicleMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.MENU, "drafting_table", + key -> new MenuType<>(DraftingTableMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> UAV_STATION = Registration.register(Registries.MENU, "uav_station", + key -> new MenuType<>(UavStationMenu::new, FeatureFlags.VANILLA_SET)); + + private ModMenus() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 44bfa7efdd1cf69b462f8003597b7aba85097bde Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:12 +0800 Subject: [PATCH 1050/1461] Add registry/ModParticles.java to the 1.21.5 shared sources --- .../mcheli/registry/ModParticles.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java new file mode 100644 index 00000000..4ed53acd --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java @@ -0,0 +1,48 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; + +public final class ModParticles { + /** Ignores the particle limit: an explosion that loses its flash to the cap looks broken. */ + public static final Registered> GLOW = Registration.register(Registries.PARTICLE_TYPE, "glow", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return GlowParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return GlowParticleOptions.STREAM_CODEC; + } + }); + + /** Smoke grenades: a marker that vanishes under the particle limit is no marker. */ + public static final Registered> SMOKE = Registration.register(Registries.PARTICLE_TYPE, "smoke", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return SmokeParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return SmokeParticleOptions.STREAM_CODEC; + } + }); + + private ModParticles() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From f7ab66130c712cf9ec5035a10847e299935079a3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:12 +0800 Subject: [PATCH 1051/1461] Add team/Markers.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/team/Markers.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/team/Markers.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/team/Markers.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/team/Markers.java new file mode 100644 index 00000000..4d869373 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/team/Markers.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.team; + +import net.minecraft.core.BlockPos; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * What this client's team has spotted, {@code MCH_GuiTargetMarker}: the spotted entities, each for + * as long as it was spotted, and the last marked point. Filled by {@code MarkerPacket} on the + * client's main thread and drawn by the HUD; a dedicated server never touches it. + */ +public final class Markers { + /** A marked point stays up this long, unless another replaces it. */ + public static final int POINT_TICKS = 60 * 20; + + private static final Map SPOTTED = new HashMap<>(); + private static @Nullable BlockPos point; + private static int pointTicks; + + private Markers() { + } + + public static void spotted(int[] entities, int ticks) { + for (int id : entities) { + SPOTTED.merge(id, ticks, Math::max); + } + } + + public static void point(BlockPos pos) { + point = pos; + pointTicks = POINT_TICKS; + } + + /** Entity ids and ticks left. */ + public static Map spotted() { + return SPOTTED; + } + + public static @Nullable BlockPos point() { + return point; + } + + public static void tick() { + SPOTTED.replaceAll((id, ticks) -> ticks - 1); + SPOTTED.values().removeIf(ticks -> ticks <= 0); + if (pointTicks > 0 && --pointTicks == 0) { + point = null; + } + } + + public static void clear() { + SPOTTED.clear(); + point = null; + pointTicks = 0; + } +} From 5310aaa1dc1ffdb25d75a3c4af08a18433f68eb8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:13 +0800 Subject: [PATCH 1052/1461] Switch the server's PVP setting on 1.21.5, which has no game rule for it --- .../jnhprojects/mcheli/team/TeamManager.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java new file mode 100644 index 00000000..4e6d4ae5 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java @@ -0,0 +1,112 @@ +package com.jnhprojects.mcheli.team; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.portal.TeleportTransition; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * The team manager's actions, {@code MCH_Multiplay} and {@code MCH_MultiplayPacketHandler}: for + * operators running a match. Teams are the vanilla scoreboard teams, so {@code /team} and the + * player list see the same ones. + */ +public final class TeamManager { + /** The colours a team can have. */ + public static final List COLORS = Arrays.stream(ChatFormatting.values()).filter(ChatFormatting::isColor).toList(); + + private TeamManager() { + } + + /** Operators only, as the original checked the scoreboard command's permission. */ + public static boolean mayManage(Player player) { + return player.hasPermissions(2); + } + + /** Switches the server's PVP setting until the next restart and says which way it went. */ + public static void togglePvp(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + boolean pvp = !server.isPvpAllowed(); + server.setPvpAllowed(pvp); + broadcast(server, Component.translatable(pvp ? "message.mcheli.team.pvp_on" : "message.mcheli.team.pvp_off")); + } + + /** {@code shuffleTeam}: every player online goes into one of the teams in turn, in random order. */ + public static void shuffle(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + Scoreboard scoreboard = server.getScoreboard(); + List teams = new ArrayList<>(scoreboard.getPlayerTeams()); + if (teams.isEmpty()) { + player.displayClientMessage(Component.translatable("message.mcheli.team.no_teams"), true); + return; + } + List players = new ArrayList<>(server.getPlayerList().getPlayers()); + Collections.shuffle(players); + Collections.shuffle(teams); + for (int i = 0; i < players.size(); i++) { + scoreboard.addPlayerToTeam(players.get(i).getScoreboardName(), teams.get(i % teams.size())); + } + broadcast(server, Component.translatable("message.mcheli.team.shuffled", teams.size())); + } + + /** The original's create team screen: a name, a colour and friendly fire. */ + public static void create(ServerPlayer player, String name, int color, boolean friendlyFire) { + Scoreboard scoreboard = player.level().getServer().getScoreboard(); + String trimmed = name.trim(); + if (trimmed.isEmpty() || trimmed.length() > 16 || trimmed.contains(" ") || scoreboard.getPlayerTeam(trimmed) != null) { + player.displayClientMessage(Component.translatable("message.mcheli.team.bad_name", trimmed), true); + return; + } + PlayerTeam team = scoreboard.addPlayerTeam(trimmed); + team.setColor(COLORS.get(Math.floorMod(color, COLORS.size()))); + team.setAllowFriendlyFire(friendlyFire); + player.displayClientMessage(Component.translatable("message.mcheli.team.created", trimmed), true); + } + + /** {@code jumpSpawnPoint}: every player back to where they would respawn. */ + public static void jumpToSpawn(ServerPlayer player) { + for (ServerPlayer target : List.copyOf(player.level().getServer().getPlayerList().getPlayers())) { + target.stopRiding(); + target.teleport(target.findRespawnPositionAndUseSpawnBlock(false, TeleportTransition.DO_NOTHING)); + } + } + + /** {@code destoryAllAircraft}: every loaded vehicle is gone, its crew left standing. */ + public static void destroyAll(ServerPlayer player) { + int count = 0; + for (ServerLevel level : player.level().getServer().getAllLevels()) { + for (AircraftEntity vehicle : level.getEntities(EntityTypeTest.forClass(AircraftEntity.class), e -> true)) { + vehicle.ejectPassengers(); + vehicle.discard(); + count++; + } + } + player.displayClientMessage(Component.translatable("message.mcheli.team.destroyed", count), true); + } + + /** {@code entity} and everyone on its team. */ + public static List teammates(ServerPlayer entity) { + List out = new ArrayList<>(); + for (ServerPlayer player : entity.level().getServer().getPlayerList().getPlayers()) { + if (player == entity || player.isAlliedTo(entity)) { + out.add(player); + } + } + return out; + } + + private static void broadcast(MinecraftServer server, Component message) { + server.getPlayerList().broadcastSystemMessage(message, false); + } +} From 08db87e8d2ef1971590aa75e3c1e34269950a353 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:13 +0800 Subject: [PATCH 1053/1461] Add weapon/CasStrike.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/weapon/CasStrike.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java new file mode 100644 index 00000000..e78278a8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java @@ -0,0 +1,125 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A close air support run, {@code MCH_WeaponCAS}: three A-10 passes over a spot, 45 ticks apart, + * the second and third offset to either side. Vehicles with a CAS weapon drive theirs from their + * {@link WeaponSet}; the GLTD keeps one of these. + */ +public final class CasStrike { + /** Ticks from the call to the first pass: the time it takes the A-10 to arrive. */ + public static final int FIRST_PASS = 50; + private static final int BETWEEN_PASSES = 45; + private static final int PASSES = 3; + private static final RandomSource RANDOM = RandomSource.create(); + + private final String weapon; + private double x; + private double y; + private double z; + private int direction; + private @Nullable Entity shooter; + private @Nullable Entity user; + private int passes = PASSES; + private int ticksToNext; + + /** @param weapon the CAS weapon definition the A-10s fire, such as {@code a10gau8} */ + public CasStrike(String weapon) { + this.weapon = weapon; + } + + /** Whether passes are still to come. */ + public boolean isRunning() { + return passes < PASSES; + } + + /** + * Calls the strike on {@code at}. The A-10 comes in from one side of the caller's view, + * picked at random, never straight from behind or towards them. + */ + public void start(Vec3 at, float callerYaw, @Nullable Entity shooter, @Nullable Entity user) { + x = at.x; + y = at.y; + z = at.z; + direction = Math.floorMod(approach(callerYaw), 4); + this.shooter = shooter; + this.user = user; + passes = 0; + ticksToNext = FIRST_PASS; + } + + /** The original's approach: the quarter the caller faces, turned one way or the other. */ + static int approach(float callerYaw) { + int facing = (int) Mth.positiveModulo(callerYaw + 45.0F, 360.0F) / 90; + return facing + (RANDOM.nextBoolean() ? -1 : 1); + } + + public void tick(ServerLevel level, WeaponDefinition def) { + if (passes >= PASSES || --ticksToNext > 0) { + return; + } + Vec3 offset = passOffset(passes, direction); + spawnA10(level, weapon, def, direction, x + offset.x, y + 20.0, z + offset.z, shooter, user); + passes++; + ticksToNext = BETWEEN_PASSES; + } + + /** The first pass is dead on; the second strays to one side of the track, the third to the other. */ + static Vec3 passOffset(int pass, int direction) { + if (pass == 0) { + return Vec3.ZERO; + } + double sign = pass == 1 ? 1.0 : -1.0; + if (direction % 2 == 0) { + return new Vec3(RANDOM.nextDouble() * 10.0 * sign, 0.0, (RANDOM.nextDouble() - 0.5) * 10.0); + } + return new Vec3((RANDOM.nextDouble() - 0.5) * 10.0, 0.0, RANDOM.nextDouble() * 10.0 * sign); + } + + /** One A-10, starting 90 blocks short of the spot and flying over it at three blocks a tick. */ + static void spawnA10(ServerLevel level, String weapon, WeaponDefinition def, int direction, double x, double y, double z, + @Nullable Entity shooter, @Nullable Entity user) { + double mx = 0.0; + double mz = 0.0; + switch (direction) { + case 0 -> { + mz = 3.0; + z -= 90.0; + } + case 1 -> { + mx = -3.0; + x += 90.0; + } + case 2 -> { + mz = -3.0; + z += 90.0; + } + default -> { + mx = 3.0; + x -= 90.0; + } + } + A10Entity a10 = ModEntities.A10.get().create(level, EntitySpawnReason.TRIGGERED); + if (a10 == null) { + return; + } + a10.setPos(x, y, z); + a10.setup(weapon, direction, mx, mz, shooter, user); + a10.power = def.power; + a10.explosionPower = def.explosion; + a10.acceleration = def.acceleration; + level.addFreshEntity(a10); + ModSounds.playAt(level, x, y, z, "a-10_snd", 150.0F, 1.0F); + } +} From 0dc98b72a19b282ee9b1671a57cb312ead9e10b3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:14 +0800 Subject: [PATCH 1054/1461] Add weapon/Combat.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/weapon/Combat.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java new file mode 100644 index 00000000..d94d3778 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +/** Damage rules shared by bullets and explosions. */ +public final class Combat { + private Combat() { + } + + /** The {@code DamageFactor} kind a target belongs to, or {@code null} for anything else. */ + public static @Nullable String kind(Entity entity) { + return switch (entity) { + case Player p -> "player"; + case HelicopterEntity h -> "helicopter"; + case PlaneEntity p -> "plane"; + case TankEntity t -> "tank"; + case GroundVehicleEntity v -> "vehicle"; + default -> null; + }; + } + + public static float factor(@Nullable WeaponDefinition def, Entity target) { + String kind = kind(target); + return def != null && kind != null ? def.damageFactor(kind) : 1.0F; + } + + /** + * Deals weapon damage. The original scaled every living target's invulnerability window by + * {@code HurtResistantTime}, zero by default, so each round of a burst lands. + */ + public static boolean hurt(Entity target, DamageSource source, float damage, @Nullable WeaponDefinition def) { + if (!(target.level() instanceof ServerLevel level)) { + return false; + } + if (target instanceof LivingEntity living) { + living.invulnerableTime = 0; + } + return target.hurtServer(level, source, damage * factor(def, target)); + } + + /** The convention tag every loader fills, so modded panes count too. */ + private static final TagKey GLASS_PANES = TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath("c", "glass_panes")); + + /** + * {@code BulletBreakableBlocks} with its default list: panes, grass, flowers, vines, wheat, + * sugar cane and lily pads, which a round shoots through. + */ + public static boolean isBulletBreakable(BlockState state) { + return state.is(GLASS_PANES) + || state.is(BlockTags.FLOWERS) + || state.is(Blocks.SHORT_GRASS) || state.is(Blocks.TALL_GRASS) + || state.is(Blocks.FERN) || state.is(Blocks.LARGE_FERN) + || state.is(Blocks.VINE) || state.is(Blocks.WHEAT) + || state.is(Blocks.SUGAR_CANE) || state.is(Blocks.LILY_PAD); + } +} From 599efdd678a66702abf6a5e7f802f0d4dc1caf7e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:14 +0800 Subject: [PATCH 1055/1461] Add weapon/ExplosionEffect.java to the 1.21.5 shared sources --- .../mcheli/weapon/ExplosionEffect.java | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java new file mode 100644 index 00000000..4f4b0dc8 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java @@ -0,0 +1,232 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.HashSet; +import java.util.Set; + +/** + * The look of an MCHeli explosion, ported from {@code MCH_Explosion.effectExplosion} and + * {@code effectExplosionInWater}: a fireball, ground dust thrown up from the crater, and columns + * and rings of smoke that hang around for seconds. It runs on each client that receives the + * explosion; the burning debris arcs are real flares spawned by the server. + */ +public final class ExplosionEffect { + /** The largest crash fireball drawn; beyond this the particle count gets out of hand. */ + public static final float MAX_CRASH_SIZE = 12.0F; + /** The widest explosion flash, in blocks; past this it washes out the whole screen. */ + private static final float MAX_FLASH_RADIUS = 30.0F; + + private ExplosionEffect() { + } + + /** + * Not in the original: a crashing vehicle goes up in a far bigger show than its fuel explosion + * alone makes. The same shell of smoke as a missile hit in mid-air, a cluster of fireballs + * around it, a plume of smoke that keeps climbing for seconds and burning fuel thrown about. + * The fire glows for several seconds afterwards, which at night lights up the smoke above it. + */ + public static void crash(Level level, double x, double y, double z, float size) { + play(level, x, y, z, size, false); + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, Math.min(size * 3.0F, MAX_FLASH_RADIUS), 20), + true, x, y + size * 0.3, z, 0.0, 0.0, 0.0); + int fireballs = Math.max(3, (int) (size / 2.0F)); + for (int i = 0; i < fireballs; i++) { + double r = size * 0.5 * random.nextDouble(); + double a = random.nextDouble() * Math.PI * 2.0; + double fx = x + Math.cos(a) * r; + double fy = y + random.nextDouble() * size * 0.4; + double fz = z + Math.sin(a) * r; + level.addAlwaysVisibleParticle(ParticleTypes.EXPLOSION_EMITTER, true, fx, fy, fz, 1.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size * (0.4F + random.nextFloat() * 0.3F), + 80 + random.nextInt(60)), true, fx, fy, fz, 0.0, 0.01, 0.0); + } + for (int i = 0; i < size * 4; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + x + (random.nextDouble() - 0.5) * size * 0.6, y + random.nextDouble() * 2.0, z + (random.nextDouble() - 0.5) * size * 0.6, + (random.nextDouble() - 0.5) * 0.02, 0.05 + random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.02); + } + for (int i = 0; i < size * 10; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = (0.2 + random.nextDouble() * 0.4) * size / 6.0; + double up = random.nextDouble() * speed; + level.addAlwaysVisibleParticle(random.nextInt(4) == 0 ? ParticleTypes.LAVA : ParticleTypes.FLAME, true, x, y + 0.5, z, + Math.cos(a) * speed, up, Math.sin(a) * speed); + if (i % 4 == 0) { + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.5F + random.nextFloat() * 0.4F, + 20 + random.nextInt(20)), true, x, y + 0.5, z, Math.cos(a) * speed, up, Math.sin(a) * speed); + } + } + } + + /** + * Not in the original: the light of the blast itself, a white hot flash inside a wider red + * glow, and a few embers thrown out. Faint by day; at night it lights up the smoke. + */ + private static void flash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + float radius = Math.min(2.0F + size * 1.5F, MAX_FLASH_RADIUS); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, radius, 8 + (int) size), true, x, y, z, 0.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.EMBER, Math.min(radius * 1.6F, MAX_FLASH_RADIUS), + 14 + (int) (size * 2.0F)), true, x, y, z, 0.0, 0.0, 0.0); + int embers = Math.min((int) (size * 2.0F), 24); + for (int i = 0; i < embers; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = 0.2 + random.nextDouble() * 0.3 * Math.min(size, 4.0F); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 15 + random.nextInt(15)), true, x, y, z, Math.cos(a) * speed, 0.1 + random.nextDouble() * speed, Math.sin(a) * speed); + } + } + + public static void play(Level level, double x, double y, double z, float size, boolean inWater) { + if (size <= 0.0F) { + return; + } + if (inWater) { + splash(level, x, y, z, size); + return; + } + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(size >= 2.0F ? ParticleTypes.EXPLOSION_EMITTER : ParticleTypes.EXPLOSION, true, x, y, z, 1.0, 0.0, 0.0); + flash(level, x, y, z, size); + + int count = 0; + int puffEvery = (int) Math.max(size, 4.0F); + for (BlockPos pos : affectedBlocks(level, x, y, z, size)) { + count++; + double bx = pos.getX() + random.nextFloat(); + double by = pos.getY() + random.nextFloat(); + double bz = pos.getZ() + random.nextFloat(); + double mx = bx - x; + double my = by - y; + double mz = bz - z; + double d = Math.sqrt(mx * mx + my * my + mz * mz); + if (d == 0.0) { + continue; + } + double push = 0.5 / (d / size + 0.1) * (random.nextFloat() * random.nextFloat() + 0.3F) * 0.5; + mx = mx / d * push; + my = my / d * push; + mz = mz / d * push; + double px = (bx + x) / 2.0; + double py = (by + y) / 2.0; + double pz = (bz + z) / 2.0; + double angle = Math.toRadians(random.nextInt(360)); + + if (count % 4 == 0) { + // Dirt and stone thrown up from the ground under the blast. + BlockState ground = level.getBlockState(BlockPos.containing(px + 0.5, py - 0.5, pz + 0.5)); + if (!ground.isAir()) { + float force = Math.min(size / 3.0F, 2.0F) * (0.5F + random.nextFloat() * 0.5F); + BlockParticleOption dust = new BlockParticleOption(ParticleTypes.BLOCK, ground); + for (int i = 0; i < 3; i++) { + level.addAlwaysVisibleParticle(dust, true, px, py + 1.0, pz, + Math.sin(angle) * force * 0.5, 0.5 + my / 5.0 * force, Math.cos(angle) * force * 0.5); + } + } + } + if (size <= 1.0F || count % puffEvery == 0) { + // Smoke either shoots up as a column or rolls out along the ground. + if (random.nextBoolean()) { + my *= 3.0; + mx *= 0.1; + mz *= 0.1; + } else { + my *= 0.2; + mx *= 3.0; + mz *= 3.0; + } + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, px, py, pz, mx * 0.3, my * 0.3, mz * 0.3); + for (int i = 0; i < 2; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, px, py, pz, mx, my, mz); + } + if (random.nextInt(3) == 0) { + level.addAlwaysVisibleParticle(ParticleTypes.FLAME, true, px, py, pz, mx * 0.5, my * 0.5, mz * 0.5); + } + } + } + } + + /** + * The blocks the original's effect ray marched through: sixteen by sixteen rays from the + * centre, each running until block resistance used up its strength. + */ + @SuppressWarnings("deprecation") // The plain block resistance is what the original used here. + private static Set affectedBlocks(Level level, double x, double y, double z, float size) { + Set out = new HashSet<>(); + RandomSource random = level.getRandom(); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + for (int k = 0; k < 16; k++) { + if (i != 0 && i != 15 && j != 0 && j != 15 && k != 0 && k != 15) { + continue; + } + double dx = i / 15.0F * 2.0F - 1.0F; + double dy = j / 15.0F * 2.0F - 1.0F; + double dz = k / 15.0F * 2.0F - 1.0F; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + dx /= len; + dy /= len; + dz /= len; + float strength = size * (0.7F + random.nextFloat() * 0.6F); + double cx = x; + double cy = y; + double cz = z; + for (float step = 0.3F; strength > 0.0F; strength -= step * 0.75F) { + cursor.set(Mth.floor(cx), Mth.floor(cy), Mth.floor(cz)); + BlockState state = level.getBlockState(cursor); + if (!state.isAir()) { + strength -= (state.getBlock().getExplosionResistance() + 0.3F) * step; + } + if (strength > 0.0F) { + out.add(cursor.immutable()); + } + cx += dx * step; + cy += dy * step; + cz += dz * step; + } + } + } + } + return out; + } + + /** {@code effectExplosionInWater}: a dome of spray from the water around the charge. */ + private static void splash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + int range = Math.max((int) (size + 0.5), 1); + int ex = (int) (x + 0.5); + int ey = (int) (y + 0.5); + int ez = (int) (z + 0.5); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int dy = -range; dy <= range; dy++) { + for (int dx = -range; dx <= range; dx++) { + for (int dz = -range; dz <= range; dz++) { + if (dx * dx + dy * dy + dz * dz >= range * range) { + continue; + } + cursor.set(ex + dx, ey + dy, ez + dz); + if (!level.getFluidState(cursor).is(FluidTags.WATER)) { + continue; + } + for (int n = random.nextInt(2); n > 0; n--) { + double up = 1.0 - Math.sqrt(dx * dx + dz * dz) / range + random.nextFloat() * 0.4 * range * 0.4; + level.addAlwaysVisibleParticle(ParticleTypes.SPLASH, true, cursor.getX(), cursor.getY() + 1.0, cursor.getZ(), + (double) dx / range * (random.nextFloat() - 0.2), up, (double) dz / range * (random.nextFloat() - 0.2)); + level.addAlwaysVisibleParticle(ParticleTypes.BUBBLE, true, cursor.getX(), cursor.getY(), cursor.getZ(), 0.0, up * 0.5, 0.0); + } + } + } + } + } +} From 31720d665d2d3ab1661c6d05fb5b721d8abfab4b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:15 +0800 Subject: [PATCH 1056/1461] Add weapon/Explosions.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/weapon/Explosions.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java new file mode 100644 index 00000000..f7fe3e89 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java @@ -0,0 +1,217 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.network.ExplosionEffectPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerExplosion; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Port of {@code MCH_Explosion}. + * + *

The original explosion had two sizes: one for blocks and one, doubled, for entities, with its + * own damage curve and the weapon's per-target {@code DamageFactor}. Entity damage is computed + * here as it was; the blocks, particles and sound go through a vanilla explosion of the block size, + * so mob griefing rules, protection mods and the client effects all behave as players expect. + */ +public final class Explosions { + /** Players this far away still see the smoke of an explosion. */ + private static final double EFFECT_RANGE = 256.0; + /** Share of the blast knockback an empty vehicle takes; they are far heavier than a mob. */ + private static final double VEHICLE_PUSH = 0.25; + + private Explosions() { + } + + /** What an explosion request needs besides its position and size. */ + public record Params(@Nullable Entity exploder, @Nullable Entity cause, float size, float blockSize, + boolean flaming, boolean destroyBlocks, int fireSeconds, boolean inWater, + @Nullable WeaponDefinition weapon) { + } + + /** + * @return whether any entity took damage, which the shooter hears as a hit confirmation + */ + public static boolean explode(ServerLevel level, double x, double y, double z, Params p) { + Vec3 center = new Vec3(x, y, z); + boolean hit = damageEntities(level, center, p); + + boolean breakBlocks = p.destroyBlocks && p.blockSize > 0.0F && ServerConfig.explosionsBreakBlocks(); + float radius = breakBlocks ? p.blockSize : Math.max(p.size, 0.5F); + level.explode(p.exploder, explosionSource(level, p), new Calculator(p.inWater, level), + x, y, z, radius, p.flaming && breakBlocks && ServerConfig.explosionsStartFires(), + breakBlocks ? Level.ExplosionInteraction.MOB : Level.ExplosionInteraction.NONE); + ModNetwork.sendNear(level, x, y, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y, z, p.size, p.inWater)); + if (!p.inWater) { + throwDebris(level, x, y, z, p.size); + } + return hit; + } + + /** + * The look of a vehicle going up when it crashes: no damage or blocks of its own (the crash + * explosion does those), but the {@link ExplosionEffect#crash big fireball} on every nearby + * client and twice the burning debris. Drawn a little above the impact so its smoke spreads + * into the air the way a missile hit's does, rather than straight into the ground. + */ + public static void crashFireball(ServerLevel level, double x, double y, double z, float size) { + float visual = Math.clamp(size * 2.0F, 6.0F, ExplosionEffect.MAX_CRASH_SIZE); + double lift = visual * 0.25; + ModNetwork.sendNear(level, x, y + lift, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y + lift, z, visual, false, true)); + throwDebris(level, x, y, z, visual, 24); + } + + private static void throwDebris(ServerLevel level, double x, double y, double z, float size) { + throwDebris(level, x, y, z, size, 12); + } + + /** + * Big blasts throw burning debris that arcs away trailing smoke, which the original made of + * flares. One piece per point of size, up to {@code maxPieces}. + */ + private static void throwDebris(ServerLevel level, double x, double y, double z, float size, int maxPieces) { + if (size < 4.0F) { + return; + } + int pieces = Math.min((int) size * maxPieces / 12, maxPieces); + for (int i = 0; i < pieces; i++) { + double angle = Math.toRadians(level.getRandom().nextInt(360)); + double speed = Math.min(size / 12.0F, 0.6) * (0.5F + level.getRandom().nextFloat() * 0.5F); + FlareEntity debris = ModEntities.FLARE.get().create(level, EntitySpawnReason.TRIGGERED); + if (debris != null) { + double spread = size / 2.0; + debris.launch(x + (level.getRandom().nextDouble() - 0.5) * spread, y + 2.0, z + (level.getRandom().nextDouble() - 0.5) * spread, + Math.sin(angle) * speed, (1.0 + level.getRandom().nextFloat() * 0.2) * speed, Math.cos(angle) * speed, 2.0F, 0, false); + level.addFreshEntity(debris); + } + } + } + + private static DamageSource explosionSource(ServerLevel level, Params p) { + return level.damageSources().explosion(p.exploder, p.cause); + } + + /** {@code doExplosionA} for entities: twice the block size, the 1.7.10 damage curve and knockback. */ + private static boolean damageEntities(ServerLevel level, Vec3 center, Params p) { + float size = p.size * 2.0F; + if (size <= 0.0F) { + return false; + } + AABB area = new AABB(center, center).inflate(size + 1.0); + DamageSource source = explosionSource(level, p); + boolean hit = false; + for (Entity entity : level.getEntities(p.exploder, area)) { + double d7 = Math.sqrt(entity.distanceToSqr(center)) / size; + if (d7 > 1.0) { + continue; + } + double dx = entity.getX() - center.x; + double dy = entity.getEyeY() - center.y; + double dz = entity.getZ() - center.z; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist == 0.0) { + continue; + } + dx /= dist; + dy /= dist; + dz /= dist; + double exposure = ServerExplosion.getSeenPercent(center, entity); + double impact = (1.0 - d7) * exposure; + float damage = (int) ((impact * impact + impact) / 2.0 * 8.0 * size + 1.0); + if (damage > 0.0F && !(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) + && !(entity instanceof FallingBlockEntity)) { + // Our own rounds caught in the blast do not count as hitting something. + if (!(entity instanceof ProjectileEntity bullet) || bullet.user() != p.cause) { + hit = true; + } + } + Combat.hurt(entity, source, damage, p.weapon); + if (entity instanceof AircraftEntity vehicle) { + // Not in the original: a vehicle someone is driving is simulated on their client, + // so a push here only snaps it back a step. An empty one still rocks a little. + if (vehicle.getControllingPassenger() == null) { + entity.push(dx * impact * VEHICLE_PUSH * 0.4, dy * impact * VEHICLE_PUSH * 0.1, dz * impact * VEHICLE_PUSH * 0.4); + } + } else if (!(entity instanceof ProjectileEntity)) { + entity.push(dx * impact * 0.4, dy * impact * 0.1, dz * impact * 0.4); + } + if (damage > 0.0F && p.fireSeconds > 0) { + double fire = 1.0 - dist / size; + if (fire > 0.0) { + entity.igniteForSeconds((float) (fire * p.fireSeconds)); + } + } + } + return hit; + } + + /** + * Vanilla explosion settings for the block pass: no entity damage or knockback (done above), + * and under water the blocks crumble the way the original's water charges made them. + */ + private static final class Calculator extends ExplosionDamageCalculator { + private final boolean inWater; + private final Level level; + + Calculator(boolean inWater, Level level) { + this.inWater = inWater; + this.level = level; + } + + // Vanilla's own calculator reads the block's resistance the same deprecated way. + @SuppressWarnings("deprecation") + @Override + public Optional getBlockExplosionResistance(Explosion explosion, BlockGetter getter, BlockPos pos, + BlockState block, FluidState fluid) { + if (!inWater) { + return super.getBlockExplosionResistance(explosion, getter, pos, block, fluid); + } + if (block.isAir()) { + return Optional.empty(); + } + float resistance = block.getBlock().getExplosionResistance(); + return Optional.of(resistance * (level.getRandom().nextFloat() * 0.2F + 0.2F)); + } + + @Override + public boolean shouldDamageEntity(Explosion explosion, Entity entity) { + return false; + } + + @Override + public float getKnockbackMultiplier(Entity entity) { + return 0.0F; + } + } + + /** {@code playExplosionSound}: the bang of a round that has no explosive charge. */ + public static void playSound(Level level, double x, double y, double z) { + float pitch = (1.0F + (level.getRandom().nextFloat() - level.getRandom().nextFloat()) * 0.2F) * 0.7F; + level.playSound(null, x, y, z, SoundEvents.GENERIC_EXPLODE.value(), SoundSource.BLOCKS, 4.0F, pitch); + } +} From 7fa1178b58ac6da770f78a8df6771bc0657d6a8d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:15 +0800 Subject: [PATCH 1057/1461] Add weapon/FlareDispenser.java to the 1.21.5 shared sources --- .../mcheli/weapon/FlareDispenser.java | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java new file mode 100644 index 00000000..12ddc658 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java @@ -0,0 +1,195 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.level.block.LevelEvent; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and smoke dischargers, ported from {@code MCH_Flare} and the flare half of + * {@code MCH_MissileDetector}. While a salvo burns, every missile homing on the vehicle within 60 + * blocks is decoyed. + * + *

The ten {@code FlareType} values pick a pattern: 1 random scatter, 2 and 3 to the sides and + * rear, 4 and 5 trailing, 10 a ring of smoke grenades. Unknown types fall back to type 1. + */ +public final class FlareDispenser { + private static final int DECOY_RANGE = 60; + + /** {@code num, interval, tickWait, tickEnable, numFlareMax} per type. */ + private static final int[][] TYPES = new int[11][]; + + static { + TYPES[1] = new int[]{1, 3, 200, 100, 16}; + TYPES[2] = new int[]{3, 5, 300, 200, 16}; + TYPES[3] = new int[]{2, 3, 200, 100, 16}; + TYPES[4] = new int[]{1, 3, 200, 100, 16}; + TYPES[5] = new int[]{2, 3, 200, 100, 16}; + TYPES[10] = new int[]{8, 1, 250, 60, 1}; + for (int i : new int[]{0, 6, 7, 8, 9}) { + TYPES[i] = TYPES[1]; + } + } + + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final RandomSource random = RandomSource.create(); + private int tick; + private int fired; + private int type; + private int typeIndex; + + FlareDispenser(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + } + + public boolean isFitted() { + return !def.flareTypes.isEmpty(); + } + + /** Waiting out the reload of the last salvo. */ + public boolean isPreparing() { + return tick != 0; + } + + /** The salvo is still burning, so incoming missiles are being decoyed. */ + public boolean isActive() { + int[] p = params(type); + return tick != 0 && tick > p[2] - p[3]; + } + + public int ticksLeft() { + return tick; + } + + public int cooldown() { + return params(type)[2]; + } + + public int currentType() { + return def.flareTypes.isEmpty() ? 0 : def.flareTypes.get(typeIndex % def.flareTypes.size()); + } + + public void nextType() { + if (!def.flareTypes.isEmpty()) { + typeIndex = (typeIndex + 1) % def.flareTypes.size(); + } + } + + /** {@code useFlare}: starts a salvo of the current type if the last one has finished. */ + public boolean use() { + if (!isFitted() || tick != 0 || vehicle.isDestroyed()) { + return false; + } + type = currentType(); + fired = 0; + // The client waited 50 ticks longer before showing the dispenser as ready again. + tick = params(type)[2] + (vehicle.level().isClientSide() ? 50 : 0); + return true; + } + + private static int[] params(int type) { + return type >= 0 && type < TYPES.length && TYPES[type] != null ? TYPES[type] : TYPES[1]; + } + + void tick() { + if (type <= 0 || vehicle.isRemoved()) { + return; + } + if (tick > 0) { + tick--; + } + int[] p = params(type); + if (!vehicle.level().isClientSide()) { + if (tick > 0 && tick % p[1] == 0 && fired < p[4]) { + Vec3 at = vehicle.toWorld(def.flarePosition); + spawnSalvo(new Vec3(at.x + vehicle.xo - vehicle.getX(), at.y + vehicle.yo - vehicle.getY(), at.z + vehicle.zo - vehicle.getZ())); + } + if (isActive()) { + decoyMissiles(); + } + } + } + + private void spawnSalvo(Vec3 at) { + fired++; + int[] p = params(type); + Vec3 m = vehicle.getDeltaMovement(); + double x = at.x - m.x * 2.0; + double y = at.y - m.y * 2.0 - 1.0; + double z = at.z - m.z * 2.0; + vehicle.level().levelEvent(LevelEvent.SOUND_DISPENSER_PROJECTILE_LAUNCH, net.minecraft.core.BlockPos.containing(x, y, z), 0); + for (int i = 0; i < p[0]; i++) { + double tx = 0.0; + double ty = m.y; + double tz = 0.0; + int fuse = 0; + double r = vehicle.getYRot(); + double fy = y; + switch (type) { + case 1 -> { + tx = Mth.sin(random.nextFloat() * 360.0F); + tz = Mth.cos(random.nextFloat() * 360.0F); + } + case 2, 3 -> { + if (i == 0) { + r += 90.0; + } else if (i == 1) { + r -= 90.0; + } else if (i == 2) { + r += 180.0; + } + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 0.6; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 0.6; + } + case 4 -> { + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 1.3; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 1.3; + } + case 5 -> { + r = Math.toRadians(r); + tx = (-Math.sin(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + tz = (Math.cos(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + } + default -> { + } + } + tx += m.x; + ty += m.y / 2.0; + tz += m.z; + if (type == 10) { + r += 360.0 / p[0] / 2.0 + i * (360.0 / p[0]); + r = Math.toRadians(r); + tx = -Math.sin(r) * 2.0; + tz = Math.cos(r) * 2.0; + ty = 0.7; + fy += 2.0; + fuse = 10; + } + FlareEntity flare = ModEntities.FLARE.get().create(vehicle.level(), EntitySpawnReason.TRIGGERED); + if (flare != null) { + flare.launch(x, fy, z, tx * 0.5, ty * 0.5, tz * 0.5, 6.0F, fuse, type == 4); + vehicle.level().addFreshEntity(flare); + } + } + } + + /** {@code destroyMissile}: missiles homing on this vehicle or its crew lose their target. */ + private void decoyMissiles() { + for (ProjectileEntity missile : vehicle.level().getEntitiesOfClass(ProjectileEntity.class, + vehicle.getBoundingBox().inflate(DECOY_RANGE), p -> p.target() != null)) { + if (missile.target() == vehicle || vehicle.hasPassenger(missile.target())) { + missile.decoy(); + } + } + } +} From c8244b577f0f2f52b5cc588e0d30e608ab622cb1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:16 +0800 Subject: [PATCH 1058/1461] Add weapon/Guidance.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/weapon/Guidance.java | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java new file mode 100644 index 00000000..b8142e92 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java @@ -0,0 +1,220 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Predicate; + +/** + * Lock-on for seeking missiles, ported from {@code MCH_WeaponGuidanceSystem}. It runs on the + * client of the crew member aiming; the locked entity travels to the server with the fire request. + * + *

A target is acquired inside a cone around the aim, then the lock count climbs while it stays + * in the cone. Stealth shrinks both the range and the cone and stretches the time to lock. + */ +public final class Guidance { + public final boolean canLockInAir; + public final boolean canLockOnGround; + public final boolean canLockInWater = false; + public final boolean ridableOnly; + public final double lockRange = 200.0; + public final int lockAngle = 5; + private final int lockCountMax; + private final @Nullable Predicate filter; + + private @Nullable Entity target; + private @Nullable Entity lastLocked; + private int lockCount; + private int prevLockCount; + private int continueLockCount; + private int soundCount; + + public Guidance(boolean air, boolean ground, boolean ridableOnly, int lockTime, @Nullable Predicate filter) { + this.canLockInAir = air; + this.canLockOnGround = ground; + this.ridableOnly = ridableOnly; + this.lockCountMax = Math.max(lockTime, 1); + this.filter = filter; + } + + public int lockCount() { + return lockCount; + } + + public int lockCountMax() { + float stealth = stealth(target); + return (int) (lockCountMax + lockCountMax * stealth); + } + + public @Nullable Entity target() { + return lockCount > 0 && target != null && !target.isRemoved() ? target : null; + } + + public @Nullable Entity lastLocked() { + return lastLocked; + } + + public boolean isLocked() { + return lockCount == lockCountMax() && lastLocked != null; + } + + /** Called every client tick: a lock decays unless {@link #lock} kept it alive this tick. */ + public void tick() { + if (lockCount != prevLockCount) { + prevLockCount = lockCount; + } else { + lockCount = 0; + prevLockCount = 0; + } + } + + /** + * Advances the lock for one tick of aiming from {@code user}'s view. + * + * @param eye where the view is seen from. The original used the player's position, which on a + * 1.7.10 client was eye height; gunner sights and launcher cameras sit higher still, + * and a 5 degree cone measured from the wrong point misses targets close by. + * @return true once the lock is complete; {@link #lastLocked()} is the target to send + */ + public boolean lock(Entity user, Vec3 eye) { + if (lockCount == 0) { + acquire(user, eye); + return false; + } + if (target == null || target.isRemoved() || !inLockableState(target)) { + clear(); + return false; + } + double range = lockRange * (1.0F - stealth(target)); + if (user.distanceToSqr(target) >= range * range) { + clear(); + return false; + } + soundCount = (soundCount + 1) % 15; + if (inCone(eye, user.getYRot(), user.getXRot(), target, lockAngle)) { + if (lockCount < lockCountMax()) { + lockCount++; + } + } else if (continueLockCount > 0) { + continueLockCount--; + if (continueLockCount <= 0 && lockCount > 0) { + lockCount--; + } + } else { + continueLockCount = 0; + lockCount--; + } + if (lockCount >= lockCountMax()) { + if (continueLockCount <= 0) { + continueLockCount = Math.min(lockCountMax() / 3, 20); + } + lastLocked = target; + prevLockCount = lockCount - 1; + return true; + } + return false; + } + + /** True on the tick the target should hear that it is being tracked. */ + public boolean shouldNotifyTarget() { + return lockCount > 0 && soundCount == 1; + } + + private void acquire(Entity user, Vec3 eye) { + Entity best = null; + double bestDistance = lockRange * lockRange * 2.0; + for (Entity entity : user.level().getEntities(user, user.getBoundingBox().inflate(lockRange), this::canLock)) { + double d = entity.distanceToSqr(user); + float stealth = 1.0F - stealth(entity); + double range = lockRange * stealth; + float angle = lockAngle * (stealth / 2.0F + 0.5F); + if (d < range * range && d < bestDistance && inCone(eye, user.getYRot(), user.getXRot(), entity, angle)) { + Vec3 to = new Vec3(entity.getX(), entity.getY() + entity.getBbHeight() / 2.0F, entity.getZ()); + HitResult hit = user.level().clip(new ClipContext(eye, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, user)); + if (hit.getType() == HitResult.Type.MISS) { + best = entity; + bestDistance = d; + } + } + } + target = best; + if (best != null) { + lockCount++; + } + } + + public void clear() { + target = null; + lockCount = 0; + continueLockCount = 0; + soundCount = 0; + } + + private boolean canLock(Entity entity) { + if (ridableOnly && entity instanceof Player && entity.getVehicle() == null) { + return false; + } + if (!(entity instanceof LivingEntity) && !(entity instanceof AircraftEntity)) { + return false; + } + if (filter != null && !filter.test(entity)) { + return false; + } + return inLockableState(entity); + } + + private boolean inLockableState(Entity entity) { + if (!canLockInWater && entity.isInWater()) { + return false; + } + boolean onGround = isOnGround(entity); + return onGround ? canLockOnGround : canLockInAir; + } + + /** On the ground, or less than twelve blocks above something solid. */ + public static boolean isOnGround(Entity entity) { + if (entity.onGround()) { + return true; + } + BlockPos.MutableBlockPos pos = BlockPos.containing(entity.getX() + 0.5, entity.getY() + 0.5, entity.getZ() + 0.5).mutable(); + for (int i = 0; i < 12; i++) { + if (!entity.level().getBlockState(pos).isAir() && !entity.level().getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + pos.move(0, -1, 0); + } + return false; + } + + public static float stealth(@Nullable Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + return vehicle.stealth(); + } + return entity != null && entity.getVehicle() instanceof AircraftEntity vehicle ? vehicle.stealth() : 0.0F; + } + + /** {@code inLockRange}: whether {@code target} is within {@code angle} degrees of the aim. */ + public static boolean inCone(Entity from, float yaw, float pitch, Entity target, float angle) { + return inCone(from.position(), yaw, pitch, target, angle); + } + + public static boolean inCone(Vec3 from, float yaw, float pitch, Entity target, float angle) { + double dx = target.getX() - from.x; + double dy = target.getY() + target.getBbHeight() / 2.0F - from.y; + double dz = target.getZ() - from.z; + float targetYaw = (float) Math.toDegrees(Math.atan2(dz, dx)); + float diffYaw = Mth.positiveModulo(targetYaw - yaw - 90.0F, 360.0F); + float targetPitch = -(float) Math.toDegrees(Math.atan2(dy, Math.sqrt(dx * dx + dz * dz))); + float diffPitch = targetPitch - pitch; + return (diffYaw < angle || diffYaw > 360.0F - angle) && Math.abs(diffPitch) < angle; + } +} From f89180b0ffff1bfc4e67aca38485cf64e7f03484 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:16 +0800 Subject: [PATCH 1059/1461] Add weapon/LegacyItems.java to the 1.21.5 shared sources --- .../mcheli/weapon/LegacyItems.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java new file mode 100644 index 00000000..100e5ce1 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import org.jspecify.annotations.Nullable; + +/** + * Maps the 1.7.10 item names and damage values in the content files to today's items. Names + * that still exist resolve directly; the few that were renamed or flattened are listed here. + */ +public final class LegacyItems { + private static final String[] DYES = { + "ink_sac", "red_dye", "green_dye", "cocoa_beans", "lapis_lazuli", "purple_dye", "cyan_dye", "light_gray_dye", + "gray_dye", "pink_dye", "lime_dye", "yellow_dye", "light_blue_dye", "magenta_dye", "orange_dye", "bone_meal"}; + + private LegacyItems() { + } + + public static @Nullable Item item(String name, int damage) { + if (name.isEmpty()) { + return null; + } + String id = switch (name) { + case "dye" -> DYES[Math.floorMod(damage, DYES.length)]; + case "boat" -> "oak_boat"; + case "reeds" -> "sugar_cane"; + case "wool" -> "white_wool"; + case "log" -> "oak_log"; + case "planks" -> "oak_planks"; + default -> name.startsWith("minecraft:") ? name.substring("minecraft:".length()) : name; + }; + ResourceLocation key = ResourceLocation.tryParse(id.contains(":") ? id : "minecraft:" + id); + if (key == null) { + return null; + } + Item item = BuiltInRegistries.ITEM.getValue(key); + return item == Items.AIR ? null : item; + } +} From 328cb6d1baebd688ae10be932e45c107f41ec0ed Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:16 +0800 Subject: [PATCH 1060/1461] Add weapon/ModDamageTypes.java to the 1.21.5 shared sources --- .../mcheli/weapon/ModDamageTypes.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java new file mode 100644 index 00000000..73d53c7b --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * Damage types defined in {@code data/mcheli/damage_type}. Bullets and missiles use their own type + * so death messages name the gun instead of calling it a thrown snowball, which is what the + * original's {@code causeThrownDamage} amounted to. + */ +public final class ModDamageTypes { + public static final ResourceKey BULLET = + ResourceKey.create(Registries.DAMAGE_TYPE, ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "bullet")); + + private ModDamageTypes() { + } + + public static DamageSource bullet(Level level, Entity projectile, @Nullable Entity owner) { + return new DamageSource(level.registryAccess().lookupOrThrow(Registries.DAMAGE_TYPE).getOrThrow(BULLET), projectile, owner); + } +} From 6a6f70d003040f51097dfc1cc3f70ac2f28e4911 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:17 +0800 Subject: [PATCH 1061/1461] Add weapon/Resupply.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/weapon/Resupply.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java new file mode 100644 index 00000000..533f5fba --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** + * Resupplying a weapon from a crew member's own items, the {@code supplyAmmo} path of the + * original's vehicle screen. One supply costs the weapon's {@code Item} lines and adds + * {@code SuppliedNum} rounds; it only works while the vehicle stands on something. + */ +public final class Resupply { + private Resupply() { + } + + /** {@code canPlayerSupplyAmmo}: whether {@link #supply} would do anything right now. */ + public static boolean canSupply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.set(weapon) : null; + if (set == null || vehicle.isDestroyed() || player.getVehicle() != vehicle || !vehicle.canSupply()) { + return false; + } + if (set.ammo() + set.restAmmo() >= set.maxAmmo()) { + return false; + } + return player.getAbilities().instabuild || canPay(player.getInventory(), set.definition()); + } + + /** Pays for and adds one supply of {@code weapon}. Server only. */ + public static boolean supply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + if (!canSupply(player, vehicle, weapon)) { + return false; + } + WeaponSet set = vehicle.weapons().set(weapon); + if (!player.getAbilities().instabuild) { + pay(player.getInventory(), set.definition()); + } + set.supply(); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + return true; + } + + private static boolean canPay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null || inventory.countItem(item) < cost.count()) { + return false; + } + } + return true; + } + + private static void pay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + int left = cost.count(); + for (int i = 0; i < inventory.getContainerSize() && left > 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.is(item)) { + continue; + } + if (item == Items.WATER_BUCKET || item == Items.LAVA_BUCKET) { + // Buckets give their bucket back, one at a time. + inventory.setItem(i, new ItemStack(Items.BUCKET)); + left--; + } else { + int take = Math.min(left, stack.getCount()); + stack.shrink(take); + left -= take; + } + } + } + inventory.setChanged(); + } +} From 74aa75b4d656cd8a99d24a2baa7aea443aed043d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:17 +0800 Subject: [PATCH 1062/1461] Add weapon/ServerFiring.java to the 1.21.5 shared sources --- .../mcheli/weapon/ServerFiring.java | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java new file mode 100644 index 00000000..3f867744 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java @@ -0,0 +1,418 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * What each weapon type does when the server fires it: the {@code shot} methods of the + * {@code MCH_Weapon*} classes. Every method returns whether a round was used. + */ +public final class ServerFiring implements WeaponSet.Trigger { + public static final ServerFiring INSTANCE = new ServerFiring(); + private static final RandomSource RANDOM = RandomSource.create(); + + private ServerFiring() { + } + + @Override + public boolean fire(WeaponSet set, int mount, Shot shot) { + if (!(shot.shooter.level() instanceof ServerLevel level)) { + return false; + } + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_1, MACHINE_GUN_2 -> gun(level, set, shot); + case ROCKET -> rocket(level, set, shot); + case BOMB -> bomb(level, set, shot); + case AA_MISSILE -> aaMissile(level, set, mount, shot); + case AS_MISSILE -> asMissile(level, set, shot); + case AT_MISSILE -> atMissile(level, set, mount, shot); + case TV_MISSILE -> tvMissile(level, set, mount, shot); + case TORPEDO -> torpedo(level, set, shot); + case CAS -> cas(level, set, shot); + case MARKER_ROCKET -> markerRocket(level, set, shot); + case DISPENSER -> dispenser(level, set, shot); + case TARGETING_POD -> targetingPod(level, set, shot); + case DUMMY, SMOKE -> false; + case null -> false; + }; + } + + private static @Nullable T spawn(ServerLevel level, EntityType type, WeaponSet set, Shot shot, + Vec3 dir, float yaw, float pitch, double acceleration) { + T e = type.create(level, EntitySpawnReason.TRIGGERED); + if (e == null) { + return null; + } + e.launch(shot.x, shot.y, shot.z, dir.x, dir.y, dir.z, yaw, pitch, acceleration); + e.setWeapon(set.name()); + e.setParameters(set.definition(), shot.shooter, shot.user); + return e; + } + + private static Vec3 forward(float yaw, float pitch) { + return Vec3.directionFromRotation(pitch, yaw); + } + + private static void sound(WeaponSet set, Entity at) { + sound(set, at, soundName(set)); + } + + private static String soundName(WeaponSet set) { + return set.ownSound != null ? set.ownSound.sound() : set.definition().sound; + } + + private static void sound(WeaponSet set, Entity at, String name) { + if (set.soundAllowed) { + WeaponDefinition w = set.definition(); + float rnd = w.soundPitchRandom; + float pitch = set.ownSound != null && set.ownSound.pitch() != null ? set.ownSound.pitch() : w.soundPitch; + ModSounds.playAt(at, name, w.soundVolume, pitch * (1.0F - rnd) + RANDOM.nextFloat() * rnd); + } + } + + // Guns, rockets, bombs + + private static boolean gun(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + BulletEntity e = spawn(level, ModEntities.BULLET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, w.acceleration); + if (e == null) { + return false; + } + if (w.type == WeaponType.MACHINE_GUN_2 && w.modeNum >= 2) { + // Mode 0 fires ball ammunition, which only bangs; mode 1 is high explosive. + e.explosionPower = shot.option1 == 0 ? -w.explosion : w.explosion; + } + Vec3 m = e.getDeltaMovement(); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean rocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + RocketEntity e = spawn(level, ModEntities.ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + if (shot.option1 == 0 && set.definition().modeNum > 1) { + e.piercing = 0; + } + level.addFreshEntity(e); + return true; + } + + private static boolean bomb(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + if (w.destruct) { + // A drone's self destruct charge. + if (shot.shooter instanceof HelicopterEntity drone && drone.definition() != null && drone.definition().uav) { + Explosions.explode(level, drone.getX(), drone.getY(), drone.getZ(), + new Explosions.Params(null, shot.user, w.explosion, w.explosionBlock, w.flaming, true, 0, false, null)); + sound(set, shot.shooter); + drone.ejectPassengers(); + drone.discard(); + } + return true; + } + sound(set, shot.shooter); + Vec3 motion = shot.shooter.getDeltaMovement(); + BombEntity e = spawn(level, ModEntities.BOMB.get(), set, shot, motion, shot.shooter.getYRot(), 0.0F, w.acceleration); + if (e == null) { + return false; + } + e.setDeltaMovement(motion); + level.addFreshEntity(e); + return true; + } + + // Missiles + + private static float fixedYaw(WeaponSet set, int mount) { + return set.mounts().get(mount).yaw(); + } + + private static float fixedPitch(WeaponSet set, int mount) { + return set.mounts().get(mount).pitch(); + } + + private static boolean aaMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + sound(set, shot.shooter); + float yaw = shot.shooter.getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + AAMissileEntity e = spawn(level, ModEntities.AA_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + level.addFreshEntity(e); + return true; + } + + /** Ray from the shooter along the aimer's view, as the original's AS missile and CAS sights. */ + private static @Nullable BlockHitResult sight(ServerLevel level, Shot shot, double eyeHeight, double range) { + Entity aimer = shot.aimer(); + Vec3 from = shot.shooter.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(forward(aimer.getYRot(), aimer.getXRot()).scale(range)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + return hit.getType() == HitResult.Type.BLOCK ? hit : null; + } + + private static boolean nextToWater(ServerLevel level, BlockPos pos) { + if (level.getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + for (Direction d : Direction.values()) { + if (level.getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + private static boolean asMissile(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 1.62, 250.0); + if (hit == null || nextToWater(level, hit.getBlockPos())) { + return false; + } + Entity aimer = shot.aimer(); + ASMissileEntity e = spawn(level, ModEntities.AS_MISSILE.get(), set, shot, forward(aimer.getYRot(), aimer.getXRot()), + aimer.getYRot(), aimer.getXRot(), set.definition().acceleration); + if (e == null) { + return false; + } + e.targetX = hit.getLocation().x; + e.targetY = hit.getLocation().y; + e.targetZ = hit.getLocation().z; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean atMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + ATMissileEntity e = spawn(level, ModEntities.AT_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + e.guidanceType = shot.option2; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean tvMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.aimer().getXRot() + fixedPitch(set, mount); + boolean tvGuided = shot.option1 == 0; + float acceleration = set.definition().acceleration * (tvGuided ? 1.0F : 1.5F); + TvMissileEntity e = spawn(level, ModEntities.TV_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, acceleration); + if (e == null) { + return false; + } + level.addFreshEntity(e); + if (shot.shooter instanceof AircraftEntity vehicle) { + vehicle.setTvMissile(e); + } + sound(set, shot.shooter); + return true; + } + + private static boolean torpedo(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Vec3 vehicleMotion = shot.shooter.getDeltaMovement(); + float yaw = shot.yaw; + float pitch = shot.pitch; + Vec3 target = null; + if (w.guidedTorpedo) { + Entity aimer = shot.aimer(); + yaw = aimer.getYRot(); + pitch = aimer.getXRot(); + Vec3 from = aimer.position(); + Vec3 to = from.add(forward(yaw, pitch).scale(150.0)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + if (hit.getType() != HitResult.Type.BLOCK || !nextToWater(level, hit.getBlockPos())) { + return false; + } + target = hit.getLocation(); + } + Vec3 m = forward(yaw, pitch).scale(w.acceleration).add(vehicleMotion); + double speed = m.length(); + TorpedoEntity e = spawn(level, ModEntities.TORPEDO.get(), set, shot, m, yaw, 0.0F, speed); + if (e == null) { + return false; + } + e.setDeltaMovement(m); + e.accelerationInWater = w.accelerationInWater; + if (target != null) { + e.targetX = target.x; + e.targetY = target.y; + e.targetZ = target.z; + } + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + // Support weapons + + private static boolean markerRocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + MarkerRocketEntity e = spawn(level, ModEntities.MARKER_ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + e.setStatus(1); + level.addFreshEntity(e); + return true; + } + + private static boolean dispenser(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + DispensedItemEntity e = spawn(level, ModEntities.DISPENSED_ITEM.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + Vec3 m = shot.shooter.getDeltaMovement().add(e.getDeltaMovement().scale(0.5)); + e.setDeltaMovement(m); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + return true; + } + + /** + * Spots everything of the weapon's {@code Target} kinds inside its cone, which then glows for + * {@code MarkTime}. Marking a block for the team needs the team manager of a later phase. + */ + private static boolean targetingPod(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Entity aimer = shot.aimer(); + if ((w.target & WeaponDefinition.TARGET_BLOCK) != 0) { + // {@code MCH_Multiplay.markPoint}: the block under the crew's view, 300 blocks out, goes to the team. + boolean marked = aimer instanceof ServerPlayer player && Spotting.markPoint(player, new Vec3(shot.x, shot.y, shot.z), 300.0); + sound(set, aimer, marked ? soundName(set) : "ng"); + return true; + } + List spotted = new ArrayList<>(); + double range = Math.max(w.length, 1.0F); + for (LivingEntity entity : level.getEntitiesOfClass(LivingEntity.class, aimer.getBoundingBox().inflate(range))) { + if (entity == aimer || shot.shooter.hasPassenger(entity) || aimer.distanceToSqr(entity) > range * range + || !matchesTarget(entity, w.target) || !Guidance.inCone(aimer, aimer.getYRot(), aimer.getXRot(), entity, w.angle)) { + continue; + } + spotted.add(entity.getId()); + } + if (aimer instanceof ServerPlayer player) { + Spotting.tellTeam(player, spotted, Math.max(w.markTime, 20)); + } + sound(set, shot.shooter, !spotted.isEmpty() ? soundName(set) : "ng"); + return true; + } + + private static boolean matchesTarget(LivingEntity entity, int bits) { + Entity vehicle = entity.getVehicle(); + if (vehicle instanceof PlaneEntity) { + return (bits & WeaponDefinition.TARGET_PLANES) != 0; + } + if (vehicle instanceof HelicopterEntity) { + return (bits & WeaponDefinition.TARGET_HELICOPTERS) != 0; + } + if (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity) { + return (bits & WeaponDefinition.TARGET_GROUND_VEHICLES) != 0; + } + if (entity instanceof Player) { + return (bits & WeaponDefinition.TARGET_PLAYERS) != 0; + } + if (entity instanceof Enemy) { + return (bits & WeaponDefinition.TARGET_MONSTERS) != 0; + } + return (bits & WeaponDefinition.TARGET_OTHERS) != 0; + } + + // Close air support + + private static boolean cas(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 2.0, 150.0); + if (hit == null) { + return false; + } + Vec3 at = hit.getLocation(); + set.casX = at.x; + set.casY = at.y; + set.casZ = at.z; + set.casDirection = Math.floorMod(CasStrike.approach(shot.aimer().getYRot()), 4); + set.casUser = shot.user; + set.casShooter = shot.shooter; + set.casStartTick = CasStrike.FIRST_PASS; + set.casCount = 0; + if (shot.user instanceof ServerPlayer player) { + ModSounds.playTo(player, set.definition().sound, 1.0F, 1.0F); + } + return true; + } + + /** {@code MCH_WeaponCAS.update}: three passes, 45 ticks apart, the later ones offset to the sides. */ + static void tick(AircraftEntity vehicle, WeaponSet set) { + if (set.casCount >= 3 || set.countWait == 0 || set.ticksSinceShot(0) != set.casStartTick + || !(vehicle.level() instanceof ServerLevel level)) { + return; + } + Vec3 offset = CasStrike.passOffset(set.casCount, set.casDirection); + CasStrike.spawnA10(level, set.name(), set.definition(), set.casDirection, set.casX + offset.x, set.casY + 20.0, + set.casZ + offset.z, set.casShooter, set.casUser); + set.casStartTick = set.ticksSinceShot(0) + 45; + set.casCount++; + } +} From 3e052b2959143d8b9d3d17af056d1751e61bd6a3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:18 +0800 Subject: [PATCH 1063/1461] Add weapon/Shot.java to the 1.21.5 shared sources --- .../com/jnhprojects/mcheli/weapon/Shot.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java new file mode 100644 index 00000000..6a9f53cf --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java @@ -0,0 +1,40 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +/** + * One trigger pull, the port of {@code MCH_WeaponParam}. The weapon set fills in the aim and the + * muzzle position; the client fills in the two option values (a locked target, a fire mode) and + * the server receives them with the fire request. + */ +public final class Shot { + /** The vehicle firing, or the player for hand held weapons. */ + public final Entity shooter; + /** Who pulled the trigger. Damage and kills are credited to this entity. */ + public final @Nullable Entity user; + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + public float roll; + public int option1; + public int option2; + public boolean infiniteAmmo; + /** Set by the weapon set when the magazine ran dry with this shot. */ + public boolean reload; + + public Shot(Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + this.x = shooter.getX(); + this.y = shooter.getY(); + this.z = shooter.getZ(); + } + + /** The entity whose view steers guided weapons: the user if there is one, else the shooter. */ + public Entity aimer() { + return user != null ? user : shooter; + } +} From b404776f6397909f2f10cbecd31b486f6590d1de Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:18 +0800 Subject: [PATCH 1064/1461] Add weapon/Spotting.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/weapon/Spotting.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java new file mode 100644 index 00000000..10167f4a --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.network.MarkerPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +/** + * Spotting with the rangefinder, {@code MCH_Multiplay.spotEntity} and {@code markPoint}. + * + *

Spotted targets and marked points go to the spotter and their team only, as screen markers + * the HUD draws ({@code MCH_GuiTargetMarker}); nobody else sees them. + */ +public final class Spotting { + // The original's target filter bits. + public static final int OTHERS = 1; + public static final int MONSTERS = 2; + public static final int PLAYERS = 4; + public static final int GROUND_VEHICLES = 8; + public static final int HELICOPTERS = 16; + public static final int PLANES = 32; + + /** {@code RangeFinderSpotDist}. */ + public static final double RANGE = 400.0; + /** {@code RangeFinderSpotTime} in ticks, for players and vehicles. */ + private static final int PLAYER_TIME = 15 * 20; + /** Mobs stay marked for a minute. */ + private static final int MOB_TIME = 60 * 20; + /** Half the width of the cone a spot covers, in degrees: the original's angle times two. */ + private static final double CONE = 40.0; + + private Spotting() { + } + + static boolean matches(int filter, Entity entity) { + int bit; + if (entity instanceof PlaneEntity) { + bit = PLANES; + } else if (entity instanceof HelicopterEntity) { + bit = HELICOPTERS; + } else if (entity instanceof AircraftEntity) { + bit = GROUND_VEHICLES; + } else if (entity instanceof Player) { + bit = PLAYERS; + } else if (entity instanceof Enemy) { + bit = MONSTERS; + } else if (entity instanceof LivingEntity) { + bit = OTHERS; + } else { + return false; + } + return (filter & bit) != 0; + } + + /** + * Marks what {@code spotter} sees within {@link #RANGE} and the cone around their view: never + * a teammate, and only targets in plain sight. + * + * @return whether anything was spotted + */ + public static boolean spot(ServerPlayer spotter, int filter) { + ServerLevel level = spotter.serverLevel(); + Vec3 eye = spotter.getEyePosition(); + Vec3 look = spotter.getLookAngle(); + int ticks = (filter & ~(OTHERS | MONSTERS)) == 0 ? MOB_TIME : PLAYER_TIME; + List spotted = new ArrayList<>(); + for (Entity entity : level.getEntities(spotter, spotter.getBoundingBox().inflate(RANGE), e -> matches(filter, e))) { + if (isFriendly(spotter, entity)) { + continue; + } + Vec3 to = entity.getBoundingBox().getCenter().subtract(eye); + double dist = to.length(); + if (dist < 1.0 || dist > RANGE || Math.toDegrees(Math.acos(to.scale(1.0 / dist).dot(look))) > CONE) { + continue; + } + if (level.clip(new ClipContext(eye, entity.getBoundingBox().getCenter(), ClipContext.Block.COLLIDER, + ClipContext.Fluid.NONE, spotter)).getType() == HitResult.Type.BLOCK) { + continue; + } + spotted.add(entity.getId()); + } + tellTeam(spotter, spotted, ticks); + return !spotted.isEmpty(); + } + + /** Sends the spotted entities to {@code spotter} and their team, {@code MCH_PacketNotifySpotedEntity}. */ + public static void tellTeam(ServerPlayer spotter, List entities, int ticks) { + if (entities.isEmpty()) { + return; + } + MarkerPacket packet = MarkerPacket.spotted(entities.stream().mapToInt(Integer::intValue).toArray(), ticks); + for (ServerPlayer player : TeamManager.teammates(spotter)) { + ModNetwork.sendToPlayer(player, packet); + } + } + + /** Teammates, and a vehicle crewed by one, are never spotted. */ + private static boolean isFriendly(ServerPlayer spotter, Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + for (Entity crew : vehicle.getPassengers()) { + if (crew.isAlliedTo(spotter)) { + return true; + } + } + return false; + } + return entity.isAlliedTo(spotter); + } + + /** + * Tells the spotter and their team where they are looking, as block coordinates. + * + * @return whether the view ended on a block within range + */ + public static boolean markPoint(ServerPlayer spotter) { + return markPoint(spotter, spotter.getEyePosition(), RANGE); + } + + /** + * As above, looking from {@code from} along the spotter's view: a targeting pod marks from + * the vehicle, out to {@code range}. + */ + public static boolean markPoint(ServerPlayer spotter, Vec3 from, double range) { + ServerLevel level = spotter.serverLevel(); + BlockHitResult hit = level.clip(new ClipContext(from, from.add(spotter.getLookAngle().scale(range)), + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, spotter)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + BlockPos pos = hit.getBlockPos(); + MarkerPacket marker = MarkerPacket.point(pos); + Component message = Component.translatable("message.mcheli.mark_point", spotter.getDisplayName(), pos.getX(), pos.getY(), pos.getZ()); + for (ServerPlayer player : level.getServer().getPlayerList().getPlayers()) { + if (player == spotter || player.isAlliedTo(spotter)) { + player.sendSystemMessage(message); + ModNetwork.sendToPlayer(player, marker); + } + } + return true; + } +} From be9f76dc9ffe52b41b53afda43ca6f008b536ac7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:19 +0800 Subject: [PATCH 1065/1461] Add weapon/VehicleWeapons.java to the 1.21.5 shared sources --- .../mcheli/weapon/VehicleWeapons.java | 598 ++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java new file mode 100644 index 00000000..af94a9dc --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java @@ -0,0 +1,598 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.weapon.MuzzleFlash; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.WeaponStatePacket; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The armament of one vehicle: its weapon sets, which one each seat has selected, the flares, and + * the effects every client derives from the synced firing state. Ported from the weapon handling + * in {@code MCH_EntityAircraft} ({@code createWeapon}, {@code switchWeapon}, + * {@code useCurrentWeapon}, {@code updateWeapons} and {@code updateWeaponsRotation}). + */ +public final class VehicleWeapons { + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final boolean clientSide; + private final List sets = new ArrayList<>(); + private final int[] selected; + private final @Nullable Guidance[] guidance; + private final RandomSource random = RandomSource.create(); + + /** One bit per mount, set while that mount shows muzzle effects. Synced by the vehicle. */ + private int useStat; + private int prevUseStat; + + private final float[] bayAngle; + private final float[] prevBayAngle; + + private final int[] sentAmmo; + private final int[] sentRest; + + public final FlareDispenser flares; + + // Recoil that kicks the whole hull, from Recoil in the weapon file. + private int recoilCount; + private float recoilValue; + private float recoilYaw; + + public VehicleWeapons(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + this.clientSide = vehicle.level().isClientSide(); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + if (weapon != null && !group.mounts().isEmpty()) { + WeaponSet set = new WeaponSet(weapon, group.mounts(), clientSide); + set.ownSound = def.weaponSounds.get(group.weaponType()); + sets.add(set); + } + } + selected = new int[def.seatCount()]; + Arrays.fill(selected, -1); + guidance = new Guidance[sets.size()]; + for (int i = 0; i < sets.size(); i++) { + WeaponDefinition w = sets.get(i).definition(); + if (w.type == WeaponType.AA_MISSILE || w.type == WeaponType.AT_MISSILE) { + boolean aa = w.type == WeaponType.AA_MISSILE; + guidance[i] = new Guidance(aa, !aa, w.ridableOnly, w.lockTime, this::canLock); + } + } + bayAngle = new float[def.weaponBays.size()]; + prevBayAngle = new float[def.weaponBays.size()]; + sentAmmo = new int[sets.size()]; + sentRest = new int[sets.size()]; + Arrays.fill(sentAmmo, -1); + flares = new FlareDispenser(vehicle, def); + } + + public List sets() { + return sets; + } + + public @Nullable WeaponSet set(int id) { + return id >= 0 && id < sets.size() ? sets.get(id) : null; + } + + public int indexOf(String weapon) { + for (int i = 0; i < sets.size(); i++) { + if (sets.get(i).name().equalsIgnoreCase(weapon)) { + return i; + } + } + return -1; + } + + public @Nullable WeaponSet byName(String weapon) { + return set(indexOf(weapon)); + } + + public @Nullable Guidance guidance(int id) { + return id >= 0 && id < guidance.length ? guidance[id] : null; + } + + /** A vehicle does not lock its own crew or cargo. */ + private boolean canLock(Entity entity) { + return entity != vehicle && !vehicle.hasPassenger(entity); + } + + // Selection + + public int selected(int seat) { + return seat >= 0 && seat < selected.length ? selected[seat] : -1; + } + + public @Nullable WeaponSet current(int seat) { + return set(selected(seat)); + } + + /** The seat that operates a weapon: its own seat, or the pilot while that seat has no player. */ + public int operatorSeat(int id) { + WeaponSet set = set(id); + if (set == null) { + return -1; + } + WeaponMount mount = set.firstMount(); + if (mount.usableByPilot() && !(vehicle.occupant(mount.seatIndex()) instanceof Player)) { + return 0; + } + return mount.seatIndex(); + } + + /** {@code getNextWeaponID}: the next weapon {@code seat} may use, stepping forwards or back. */ + public int next(int seat, int step) { + if (sets.isEmpty() || seat < 0) { + return -1; + } + int id = selected(seat); + for (int i = 0; i < sets.size(); i++) { + id = step >= 0 ? (id + 1) % sets.size() : id > 0 ? id - 1 : sets.size() - 1; + WeaponMount mount = sets.get(id).firstMount(); + int owner = mount.seatIndex(); + if (owner == seat || seat == 0 && mount.usableByPilot() && !(vehicle.occupant(owner) instanceof Player)) { + return id; + } + } + return -1; + } + + /** {@code initCurrentWeapon}: a player took a seat. Server only. */ + public void onSeated(Entity entity, int seat) { + if (seat < 0 || seat >= selected.length) { + return; + } + selected[seat] = -1; + if (entity instanceof Player) { + switchTo(entity, seat, next(seat, 1)); + } else { + vehicle.publishWeaponSelection(); + } + } + + public void onLeft(int seat) { + if (seat >= 0 && seat < selected.length) { + selected[seat] = -1; + vehicle.publishWeaponSelection(); + } + } + + /** {@code switchWeapon}. Server only; the selection reaches clients through synced data. */ + public void switchTo(Entity user, int seat, int id) { + if (seat < 0 || seat >= selected.length || sets.isEmpty()) { + return; + } + WeaponSet before = current(seat); + if (before != null) { + before.reload(); + } + selected[seat] = Math.min(id, sets.size() - 1); + WeaponSet now = current(seat); + if (now != null) { + now.onSwitchWeapon(vehicle.hasInfiniteAmmo(user)); + } + vehicle.publishWeaponSelection(); + } + + public String encodeSelection() { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < selected.length; i++) { + if (i > 0) { + b.append(','); + } + b.append(selected[i]); + } + return b.toString(); + } + + /** Applies the server's selection on a client, running the switch delay where it changed. */ + public void decodeSelection(String encoded, @Nullable Entity localPlayer) { + String[] parts = encoded.isEmpty() ? new String[0] : encoded.split(","); + for (int i = 0; i < selected.length; i++) { + int id; + try { + id = i < parts.length ? Integer.parseInt(parts[i]) : -1; + } catch (NumberFormatException e) { + id = -1; + } + if (selected[i] != id) { + selected[i] = id; + WeaponSet set = set(id); + Entity occupant = vehicle.occupant(i); + if (set != null && occupant != null && occupant == localPlayer) { + set.onSwitchWeapon(vehicle.hasInfiniteAmmo(occupant)); + Guidance g = guidance(id); + if (g != null) { + g.clear(); + } + } + } + } + } + + // Firing + + /** + * {@code useCurrentWeapon}: fires the selected weapon of {@code seat} if it is ready. The + * trigger decides what the shot does on this side. + */ + public boolean use(Entity user, int seat, Shot shot, WeaponSet.Trigger trigger) { + int id = selected(seat); + WeaponSet set = set(id); + if (set == null || !set.canUse() || vehicle.isDestroyed()) { + return false; + } + shot.infiniteAmmo = vehicle.hasInfiniteAmmo(user); + int mount = set.currentMount(); + // As the original did, face the vehicle where the user looks for the length of the shot. + boolean followUser = vehicle.aimsWithUser(set); + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + if (followUser) { + vehicle.setYRot(user.getYRot()); + vehicle.setXRot(user.getXRot()); + } + boolean fired; + try { + fired = set.use(shot, (s, m, sh) -> { + Vec3 muzzle = muzzleOffset(s, m, 1.0F); + sh.x += muzzle.x; + sh.y += muzzle.y; + sh.z += muzzle.z; + return trigger.fire(s, m, sh); + }); + } finally { + if (followUser) { + vehicle.setYRot(hullYaw); + vehicle.setXRot(hullPitch); + } + } + if (!fired) { + return false; + } + String group = set.definition().group; + if (!group.isEmpty()) { + for (WeaponSet other : sets) { + if (other != set && group.equals(other.definition().group)) { + other.waitForGroup(shot.reload); + } + } + } + if (!clientSide) { + int bit = bitOf(id, mount); + if (bit < 32) { + useStat |= 1 << bit; + } + } + return true; + } + + /** World offset of a mount from the vehicle origin; turret mounts turn with the turret. */ + public Vec3 muzzleOffset(WeaponSet set, int mount, float partialTicks) { + WeaponMount m = set.mounts().get(mount); + Vector3 local = m.turret() ? vehicle.onTurret(def, m.position(), partialTicks) : m.position(); + Vector3f v = vehicle.toWorldOffset(local); + return new Vec3(v.x, v.y, v.z); + } + + private int bitOf(int id, int mount) { + int shift = 0; + for (int i = 0; i < id; i++) { + shift += sets.get(i).mounts().size(); + } + return shift + mount; + } + + /** Whether a mount is cooling down after a shot; missile parts are hidden meanwhile. */ + public boolean isMountUsed(int id, int mount) { + int bit = bitOf(id, mount); + return bit < 32 && (useStat & 1 << bit) != 0; + } + + public int useStat() { + return useStat; + } + + public void setUseStat(int stat) { + useStat = stat; + } + + public void switchMode(int seat) { + WeaponSet set = current(seat); + if (set != null) { + set.switchMode(); + } + } + + // Ticking + + /** {@code updateWeapons} and {@code updateWeaponsRotation}, once per tick on both sides. */ + public void tick() { + if (sets.isEmpty()) { + flares.tick(); + return; + } + int previous = prevUseStat; + if (!clientSide) { + useStat |= usedStat(); + vehicle.publishWeaponUse(useStat); + } else { + useStat = vehicle.syncedWeaponUse(); + } + prevUseStat = useStat; + + float yaw = Mth.wrapDegrees(vehicle.getYRot()); + float pitch = Mth.wrapDegrees(vehicle.getXRot()); + int bit = 0; + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + boolean longDelay = set.isLongDelay(); + boolean anyUsed = false; + for (int mount = 0; mount < set.mounts().size(); mount++) { + boolean wasUsed = bit < 32 && (previous & 1 << bit) != 0; + boolean used = bit < 32 && (useStat & 1 << bit) != 0; + if (longDelay && wasUsed && used) { + used = false; + } + anyUsed |= used; + if (!wasUsed && used && set.definition().recoil > 0.0F) { + recoilCount = 30; + recoilValue = set.definition().recoil; + recoilYaw = set.rotationYaw; + } + if (clientSide && used) { + muzzleEffects(set, mount, yaw); + } + set.tickRecoil(mount, used); + bit++; + } + set.tick(anyUsed); + if (!clientSide) { + ServerFiring.tick(vehicle, set); + } + updateRotation(set); + } + if (!clientSide) { + useStat = 0; + syncAmmo(); + } else { + for (Guidance g : guidance) { + if (g != null) { + g.tick(); + } + } + } + if (recoilCount > 0) { + recoilCount--; + } + tickBays(); + if (clientSide) { + smoke(); + } + flares.tick(); + } + + private int usedStat() { + int stat = 0; + int bit = 0; + for (WeaponSet set : sets) { + for (int mount = 0; mount < set.mounts().size() && bit < 32; mount++) { + stat |= set.isUsed(mount) ? 1 << bit : 0; + bit++; + } + } + return stat; + } + + /** Turret aim of one set, following whoever operates it, within the mount's traverse limits. */ + private void updateRotation(WeaponSet set) { + if (vehicle.isDestroyed()) { + return; + } + WeaponMount mount = set.firstMount(); + int seat = mount.seatIndex(); + Entity operator = vehicle.occupant(seat); + if (mount.usableByPilot() && !(operator instanceof Player)) { + operator = vehicle.occupant(0); + } + float hullYaw = Mth.wrapDegrees(vehicle.getYRot()); + float hullPitch = Mth.wrapDegrees(vehicle.getXRot()); + float turretYaw = vehicle.turretYaw(); + if (operator instanceof Player) { + if ((int) mount.minYaw() != 0 || (int) mount.maxYaw() != 0) { + float ty = mount.turret() ? turretYaw : 0.0F; + float ey = Mth.wrapDegrees(operator.getYRot() - hullYaw - mount.defaultYaw() - ty); + if (Math.abs((int) mount.minYaw()) < 360 && Math.abs((int) mount.maxYaw()) < 360) { + float targetYaw = Mth.clamp(ey, mount.minYaw(), mount.maxYaw()); + float wy = set.rotationYaw - mount.defaultYaw() - ty; + if (targetYaw < wy) { + wy = wy - targetYaw > 15.0F ? wy - 15.0F : targetYaw; + } else if (targetYaw > wy) { + wy = targetYaw - wy > 15.0F ? wy + 15.0F : targetYaw; + } + set.rotationYaw = wy + mount.defaultYaw() + ty; + } else { + set.rotationYaw = ey + ty; + } + } + float ep = Mth.wrapDegrees(operator.getXRot() - hullPitch); + set.rotationPitch = Mth.clamp(ep, mount.minPitch(), mount.maxPitch()); + set.rotationTurretYaw = 0.0F; + } else { + set.rotationTurretYaw = turretYaw; + } + } + + /** {@code updateRecoil}: the hull kick of a big gun, applied on the simulating side. */ + public float @Nullable [] recoilKick() { + if (recoilCount < 12) { + return null; + } + float rel = (float) Math.toRadians(recoilYaw - vehicle.roll()); + float kick = Mth.cos((float) Math.toRadians(recoilCount * 6)) * recoilValue; + return new float[]{kick * Mth.cos(rel), kick * Mth.sin(rel)}; + } + + /** {@code updateWeaponBay}: bays swing open while a seated crew member has their weapon selected. */ + private void tickBays() { + for (int i = 0; i < bayAngle.length; i++) { + WeaponBayPart bay = def.weaponBays.get(i); + boolean open = false; + for (String weapon : bay.weapons()) { + int id = indexOf(weapon); + for (int seat = 0; seat < selected.length && id >= 0; seat++) { + if (selected[seat] == id && vehicle.occupant(seat) != null) { + open = true; + } + } + } + prevBayAngle[i] = bayAngle[i]; + bayAngle[i] = Mth.clamp(bayAngle[i] + (open ? 3.0F : -3.0F), 0.0F, 90.0F); + } + } + + public float bayAngle(int bay, float partialTicks) { + return bay < bayAngle.length ? Mth.lerp(partialTicks, prevBayAngle[bay], bayAngle[bay]) : 0.0F; + } + + // Effects, client only + + /** {@code spawnParticleMuzzleFlash} plus the spent case of guns with {@code SetCartridge}. */ + private void muzzleEffects(WeaponSet set, int mount, float hullYaw) { + Level level = vehicle.level(); + WeaponDefinition w = set.definition(); + Vec3 offset = muzzleOffset(set, mount, 1.0F); + double px = vehicle.getX() + offset.x; + double py = vehicle.getY() + offset.y; + double pz = vehicle.getZ() + offset.z; + Vec3 dir = Vec3.directionFromRotation(vehicle.getXRot() + set.rotationPitch, hullYaw + set.rotationYaw); + for (MuzzleFlash mf : w.muzzleSmokes) { + double x = px + dir.x * mf.distance(); + double y = py + dir.y * mf.distance(); + double z = pz + dir.z * mf.distance(); + for (int i = 0; i < mf.count(); i++) { + double r = mf.range(); + level.addParticle(mf.size() >= 2.0F ? ParticleTypes.LARGE_SMOKE : ParticleTypes.SMOKE, + x + (random.nextDouble() - 0.5) * r, y + (random.nextDouble() - 0.5) * r, z + (random.nextDouble() - 0.5) * r, + (random.nextDouble() - 0.5) * 0.05, 0.02, (random.nextDouble() - 0.5) * 0.05); + } + } + for (MuzzleFlash mf : w.muzzleFlashes) { + int rgb = mf.color().argb() & 0xFFFFFF; + level.addParticle(new DustParticleOptions(rgb, Mth.clamp(mf.size() * 4.0F, 0.5F, 4.0F)), + px + dir.x * mf.distance(), py + dir.y * mf.distance(), pz + dir.z * mf.distance(), 0.0, 0.0, 0.0); + } + if (w.cartridge != null) { + Vec3 motion = vehicle.getDeltaMovement(); + Vec3 side = Vec3.directionFromRotation(0.0F, hullYaw + set.rotationYaw + w.cartridge.yaw() + 90.0F) + .scale(0.1 + w.cartridge.acceleration()); + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, new ItemStack(Items.GOLD_NUGGET)), + px, py, pz, motion.x + side.x, motion.y + 0.1, motion.z + side.z); + } + } + + /** {@code onUpdate_ParticleSmoke}: smoke generators trail while the pilot has them selected. */ + private void smoke() { + WeaponSet set = current(0); + if (set == null || set.definition().type != WeaponType.SMOKE || vehicle.visibleThrottle() <= 0.1F) { + return; + } + WeaponDefinition w = set.definition(); + Vec3 back = Vec3.directionFromRotation(vehicle.getXRot(), vehicle.getYRot()).reverse(); + int rgb = w.color.argb() & 0xFFFFFF; + for (int mount = 0; mount < set.mounts().size(); mount++) { + if (random.nextFloat() > vehicle.visibleThrottle() * 1.5F) { + continue; + } + Vec3 offset = muzzleOffset(set, mount, 1.0F); + for (int i = 0; i < w.smokeNum; i++) { + vehicle.level().addParticle(new DustParticleOptions(rgb, Mth.clamp(w.smokeSize * 2.0F, 0.5F, 4.0F)), + vehicle.getX() + offset.x + back.x, vehicle.getY() + offset.y + back.y, vehicle.getZ() + offset.z + back.z, + back.x * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.y * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.z * w.acceleration + (random.nextDouble() - 0.5) * 0.2); + } + } + } + + // Ammunition + + /** Sends magazine and reserve counts to the crew whenever they change. Server only. */ + private void syncAmmo() { + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + if (set.ammo() != sentAmmo[id] || set.restAmmo() != sentRest[id]) { + sentAmmo[id] = set.ammo(); + sentRest[id] = set.restAmmo(); + WeaponStatePacket packet = new WeaponStatePacket(vehicle.getId(), id, set.ammo(), set.restAmmo()); + for (Entity passenger : vehicle.getPassengers()) { + if (passenger instanceof ServerPlayer player) { + ModNetwork.sendToPlayer(player, packet); + } + } + } + } + } + + /** Forces a full resend, for a player who just boarded. */ + public void resendAmmo() { + Arrays.fill(sentAmmo, -1); + } + + public void applyAmmo(int id, int ammo, int rest) { + WeaponSet set = set(id); + if (set != null) { + set.setAmmo(ammo); + set.setRestAmmo(rest); + } + } + + /** {@code reloadAllWeapon}: tops up every magazine from its reserve. */ + public void reloadAll() { + for (WeaponSet set : sets) { + set.reloadMagazine(); + } + } + + public int[] saveAmmo() { + int[] out = new int[sets.size() * 2]; + for (int i = 0; i < sets.size(); i++) { + out[i * 2] = sets.get(i).ammo(); + out[i * 2 + 1] = sets.get(i).restAmmo(); + } + return out; + } + + public void loadAmmo(int[] saved) { + for (int i = 0; i < sets.size() && i * 2 + 1 < saved.length; i++) { + sets.get(i).setAmmo(Math.min(saved[i * 2], sets.get(i).magazineSize())); + sets.get(i).setRestAmmo(saved[i * 2 + 1]); + } + } +} From e8a6014da033e6ff2fd70c6d7a49b49e1f9f1815 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:19 +0800 Subject: [PATCH 1066/1461] Add weapon/WeaponSet.java to the 1.21.5 shared sources --- .../jnhprojects/mcheli/weapon/WeaponSet.java | 505 ++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java new file mode 100644 index 00000000..3f539030 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java @@ -0,0 +1,505 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * One selectable weapon of a vehicle: every mount that fires the same weapon, with the magazine, + * reload, heat and recoil they share. A port of {@code MCH_WeaponSet} together with the parts of + * {@code MCH_WeaponBase} and {@code MCH_WeaponCreator} that are not specific to a weapon type. + * + *

Both sides keep one. The server's is authoritative for ammunition and decides what is fired; + * the client's runs with the slightly longer intervals the original used there, so the client never + * asks to fire faster than the server accepts. + */ +public final class WeaponSet { + private static final int SWITCH_WAIT = 15; + + /** Fires one mount. Returns whether the shot happened, which consumes a round. */ + @FunctionalInterface + public interface Trigger { + boolean fire(WeaponSet set, int mount, Shot shot); + } + + private final RandomSource random = RandomSource.create(); + private final WeaponDefinition def; + private final List mounts; + private final boolean clientSide; + + public final int interval; + public final int heatCount; + private final int numMode; + + private int currentMount; + private int mode; + private int ammo; + private int restAmmo; + public int currentHeat; + private int cooldownSpeed = 1; + public int countWait; + public int countReloadWait; + private final int[] lastUsedCount; + /** Ticks since each mount last fired, {@code MCH_WeaponBase.tick}. */ + final int[] ticks; + public int soundWait; + /** Whether the shot being fired may play its sound, see {@code SoundDelay}. */ + public boolean soundAllowed = true; + /** The vehicle's own sound for this weapon, from {@code WeaponSound}. */ + public @Nullable WeaponSound ownSound; + + /** Aim relative to the hull, following the crew member who operates the weapon. */ + public float rotationYaw; + public float rotationPitch; + public float prevRotationYaw; + public float prevRotationPitch; + public final float defaultRotationYaw; + /** How far behind the pilot's view a turret weapon lags, for drawing. */ + public float rotationTurretYaw; + + public float barrelSpeed; + public float barrel; + public float prevBarrel; + private final Recoil[] recoil; + + // Close air support run, server only. See ServerFiring. + @Nullable Entity casUser; + @Nullable Entity casShooter; + double casX; + double casY; + double casZ; + int casDirection; + int casStartTick; + int casCount = 3; + + public WeaponSet(WeaponDefinition def, List mounts, boolean clientSide) { + this.def = def; + this.mounts = List.copyOf(mounts); + this.clientSide = clientSide; + this.lastUsedCount = new int[mounts.size()]; + this.ticks = new int[mounts.size()]; + this.recoil = new Recoil[mounts.size()]; + for (int i = 0; i < recoil.length; i++) { + recoil[i] = new Recoil(def.recoilBufCount, def.recoilBufCountSpeed); + } + this.defaultRotationYaw = mounts.isEmpty() ? 0.0F : mounts.getFirst().defaultYaw(); + this.rotationYaw = defaultRotationYaw; + this.prevRotationYaw = defaultRotationYaw; + + int wait = def.delay; + if (def.type == WeaponType.CAS && wait > -250) { + wait = -250; + } + int heat = def.heatCount; + if (def.maxHeatCount > 0 && heat < 2) { + heat = 2; + } + if (clientSide) { + wait = clientInterval(wait); + heat++; + } + this.interval = wait; + this.heatCount = heat; + this.numMode = def.type == WeaponType.MACHINE_GUN_2 && def.explosion == 0 ? 0 : def.modeNum; + } + + /** The original gave the client longer intervals so its requests never outran the server. */ + private static int clientInterval(int wait) { + if (wait < 4) { + return wait + 1; + } else if (wait < 7) { + return wait + 2; + } else if (wait < 10) { + return wait + 3; + } else if (wait < 20) { + return wait + 6; + } + wait += 10; + return wait >= 40 ? -wait : wait; + } + + public WeaponDefinition definition() { + return def; + } + + public String name() { + return def.name(); + } + + public List mounts() { + return mounts; + } + + public WeaponMount firstMount() { + return mounts.getFirst(); + } + + public int currentMount() { + return currentMount; + } + + // Ammunition + + public int ammo() { + return ammo; + } + + public int magazineSize() { + return def.round; + } + + public int restAmmo() { + return restAmmo; + } + + public int maxAmmo() { + return def.maxAmmo; + } + + public void setAmmo(int n) { + ammo = n; + } + + /** Everything not in the magazine, capped so magazine plus reserve never exceed {@code MaxAmmo}. */ + public void setRestAmmo(int n) { + restAmmo = Math.min(n, def.maxAmmo - ammo); + } + + /** {@code supplyRestAllAmmo}: one resupply adds {@code SuppliedNum} rounds. */ + public boolean supply() { + if (restAmmo + ammo < def.maxAmmo) { + setRestAmmo(restAmmo + ammo + def.suppliedNum); + return true; + } + return false; + } + + public boolean isInPreparation() { + return countWait < 0 || countReloadWait > 0; + } + + public boolean isReloading() { + return countReloadWait > 0; + } + + public boolean canUse() { + return countWait == 0; + } + + public boolean isLongDelay() { + return def.delay > 4; + } + + public boolean isOverheated() { + return def.maxHeatCount > 0 && currentHeat >= def.maxHeatCount; + } + + public void reload() { + if (magazineSize() > 0 && ammo < magazineSize() && def.reloadTime > 0) { + countReloadWait = def.reloadTime; + if (clientSide) { + ammo = 0; + } else { + countReloadWait = Math.max(countReloadWait - 20, 1); + } + } + } + + public void reloadMagazine() { + int needed = magazineSize() - ammo; + if (needed > 0) { + needed = Math.min(needed, restAmmo); + ammo += needed; + setRestAmmo(restAmmo - needed); + } + } + + // Modes + + public int mode() { + return def.fixMode > 0 ? def.fixMode : mode; + } + + public boolean switchMode() { + if (def.fixMode > 0) { + return false; + } + int before = mode(); + mode = numMode > 0 ? (mode() + 1) % numMode : 0; + if (before == mode()) { + return false; + } + waitAfterSwitch(SWITCH_WAIT); + return true; + } + + /** {@code getName}: the display name with the mode suffix the weapon class added. */ + public String displayName() { + String suffix = switch (def.type) { + case MACHINE_GUN_2 -> mode() == 0 ? "" : " [HE]"; + case ROCKET -> mode() == 0 ? "" : " [HEIAP]"; + case AT_MISSILE -> mode() == 1 ? " [TA]" : ""; + case TV_MISSILE -> switch (mode()) { + case 0 -> " [TV]"; + case 2 -> " [TA]"; + default -> ""; + }; + case null, default -> ""; + }; + return def.displayName + suffix; + } + + public void onSwitchWeapon(boolean infiniteAmmo) { + waitAfterSwitch(clientSide ? SWITCH_WAIT + 10 : SWITCH_WAIT); + currentMount = 0; + if (infiniteAmmo) { + ammo = magazineSize(); + } + } + + private void waitAfterSwitch(int wait) { + if (countWait >= -wait) { + countWait = countWait > wait ? -countWait : -wait; + } + } + + /** Another weapon of the same {@code Group} fired; this one waits and reloads with it. */ + public void waitForGroup(boolean reload) { + countWait = interval; + countReloadWait = 0; + if (reload && magazineSize() > 0 && def.reloadTime > 0) { + countReloadWait = clientSide ? def.reloadTime : Math.max(def.reloadTime - 20, 1); + } + } + + /** + * {@code getLandInDistance}: how far out, measured flat, a shell fired now would come down, + * stepping its arc a tick at a time as the projectile does; -1 if it would not come down in + * range. Mortars show it on the HUD. + */ + public double landingDistance(Entity shooter) { + if (def.gravity >= 0.0F || mounts.isEmpty()) { + return -1.0; + } + WeaponMount mount = mounts.get(currentMount); + float yaw = Mth.wrapDegrees(shooter.getYRot() + rotationYaw + mount.yaw()); + float pitch = Mth.wrapDegrees(shooter.getXRot() + rotationPitch + mount.pitch()); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + double acc = Math.min(def.acceleration, 4.0F); + double my = dir.y * def.acceleration; + if (my <= 0.0 || acc <= 0.0) { + return -1.0; + } + double mx = dir.x * def.acceleration; + double mz = dir.z * def.acceleration; + double gravity = def.gravity * (def.acceleration / acc); + double steps = my / def.gravity; + if (steps < -12.0) { + double f = steps / -12.0; + mx *= f; + my *= f; + mz *= f; + gravity *= f * f * 0.95; + } + Level level = shooter.level(); + double x = shooter.getX(); + double y = shooter.getY() + 3.0; + double z = shooter.getZ(); + for (int i = 0; i < 50; i++) { + Vec3 from = new Vec3(x, y, z); + Vec3 to = new Vec3(x + mx, y + my, z + mz); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shooter)); + if (hit.getType() == HitResult.Type.BLOCK) { + return Math.hypot(hit.getBlockPos().getX() - shooter.getX(), hit.getBlockPos().getZ() - shooter.getZ()); + } + my += gravity; + x += mx; + y += my; + z += mz; + if (y < shooter.getY()) { + return Math.hypot(x - shooter.getX(), z - shooter.getZ()); + } + } + return -1.0; + } + + // Firing + + /** + * Fires the next mount if the set is ready. The aim is the shooter's rotation plus this set's + * turret aim and the mount's fixed offset, scattered by {@code Accuracy}. + */ + public boolean use(Shot shot, Trigger trigger) { + if (magazineSize() > 0 && ammo <= 0 || isOverheated()) { + return false; + } + WeaponMount mount = mounts.get(currentMount); + soundAllowed = soundWait == 0; + float yaw = shot.shooter.getYRot() + rotationYaw + mount.yaw(); + float pitch = shot.shooter.getXRot() + rotationPitch + mount.pitch(); + if (def.accuracy > 0.0F) { + yaw += (random.nextFloat() - 0.5F) * def.accuracy; + pitch += (random.nextFloat() - 0.5F) * def.accuracy; + } + shot.yaw = Mth.wrapDegrees(yaw); + shot.pitch = Mth.wrapDegrees(pitch); + if (!trigger.fire(this, currentMount, shot)) { + return false; + } + ticks[currentMount] = 0; + + if (def.maxHeatCount > 0) { + cooldownSpeed = 1; + currentHeat += heatCount; + if (currentHeat >= def.maxHeatCount) { + currentHeat += 30; + } + } + if (def.soundDelay > 0 && soundWait == 0) { + soundWait = def.soundDelay; + } + lastUsedCount[currentMount] = Math.abs(interval); + if (isCooldownReloadTime() && def.reloadTime - 10 > lastUsedCount[currentMount]) { + lastUsedCount[currentMount] = def.reloadTime - 10; + } + currentMount = (currentMount + 1) % mounts.size(); + countWait = interval; + countReloadWait = 0; + if (ammo > 0) { + ammo--; + } + if (ammo <= 0) { + if (shot.infiniteAmmo && restAmmo < magazineSize()) { + setRestAmmo(magazineSize()); + } + reload(); + shot.reload = true; + } + return true; + } + + /** Missiles keep their mount empty for the whole reload, which hides the missile part meanwhile. */ + private boolean isCooldownReloadTime() { + return def.type == WeaponType.AA_MISSILE || def.type == WeaponType.AS_MISSILE || def.type == WeaponType.AT_MISSILE; + } + + /** Whether {@code mount} fired recently enough to show muzzle effects or keep a missile hidden. */ + public boolean isUsed(int mount) { + if (mount >= lastUsedCount.length) { + return false; + } + int count = lastUsedCount[mount]; + return interval >= 4 && count > interval / 2 || count >= 4; + } + + public int ticksSinceShot(int mount) { + return ticks[mount]; + } + + // Ticking + + /** {@code MCH_WeaponSet.update}, once per tick on both sides. */ + public void tick(boolean used) { + if (countReloadWait > 0) { + countReloadWait--; + if (countReloadWait == 0) { + reloadMagazine(); + } + } + for (int i = 0; i < lastUsedCount.length; i++) { + if (lastUsedCount[i] > 0) { + if (lastUsedCount[i] != 4 || currentMount == 0 && canUse() && (ammo > 0 || def.maxAmmo <= 0)) { + lastUsedCount[i]--; + } + } + } + if (currentHeat > 0) { + if (currentHeat < def.maxHeatCount) { + cooldownSpeed++; + } + currentHeat = Math.max(currentHeat - (cooldownSpeed / 20 + 1), 0); + } + if (countWait > 0) { + countWait--; + } else if (countWait < 0) { + countWait++; + } + prevRotationYaw = rotationYaw; + prevRotationPitch = rotationPitch; + if (countWait != 0) { + for (int i = 0; i < ticks.length; i++) { + ticks[i]++; + } + } + if (soundWait > 0) { + soundWait--; + } + + if (used && barrelSpeed < 75.0F) { + barrelSpeed = Math.min(barrelSpeed + 25 + random.nextInt(3), 74.0F); + } + prevBarrel = barrel; + barrel += barrelSpeed; + if (barrel >= 360.0F) { + barrel -= 360.0F; + prevBarrel -= 360.0F; + } + if (barrelSpeed > 0.0F) { + barrelSpeed = Math.max(barrelSpeed - 1.48F, 0.0F); + } + } + + /** {@code updateWeapon}: the recoil kick of one mount. */ + public void tickRecoil(int mount, boolean used) { + Recoil r = recoil[mount]; + r.prev = r.value; + if (used && r.count <= 0) { + r.count = r.max; + } + if (r.count > 0) { + if (r.max <= 1) { + r.value = 1.0F; + } else if (r.max == 2) { + r.value = r.count == 2 ? 1.0F : 0.6F; + } else { + if (r.count > r.max / 2) { + r.count -= r.speed; + } + r.value = Mth.sin((float) r.count / r.max * Mth.PI); + } + r.count--; + } else { + r.value = 0.0F; + } + } + + public float recoil(int mount, float partialTicks) { + Recoil r = recoil[Math.min(mount, recoil.length - 1)]; + return Mth.lerp(partialTicks, r.prev, r.value); + } + + private static final class Recoil { + final int max; + final int speed; + int count; + float value; + float prev; + + Recoil(int max, int speed) { + this.max = max; + this.speed = speed; + } + } +} From 9e47918934c3ba535533be42c68b13601ec75260 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:20 +0800 Subject: [PATCH 1067/1461] Add workshop/TestFlight.java to the 1.21.5 shared sources --- .../mcheli/workshop/TestFlight.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java diff --git a/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java new file mode 100644 index 00000000..8705ed01 --- /dev/null +++ b/minecraft-1.21.5/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java @@ -0,0 +1,62 @@ +package com.jnhprojects.mcheli.workshop; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.phys.Vec3; + +/** + * Flying a vehicle straight from the workshop. The pack was just saved on the machine this server + * runs on, so loading it again is enough. That only holds for the owner of a single player or LAN + * game: a dedicated server does not get a pack until an operator approves it, and then it is no + * longer a test, so the screen does not offer it there and the server refuses it too. + */ +public final class TestFlight { + /** How far up to look for room when the vehicle does not fit where the player stands. */ + private static final int MAX_RISE = 48; + + private TestFlight() { + } + + public static void start(ServerPlayer player, String vehicle) { + MinecraftServer server = player.level().getServer(); + if (!server.isSingleplayerOwner(player.getGameProfile())) { + player.displayClientMessage(Component.translatable("message.mcheli.workshop.test_flight_owner_only"), true); + return; + } + if (player.isPassenger()) { + player.displayClientMessage(Component.translatable("message.mcheli.workshop.test_flight_riding"), true); + return; + } + ContentReload.reload(server); + AircraftDefinition def = MCHeli.content().any(vehicle); + if (def == null) { + player.sendSystemMessage(Component.translatable("message.mcheli.workshop.test_flight_no_vehicle", vehicle)); + MCHeli.packs().reportProblems(player::sendSystemMessage); + return; + } + ServerLevel level = player.serverLevel(); + AircraftEntity aircraft = ModEntities.typeFor(def).create(level, EntitySpawnReason.COMMAND); + if (aircraft == null) { + return; + } + aircraft.setType(def); + Vec3 start = player.position(); + for (int rise = 0; rise <= MAX_RISE; rise++) { + aircraft.snapTo(start.x, start.y + rise, start.z, player.getYRot(), 0.0F); + if (level.noCollision(aircraft, aircraft.getBoundingBox())) { + break; + } + } + aircraft.startTestFlight(start); + level.addFreshEntity(aircraft); + player.startRiding(aircraft, true); + } +} From 7a767f6eaea3fe7f9bf04f616e0c94a2b199574f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:20 +0800 Subject: [PATCH 1068/1461] Add assets/mcheli/blockstates/drafting_table.json to the 1.21.5 shared sources --- .../resources/assets/mcheli/blockstates/drafting_table.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/blockstates/drafting_table.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/blockstates/drafting_table.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/blockstates/drafting_table.json new file mode 100644 index 00000000..82992af9 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/blockstates/drafting_table.json @@ -0,0 +1,5 @@ +{ + "multipart": [ + { "apply": { "model": "mcheli:block/drafting_table" } } + ] +} From b6d2c4b30d4e364d3aa326d24ef3641846c69e07 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:21 +0800 Subject: [PATCH 1069/1461] Add assets/mcheli/items/drafting_table.json to the 1.21.5 shared sources --- .../main/resources/assets/mcheli/items/drafting_table.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/items/drafting_table.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/items/drafting_table.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/drafting_table.json new file mode 100644 index 00000000..a3afc683 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/drafting_table.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "mcheli:block/drafting_table" + } +} From 9692a4185b7e6ece5f3e75a64398b199cdff2032 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:21 +0800 Subject: [PATCH 1070/1461] Add assets/mcheli/items/helicopter.json to the 1.21.5 shared sources --- .../src/main/resources/assets/mcheli/items/helicopter.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/items/helicopter.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/items/helicopter.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/helicopter.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/helicopter.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From b09f990cb67a298dd5ea8a81565a2d0f599833c5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:21 +0800 Subject: [PATCH 1071/1461] Add assets/mcheli/items/plane.json to the 1.21.5 shared sources --- .../src/main/resources/assets/mcheli/items/plane.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/items/plane.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/items/plane.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/plane.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/plane.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From 458ad47ad2640d55af3638396f97bea06fa7590b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:22 +0800 Subject: [PATCH 1072/1461] Add assets/mcheli/items/tank.json to the 1.21.5 shared sources --- .../src/main/resources/assets/mcheli/items/tank.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/items/tank.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/items/tank.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/tank.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/tank.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From d77a2b78bc2a3e21f88c21603f678cca7cd9b83c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:22 +0800 Subject: [PATCH 1073/1461] Add assets/mcheli/items/vehicle.json to the 1.21.5 shared sources --- .../src/main/resources/assets/mcheli/items/vehicle.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/items/vehicle.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/items/vehicle.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/vehicle.json new file mode 100644 index 00000000..24742eb7 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/items/vehicle.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "minecraft:item/barrier" + } +} From a3db3cf8a00f944af4696c8f30ff305ae36c28e9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:23 +0800 Subject: [PATCH 1074/1461] Add assets/mcheli/models/block/drafting_table.json to the 1.21.5 shared sources --- .../mcheli/models/block/drafting_table.json | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/models/block/drafting_table.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/models/block/drafting_table.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/models/block/drafting_table.json new file mode 100644 index 00000000..8dfa5bcc --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/models/block/drafting_table.json @@ -0,0 +1,27 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/oak_planks", + "legs": "minecraft:block/iron_block", + "board": "minecraft:block/oak_planks", + "paper": "minecraft:block/white_concrete" + }, + "elements": [ + { "from": [1, 0, 2], "to": [2.5, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 2], "to": [15, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [1, 0, 12.5], "to": [2.5, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 12.5], "to": [15, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [0, 10, 1], "to": [16, 11.5, 15], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { + "north": {"texture": "#board"}, "south": {"texture": "#board"}, "east": {"texture": "#board"}, "west": {"texture": "#board"}, + "up": {"texture": "#board"}, "down": {"texture": "#board"} } }, + { "from": [2, 11.5, 2.5], "to": [14, 11.6, 13.5], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { "up": {"texture": "#paper"} } } + ] +} From a291fe97c305eed4d5053c70de2df6aa60e5cd42 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:23 +0800 Subject: [PATCH 1075/1461] Add assets/mcheli/particles/glow.json to the 1.21.5 shared sources --- .../src/main/resources/assets/mcheli/particles/glow.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/particles/glow.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/particles/glow.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/particles/glow.json new file mode 100644 index 00000000..296e5397 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/particles/glow.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "mcheli:glow" + ] +} From cd7b18a2c896b539971fee0328be50be14662f40 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:24 +0800 Subject: [PATCH 1076/1461] Add assets/mcheli/particles/smoke.json to the 1.21.5 shared sources --- .../resources/assets/mcheli/particles/smoke.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/particles/smoke.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/particles/smoke.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/particles/smoke.json new file mode 100644 index 00000000..977381be --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/particles/smoke.json @@ -0,0 +1,16 @@ +{ + "textures": [ + "minecraft:big_smoke_0", + "minecraft:big_smoke_1", + "minecraft:big_smoke_2", + "minecraft:big_smoke_3", + "minecraft:big_smoke_4", + "minecraft:big_smoke_5", + "minecraft:big_smoke_6", + "minecraft:big_smoke_7", + "minecraft:big_smoke_8", + "minecraft:big_smoke_9", + "minecraft:big_smoke_10", + "minecraft:big_smoke_11" + ] +} From 16c971a696055ae5e69c275c0629e40a2b1057a9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:24 +0800 Subject: [PATCH 1077/1461] Write the thermal post effect in the 1.21.5 format --- .../assets/mcheli/post_effect/thermal.json | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/post_effect/thermal.json diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/post_effect/thermal.json b/minecraft-1.21.5/src/main/resources/assets/mcheli/post_effect/thermal.json new file mode 100644 index 00000000..e9341395 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/post_effect/thermal.json @@ -0,0 +1,36 @@ +{ + "targets": { + "swap": {} + }, + "passes": [ + { + "vertex_shader": "minecraft:post/blit", + "fragment_shader": "mcheli:post/thermal", + "inputs": [ + { + "sampler_name": "In", + "target": "minecraft:main" + } + ], + "output": "swap" + }, + { + "vertex_shader": "minecraft:post/blit", + "fragment_shader": "minecraft:post/blit", + "inputs": [ + { + "sampler_name": "In", + "target": "swap" + } + ], + "uniforms": [ + { + "name": "ColorModulate", + "type": "vec4", + "values": [1.0, 1.0, 1.0, 1.0] + } + ], + "output": "minecraft:main" + } + ] +} From f2b4aaed46b07a007774781a75a256b8f7a2b97e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:24 +0800 Subject: [PATCH 1078/1461] Read InSize as a plain uniform in the thermal shader on 1.21.5 --- .../assets/mcheli/shaders/post/thermal.fsh | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/shaders/post/thermal.fsh diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/shaders/post/thermal.fsh b/minecraft-1.21.5/src/main/resources/assets/mcheli/shaders/post/thermal.fsh new file mode 100644 index 00000000..91ec30c8 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/assets/mcheli/shaders/post/thermal.fsh @@ -0,0 +1,34 @@ +#version 150 + +// Thermal view, white hot. A post shader only sees colours, so heat is faked: bright and warm +// colours (fire, lava, skin, lamps) read hot, the sky and water cold, and outlines are drawn +// where the brightness changes sharply, like the original's "pencil" shader. + +uniform sampler2D InSampler; + +in vec2 texCoord; + +uniform vec2 InSize; + +out vec4 fragColor; + +const vec3 Gray = vec3(0.3, 0.59, 0.11); + +float heat(vec3 c) { + float luma = dot(c, Gray); + float warmth = clamp(c.r - c.b, 0.0, 1.0); + return clamp(luma * 0.7 + warmth * 0.6, 0.0, 1.0); +} + +void main() { + vec2 oneTexel = 1.0 / InSize; + float c = heat(texture(InSampler, texCoord).rgb); + float l = heat(texture(InSampler, texCoord - vec2(oneTexel.x, 0.0)).rgb); + float r = heat(texture(InSampler, texCoord + vec2(oneTexel.x, 0.0)).rgb); + float u = heat(texture(InSampler, texCoord - vec2(0.0, oneTexel.y)).rgb); + float d = heat(texture(InSampler, texCoord + vec2(0.0, oneTexel.y)).rgb); + float edge = clamp(abs(r - l) + abs(d - u), 0.0, 1.0); + + float value = smoothstep(0.08, 0.85, c) + edge * 0.6; + fragColor = vec4(vec3(clamp(value, 0.0, 1.0)), 1.0); +} From 7a848f51baa017d848aba1b193c9f5ce9a73ef23 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:25 +0800 Subject: [PATCH 1079/1461] Add assets/mcheli/textures/particle/glow.png to the 1.21.5 shared sources --- .../assets/mcheli/textures/particle/glow.png | Bin 0 -> 705 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-1.21.5/src/main/resources/assets/mcheli/textures/particle/glow.png diff --git a/minecraft-1.21.5/src/main/resources/assets/mcheli/textures/particle/glow.png b/minecraft-1.21.5/src/main/resources/assets/mcheli/textures/particle/glow.png new file mode 100644 index 0000000000000000000000000000000000000000..339b97292b3a86cec21a5a2c2d4e9159da1241c6 GIT binary patch literal 705 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFm-skIEGZjoxNt=CF&@0?BmDQ z$G05=nLjo)u{~0d=sc02G=rmQ;>?7sgruZLOlR&iZR9TC);p3q<+=OyiMy`WR_B%6 ze!zcpW%0hxt6uJ`UOHv3s-)z$2S*IQ{M+2aBkl2V^1+p-UiNMId~?@~iHS`VvGKGtbJ(?@PMX`Q_JU)-AEGyIw3c zQrRtbha-<|yVkj!cRxOeDwmZ>DLa>nOo)xwXnye(C@s6DY4^dsUB4m0R;efKDR26HaPb4r4<;3I zGNE&B-ud`Y+f9cz$|Q5W??V51i?`b=Utv|8`!Zyb_`%f=Ny@jd)@AxOp)i%w?!a<|&&Tjr8oN`JQToU~(`->fg1 zZdP9Nqf5}cIn2IwE7PsJ)h7Sugyi*n%i6MQ@`K~}e`S@^nPlVK#j7xFV z+_j8uPnwhRR~zlgnR{cs;OihI&w3VnZD^oOJGP2v)|RI;bPnXz1l3;M6Sw&D^QW!V zDOSc=cfEdS|A;M20|xneiH#;17nkTB-(PMJ>hSJ9`!Q!Lt-~I+X1}kkT&Mak7@_E? z^^gDD5jm{g_J>^m^K5+4zM_`ByS7X6x8|EK^8>{;-q`=xrFCc5-2VzEHJ@mS{q(#h zwlHh)?U((R_Hk6-{(1h>N~aR1kZ_?NOSWy;`Ple&>FE<27@gj_D0q6`GXA*nq@huc lV^{v7@AFerZkX0Ht_;k6d;0MSU0~8=@O1TaS?83{1OQn@Sb6{e literal 0 HcmV?d00001 From d68e35b66695099aa85b913a7ea8e157dea89d17 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:25 +0800 Subject: [PATCH 1080/1461] Add data/mcheli/damage_type/bullet.json to the 1.21.5 shared sources --- .../src/main/resources/data/mcheli/damage_type/bullet.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/damage_type/bullet.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/damage_type/bullet.json b/minecraft-1.21.5/src/main/resources/data/mcheli/damage_type/bullet.json new file mode 100644 index 00000000..92ee6766 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/damage_type/bullet.json @@ -0,0 +1,5 @@ +{ + "exhaustion": 0.1, + "message_id": "mcheli.bullet", + "scaling": "when_caused_by_living_non_player" +} From 3343cf877e547c4bb88ec6ca257ebd9f27447875 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:26 +0800 Subject: [PATCH 1081/1461] Add data/mcheli/loot_table/blocks/drafting_table.json to the 1.21.5 shared sources --- .../mcheli/loot_table/blocks/drafting_table.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json b/minecraft-1.21.5/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json new file mode 100644 index 00000000..610afa08 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/loot_table/blocks/drafting_table.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { "type": "minecraft:item", "name": "mcheli:drafting_table" } + ], + "conditions": [ + { "condition": "minecraft:survives_explosion" } + ] + } + ] +} From 352bd8d71beaaa40d52b34f78e030ffeced442f5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:26 +0800 Subject: [PATCH 1082/1461] Add data/mcheli/recipe/chain.json to the 1.21.5 shared sources --- .../main/resources/data/mcheli/recipe/chain.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/chain.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/chain.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/chain.json new file mode 100644 index 00000000..484f5463 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/chain.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": "#c:ingots/iron" + }, + "pattern": [ + "I I", + "III", + "I I" + ], + "result": { + "id": "mcheli:chain" + } +} From e9eeeec5759ed9546ce69f610ef5488b8cb1a90d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:27 +0800 Subject: [PATCH 1083/1461] Add data/mcheli/recipe/container.json to the 1.21.5 shared sources --- .../resources/data/mcheli/recipe/container.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/container.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/container.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/container.json new file mode 100644 index 00000000..4c3bdcd9 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/container.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "#c:chests/wooden", + "I": "#c:ingots/iron" + }, + "pattern": [ + "CCI" + ], + "result": { + "id": "mcheli:container" + } +} From 03130acc5e9c9091dc8942c4962f00219ccf789e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:27 +0800 Subject: [PATCH 1084/1461] Add data/mcheli/recipe/drafting_table.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/drafting_table.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/drafting_table.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/drafting_table.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/drafting_table.json new file mode 100644 index 00000000..f3d6324e --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/drafting_table.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "minecraft:crafting_table", + "F": "#minecraft:wooden_fences", + "P": "#minecraft:planks", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "R ", + "PCP", + "F F" + ], + "result": { + "id": "mcheli:drafting_table" + } +} From 4632952f5ef3f4d6877d5c44f985756f708486ac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:28 +0800 Subject: [PATCH 1085/1461] Add data/mcheli/recipe/fgm148.json to the 1.21.5 shared sources --- .../resources/data/mcheli/recipe/fgm148.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fgm148.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fgm148.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fgm148.json new file mode 100644 index 00000000..c2c61fd6 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fgm148.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "III", + "GR " + ], + "result": { + "id": "mcheli:fgm148" + } +} From 9f8f3f7b54776ef06848cd6e09d59f77fc9b5f1a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:28 +0800 Subject: [PATCH 1086/1461] Add data/mcheli/recipe/fgm148_bullet.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/fgm148_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fgm148_bullet.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fgm148_bullet.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fgm148_bullet.json new file mode 100644 index 00000000..c87b4800 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fgm148_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:gunpowders", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + " R ", + " I ", + " G " + ], + "result": { + "count": 2, + "id": "mcheli:fgm148_bullet" + } +} From a41dfd1e771e26810d2bfac4f9c05decaa6db640 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:28 +0800 Subject: [PATCH 1087/1461] Add data/mcheli/recipe/fim92.json to the 1.21.5 shared sources --- .../resources/data/mcheli/recipe/fim92.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fim92.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fim92.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fim92.json new file mode 100644 index 00000000..33a068e1 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fim92.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "G ", + "III", + "RI " + ], + "result": { + "id": "mcheli:fim92" + } +} From 40a0dd6e8753844ae83e89439657679e51bb8efe Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:29 +0800 Subject: [PATCH 1088/1461] Add data/mcheli/recipe/fim92_bullet.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/fim92_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fim92_bullet.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fim92_bullet.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fim92_bullet.json new file mode 100644 index 00000000..c472fd6e --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fim92_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:gunpowders", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "R ", + " I ", + " G" + ], + "result": { + "count": 2, + "id": "mcheli:fim92_bullet" + } +} From 942b5daa0d4b437c1ba27ad408df57ae0e8ebf21 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:29 +0800 Subject: [PATCH 1089/1461] Add data/mcheli/recipe/fuel.json to the 1.21.5 shared sources --- .../main/resources/data/mcheli/recipe/fuel.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fuel.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fuel.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fuel.json new file mode 100644 index 00000000..d076b041 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/fuel.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": "minecraft:coal", + "I": "#c:ingots/iron" + }, + "pattern": [ + "ICI", + "III" + ], + "result": { + "id": "mcheli:fuel" + } +} From c0f16bcff615e76b805eba8fb2d7a99aabf0416e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:30 +0800 Subject: [PATCH 1090/1461] Add data/mcheli/recipe/gltd.json to the 1.21.5 shared sources --- .../resources/data/mcheli/recipe/gltd.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/gltd.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/gltd.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/gltd.json new file mode 100644 index 00000000..104ac108 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/gltd.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": "#c:storage_blocks/iron", + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + " B ", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:gltd" + } +} From 90af3c7018236051263b833053f27959742e2d4a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:30 +0800 Subject: [PATCH 1091/1461] Add data/mcheli/recipe/m18_green.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/m18_green.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_green.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_green.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_green.json new file mode 100644 index 00000000..1046d9cf --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_green.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:green_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_green", + "minecraft:item_model": "mcheli:m18_green", + "minecraft:max_stack_size": 1 + } + } +} From 61dd23cca68c8ffc0753abd4c940730d2b6cd068 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:31 +0800 Subject: [PATCH 1092/1461] Add data/mcheli/recipe/m18_red.json to the 1.21.5 shared sources --- .../resources/data/mcheli/recipe/m18_red.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_red.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_red.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_red.json new file mode 100644 index 00000000..f98ac51f --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_red.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:red_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_red", + "minecraft:item_model": "mcheli:m18_red", + "minecraft:max_stack_size": 1 + } + } +} From df653af35e7250e10261005391aa9e43df2a0bbc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:31 +0800 Subject: [PATCH 1093/1461] Add data/mcheli/recipe/m18_violet.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/m18_violet.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_violet.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_violet.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_violet.json new file mode 100644 index 00000000..b05f48a7 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_violet.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:purple_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_violet", + "minecraft:item_model": "mcheli:m18_violet", + "minecraft:max_stack_size": 1 + } + } +} From 48833cffec99c36185a51472d3defccdb2274088 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:31 +0800 Subject: [PATCH 1094/1461] Add data/mcheli/recipe/m18_yellow.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/m18_yellow.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_yellow.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_yellow.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_yellow.json new file mode 100644 index 00000000..57ecd202 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/m18_yellow.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "minecraft:yellow_dye", + "Y": "#c:ingots/iron" + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_yellow", + "minecraft:item_model": "mcheli:m18_yellow", + "minecraft:max_stack_size": 1 + } + } +} From 4dd012b96aa412477d4e6eecb59daf2c773455ca Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:32 +0800 Subject: [PATCH 1095/1461] Add data/mcheli/recipe/parachute.json to the 1.21.5 shared sources --- .../resources/data/mcheli/recipe/parachute.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/parachute.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/parachute.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/parachute.json new file mode 100644 index 00000000..69eab191 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/parachute.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "S": "#c:strings", + "W": "#minecraft:wool" + }, + "pattern": [ + "WWW", + "S S", + " W " + ], + "result": { + "id": "mcheli:parachute" + } +} From 5bae0ab5f1d97da717969df9aee6f68255a96ecd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:32 +0800 Subject: [PATCH 1096/1461] Add data/mcheli/recipe/rangefinder.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/rangefinder.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/rangefinder.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/rangefinder.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/rangefinder.json new file mode 100644 index 00000000..58a13385 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/rangefinder.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "III", + "RGR", + "III" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From df0687016148dca11d4aa54bb30d74e573ecbf95 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:33 +0800 Subject: [PATCH 1097/1461] Add data/mcheli/recipe/rangefinder_recharge.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/rangefinder_recharge.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json new file mode 100644 index 00000000..d6bac29a --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/rangefinder_recharge.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "equipment", + "ingredients": [ + "mcheli:rangefinder", + "#c:dusts/redstone" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From ef51aed08e747fa0f795cd59f97c5aa4a6ae4424 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:33 +0800 Subject: [PATCH 1098/1461] Add data/mcheli/recipe/uav_station.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/uav_station.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/uav_station.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/uav_station.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/uav_station.json new file mode 100644 index 00000000..24b97d55 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/uav_station.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:storage_blocks/redstone" + }, + "pattern": [ + "III", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station" + } +} From 3a5046e985b9c81ebc0c42edc901e9e0dac47741 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:34 +0800 Subject: [PATCH 1099/1461] Add data/mcheli/recipe/uav_station2.json to the 1.21.5 shared sources --- .../data/mcheli/recipe/uav_station2.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/uav_station2.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/uav_station2.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/uav_station2.json new file mode 100644 index 00000000..74bf0d20 --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/uav_station2.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": "#c:gems/diamond", + "I": "#c:ingots/iron", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station2" + } +} From ee2287adcb0f0757eb10ef7046882daa2656ef05 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:34 +0800 Subject: [PATCH 1100/1461] Add data/mcheli/recipe/wrench.json to the 1.21.5 shared sources --- .../main/resources/data/mcheli/recipe/wrench.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/mcheli/recipe/wrench.json diff --git a/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/wrench.json b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/wrench.json new file mode 100644 index 00000000..b8c9bcff --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/mcheli/recipe/wrench.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": "#c:ingots/iron" + }, + "pattern": [ + " I ", + " II", + "I " + ], + "result": { + "id": "mcheli:wrench" + } +} From acc2237e987db6f725955e6249ee772fab77530e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:34 +0800 Subject: [PATCH 1101/1461] Add data/minecraft/tags/damage_type/is_projectile.json to the 1.21.5 shared sources --- .../data/minecraft/tags/damage_type/is_projectile.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json diff --git a/minecraft-1.21.5/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json b/minecraft-1.21.5/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json new file mode 100644 index 00000000..1042db9e --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "mcheli:bullet" + ] +} From e4e9abcf8c0eb3f076f404d1ac7ce6b4c704e00b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:35 +0800 Subject: [PATCH 1102/1461] Add mcheli_logo.png to the 1.21.5 shared sources --- .../src/main/resources/mcheli_logo.png | Bin 0 -> 23745 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-1.21.5/src/main/resources/mcheli_logo.png diff --git a/minecraft-1.21.5/src/main/resources/mcheli_logo.png b/minecraft-1.21.5/src/main/resources/mcheli_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..95c72198d76d40c8c513965b3c40c83712ee54cf GIT binary patch literal 23745 zcmYhi2|SeF7e9VyVeAH3vJQ%}WZ#z=Nfe=wO16}(vV_PoGbpr(RV-1m9k^W3+#H05LyW&;3lo-#Xe4geT* z2?H$5(8uLlh3?RM+^G{rwjoZ7>A@j(O+%YqnPXv(3ohVbbBWfxnA*g|DmnsC@J^w*!<}JV&wSul!dC2 zLt!s!f3yeeO~1PMF_^Jlz;>iuWCky}UX7-%PPN%Be-|7!waf1JPBX3xr#?ui9P13> z$qlu={F6Yvx3fAWU%aDK(?Y+HWfE`}YVg{{-?_QA-zRNN3C{!luNx30``K#p+_uQ> z>eQRnxLpl*lQh}^f|_$(xZUaW2}b|Ugt*Vii7|GD(eD?ATQeAdss?DCCf2+dn4{E>X`G>og9cU(oP_I^ZE-5r} zL$cd%@N9*d{n6Y1edqq}=#faPjf=N8_kVc0X><2V!``Y`1a;L;aie~C55EJA$;rD_ z&kdaGyb~*_^bJN`_<+I-f+g-_Pm+@Jz-2=K$@pazHV!pBj|MXu-z1?~3@E&s_Dxw+ z92;*-DiOx~iCAY8D2jwb{qhA0gY6RcXeH;m*Y%8rguRIWcl*o#Mkx&k+pBibQwTD2 zr-B54I&-0d<*7H!Z78CWv&IEz#3sjbIULO(;?6VZ78M??L$=jkO%I);xltZaQ~pnt zUn*JiI(<_XU$ZyjothE`?mI_|32hq;*zWfvr}Z&_@7Cn&*L=n+0eWAeoYi9C^)o3_ z&{zt$RQ0t4w8XW%tUD%-Fq~j3*Cz)%Deq+9U!cK_ojAHUn>amG+~Yn>Lcc!vsj(Wi z3`aWI51cXt-lc*06>9ogPn$e7Wl9##ZlwHvNVzV{82#kuvNgfPGQiIaHAJmXH=kOTnIgcebhPTT{t> z=YUFFVKHVIW*yI;KDQAw(>&1}pc;aBPk7rj*1(FU4H>|)QW~HRG!AIh9cv0!txf;) zIjeH+pT(%MBx8c&g+k%EmvE(DiP5-wMs|oVnVtNCi*G__Ylz+&YIY7t<-odau`1l_ z&#!M72OStVbHv`jRk(=#)x0eE>Xdh)r(FZv0fKLwvvfUbgVDcxe!=+1S+iQE`Ex(s zy}f>M>w{v5h@B=C6f(1oZBH3>oGB4Lu3@0{RPdSBt+SH?e;#H=LpI{#qB#XBTSvMu zMZ%-yxZo1N?n1^RO84&t3*W&8y@MI4%I5-FPSY+0x|AZK9B#YZi#%6vhejHL*QB&Z zzKe?^@g-`t_6-4Rzlu(^ZQrj?O8?3f#l`GogmJb0Wi*Z;J@{6=k`4_%*0Juy*%e>( z+UwzdGkfiyurI94uGg53Va9KU$^3;{wsvm+upRrgJ-q&UB`AL;-ybr+O#jmpgLM*d&o7 zQm`mNp%d&;-<1_0L)~wRr#xRW^$wTq3;6g`BqO!k`;r~&yAL@6*aAI_6bc?|bQNg+PJr@S`nVCBJZTpXKMrzl$@%#R00=F#gE<4gmOB5-}{2myYcpwac+^dih z%FCr#w{X3_H_d~oqod2iA;To=hd(a*3lglvM7PTqh@@y(iM$v`O%x{06_yYm(u%?z zep2Ct{nR|TvFVc^tg5G#XWMpuwEuOvUATna%2pX7S`3}$g0?)5=3#-~443%;lK?Mp zPGJ!mBMJ9bav)oZXyv6BO&*5yqTI;FTw~e56gg5#%Kiv7^thnA7TdS*=2=*eu(L;K zNz9CUnax7(zvTOwzb*g3jku#*jYyUPF{zx5B5*^{nJ8+5=dP`@pZr}w{V_WFKU=+) zJ9<>!ePU*l4W)f{mX zBPIJWAm{iu{{sXiJ?-V|Gq~K@0%#+?ea*TOmCNSzmC2l;3hm~bp6o!f%ynz`-qd}{T^UA=TGd&kP%M^Nu z%JrVMt)9aQ#zA-IWvMXR9w0L)7+i@!aA zq+#bnvoL;lDfV@_D|51xTH#;f(4nL^H+lKgu2lc!czAB;YV&290S2!~yEL$yxRpOz z@)qHw^`Y~-k;~~H@81UW*WY0OrRgbKK62F|+wq!6PHoSUDl?ipj{TtY$r|JY>bSmS zG1^&wx(1q}=)IUf(^`*8k|j9brbxg@(4SOCSG-b(MDfZUmo}n(JjFW{^)cYt@uGdZ zhzb28jw2D}P9f3#g%i9V${e$$K;i>ow-;*-kxvgw4}LHj;>ve4L<^e|oNOy_&wXMU}S&)`0s{Ce!Q!GLeN zDd*GG!i&{X-s08|%dVXMu8{SYc$@`wlFM7TylQsy-3ipC0=WfdE5S15A+V8 z?7Hg`S3l?>U!-T4Up)TGOXs;9F>y#!t?+=!V(!pYA#ZsyxYy3Za+s-KmvsKy#h6QM zN6gASZ_1s;VI)b@$2*XsxhZT%aLfSieZbln7R3x35UjE$lANx5%BngZ)Ho)1DEaC8 zd)me|l1o=Pq{Qz$xW#0?fpFJ!z`hCUw>5uy}R73dwF z+Q`0|)t_DW{QPi&s`<`YpP9%QwkqztZ1E|%C05Ry$FLW1^?Dn^r_=9MnQdqeXpnr4 z=S9Sv+cGph@Q0EG>Bs|D+qh8M_I;p$(oJ06Ma!|9dC>gMbaixU{N;Gy%%1B+A>yNp zEQkefx4`^Jj?%_Ir;`S!cdZ9I4GJGxo5EgAWFNEb&bq7nm*2+6Qu5k00qf99;(2+W z3N&wi(2oY_L<~R%VoIZl$Vd@n6ebS9HF!Ze-*20dT=?s;{8nYPN5iTGv95pnv+>V= zyvJUVDvTfaZ3 zwsA7D|Nec^%nL4b)Qj3*b>?xw4UD!d35V-oK7>5PQga*tzbITe68En0a>IE1xzij0 z=5DcW^L_`E)*7~NPjB@uylJ-Ypj-|2iSs+7#HoYt*S=IS?G(B(fLNaz9P3u9r~9zf zPnL%@eY&61(qn>%y?dC8Nfx(!{h=_kE*g1gVW|C6yJq6eD9!mF5k!>}@<*0V=};Yy zYP^v?oHuQA<<_U$X6WlIehFrFcL2u3)O+;$&%E``FA33+(-7Y64U-5z(Qwi-H@ECK zEb4|SOQCU;r=;~Y7*RzhbaU9pJ%O{eQe!uOP1Vfb%HN~|tLeJ{%-&|8zgF9hd zv{VYCaW9x~q2Gv!>a^I*#}0D+5O&U$FyUy+5|6<*IqqJUMY*v?%CUnC*o~UKv*r+l zw|h~x_{EfSEqFoWXGs6=4~`j&d{GySNZ;YO)15Gv55B8GYqOo@4xUpxyOa6V(L?UD z;w#ntaw`>CoZa-Yp>EAfIi8ATDz*#oD*K#`2b#kk11C+kj8ZLb-s*Ap{8*%@IrO?m zQ&3EMIpuKa$a;$QG(+*X{`FKruj2TJV(tC*44l#SR(PEMm$Qsx};E zcSNRzpGDv7Wj`wUElnse%wqN+oDFCrU+aUw2ZJ_1DPgy!bMrTLk~^ZVBy&oMP)xaY zJF{#4cgNo~(|}vk5!=8ucYLKR{_{Dv>@x zOF=x{YR**sX>iIqQ~al)ii;KdUuk1%rE)!q{n}KZx;URq`8TeMY9t4xaVEH>6-pb~ zuK^$61wIb~#cUbkn z-(4YISY7*y|B+cTxul0{f44`HUFNU9z0!C*+pzy#0Qdax`{%j>L0Mc>v7=fA#1E>g zM>Gz%+&-K&@>Oyq6jp*Rbx_ES#>F$?w9nbYHPK|xVM`9JUcIvqA06x0Ef6XHrkMAf z%iw4^N%mUsKXaYK^umY&hHZBchX1yRS$gP6Up(W6b40PknUZ}^M=RyexQDy6oqxEJ zCyw^onf+pXT>YeU9e0w*fih(w?c|vCMPE-pN}YPChlFtVN39n@3qFM&r|jhv=PVo_ zo-VX8QsJNBw|4bGeQ)!&T2%XObKYCvT3K7}`t5@;%dZdtuik&|*S>n^{fDv($8k}( ziYR3$bpkawGKz@$ropJ%pnte^wx`g~)=(H0A9>PfvNgs#V>p9%=8(&oi z87H}!imoreHzc@RBnbBh6SXGwlASb!wWQkb>`f~QOq$>PUH0na`hrq_Qq0EOyMaU} z125~XUJ~=m+XZ;Xs&f}-I;ZCQUxf+?qUntoM$3Kc%zCV&W1E(KMy$8*C|0q- z+6lMFKb4o_En)3*nt3WW<-4WI3w~?&)A=wME1ddAdSi{5rp7$)!Cr&IiSeFbgRui4 zWH_j7okJM@Np{vuUMZ?OKgIc=*+jaH*>tiEmlzp|F)GD)Mr95+6#VS2K4S3rV2C?f zIJ233-J0bbQsM6Si!Tb%B9X_U4L#vr4~V(`E>cBNWw-eAJ&<+>x?k&2J_tE`4?N)S zSCKZj!T;N=uHbp7!FSN{v$^J`avS@TSD;}2-LK;idS zfXVgn^f|l9qWrtxKCk4Re|Yool}xjyFQn*bE*4#(6KwF3U$`1cQuD=1^fGLmWY*CI z8^4NY1xnLN5@+o?oEar}q2cGN^elk2<`q z3*{hR{|;|Yfy#%v*RGyW5`TUyn;PCL(aC|9WL3 z`gL^K5Pa~}8P2|p?iE8TYVG=tr2)P&pIaqISr(G9#2utJ;jIO^pYG28Mcy-W8Gp_L z^|+l#>|(DpA|K_mhM|u^mJ6}YQzu|N)0%hnr=T6GrH3dHdeZ zSc&`*r5c>+<8Ut(L%~fd?PFJT=w9vC+h6druns2Lq3u*q`&vz^_r6K~_*AOd2fWj$2E{S_mH7N2;qD$jtk4h#|LYi1NtG zGR!(iZSeITE)PkT%@ljDo|}HFitCf8#C^H!tzb1Qg`T4VWku3Wp&mF+Z*yW@ppxDCQOXg`8oSIPV;g~ zKgT?eV>Z_u_z*ADcc0lu@%f?WqShik+c!;=%lKeV;gUyAB<4z4pR>r5CZ1yg=Ma4O zNT3EV(bh;$C)`OTeimBHFz;?Nz1&8T@)k2>}>f=v6~QOCV$*9Kaj({6IH&N9!Lhz;W^ z?=h6R+g@zh;$+EePUdj^6SIMDHU(aNRT}-(N3d^i;Mif;%+OrOC~Hj_;@N$;X}c8Et#(&BX+mb zcQ%4k!j|J?*PN-}jZ#*7cLS-TTeBYwD7{|?vDp#MdqO|bzWlM`>dAf5Q26GiCR$gE z9Euq!y&q~UiJZf=|8CE}Or1;Ngf_%q zFP$*xE(KCAe0-vfOBXPm&C@WpK}e-65Y#&2BNXZ_xtg#9fOYRIL%EA;JtMuuTmmN zL|?M7>v8xZ3lYNt-(9cWzSVXo=(Tq(;m|bY;$<`E@IMhocGrH~cgE+u@)g_k!RIo% zYLB+>F~<{{)%PwW@7=fe>Yv=5ooI2&>Pe_S?l4M!Zpm1EGexglz8HNlvIGFd$}tk2+>H@1j#L_mCAY^(L4m^j?#WHRXiy zgZFodk94#^;YIR66ljLHk8^wloviGj`{x^Xhf(7fOu$tiS|)ZWqhrmqZaY5NOHmcC z7g95=Qq>s3Yi`YB%GhRl4Ar^7|H?4;T0$f>_|>$;(RS>_Sucix`ud*{_3bZ$p52J9 zKJsp6j$KK__)hM4HKnfLVOyvOdVCF@F@uylB>>X>fy>tf5$D&t-S-1mZD$X?6ag8( z=ntk0Z0&3*&OOtV-{oqKDtLXx6uqQP%5M+W;dzq7sO|~-y6|rGz0sd31o9`3qOq4A zv&}P3Ic^Hmm0S@tR?Z_wSCO#arg!-POct~%j6Z^tTwg~}ls4v`lo-p6aRTha=Ig)e z3Z`l?=9CgZez%p-^b>n&E$MHNGK00{(e>i27j2Z>mL~1J#fq__TW@xy1~wbU8V6_* zTG%t!QZurzXN25)oS}R34#95M`Od4;0jdVZ0K}n?X<(c=QjLpv+ay9UW=41U$F3wl zg&E9NeKuF5Fx^|5wer9hjE;5C{;V@k+lmS3!Ksz}0Z6dVA&88u!>)AN-?Tl)-Y8^4?kub=Ox>i;%gSk?SOq zK{~4EoGn4P9YY@C4?b6+zz3Wp(bqxcHCUn;KzDf@0G4z0{lzm*Y(;@e;Abr*s4Yxq zgOQjtD#4$xR_{njI2jo@O-Zyr(GbH`uuxJPx%AmX!{GhGsnD(3=s?=vu9vvAYiyZ2 zspWeOm8tbJJC6O6qb+mZu*k`RMG3wu@l7w>OqnFd)m4OiSz0gj!N|whwI{*sPv5ep z<;x-x;R?CEwE)PErQ`;4-BDr(qM-y<;KT3>+xxWmp~#~IrtY&;)QVDWb_ zv2JHt&s>?hns3S#Att?sm2LOUXY|r)wO*#y?^d-G26k9ht<2FM%O72dH?Ca^Fvg6R zT~QWh0U(hJJaLCXb9Q8o|0tAkedML-ak*i3K>9Y=#ePRAS)zA?!3&;jGIXfvUdtwK zo;ze+?Zg#vvA)fN7XI(_^D8J)MQ4U=_GLWTvE_e>I=r$T|$uYD|N zBd7n`0N15jia}dY;iZ6?TXOu(6|uM*0G+V!b!2m9^ijy8KPqh-z8u9q`iAvlyey*j zmk(l6{C?S1QGZ-3+sG0t+bge_t(Hi*1pjIuUOro%AlFVyr;Pf{=2_K$xE$O6ep1%+ zDn*>1J5u=r@ide^1b`<1Wb)uBzojawR}BAHF!BTg3nX)7`hq?Rc7HEVoxSf7+}9Fo z_))#8M6EzDAzbhib?Q;QI<#yfmcqU(6G}L%3k%fuhSK}br0ivw7WRE#w7oTGZ2M{R z?9Oy0oz|YiZRi3pKhS(r_IL>9ITSH4xb2N5~5pVOL$Sw1a`t*YZ zuRkBE=ORrrNC@9rQ`0v+6@SiqxL)yD{pcHVX-bfNO|T>+@xf9!(Pwz$!PKSPbDgi8kaT6Biu^u%F=)RldQC*cD;$r}CVMgq z-NXoHUcCB=z<4=#EaxihUB>kwW650!~H?6NQZYL!s1}t0~K!PtaxX`<>wn( zwhCdiSFs=e&3$O4v*qRWCzrGXbYD#8ikur{4&;^NnNL0(1H1Tu+C*W3vst5H_rfJv ziI-t9msiwGkI0`%!AAG?-K8=i&oxnw5{a-V0FXjYo4O9cB7fXO93*c4%DyRZ_`w(- z5}6{4QYHR94M3q|3tnK-6b^JYx9rpcmv`ONRg0$yM79(2V@AnXiRt>AYE#EXPay0m zrF6N~q^*DXOP^&x)lu~9KOQ&$jj+UY_j^P!FckL|cbteI8k5d}@qdk?-{r)vs1URP zc?-7Oylh7=U!DrSA+V!evbs>WsnSRL+U}T3wNIH=x~)v;%-%h+@FTu#cT!1V5bk=| zqkcl)lqDIocEd3#3OE#~P=I;`aAe>sq9E8dAao2=kBPtYTk|+JjI%dfehc}8yXC8Y zEI?`Biu&y1VEiH^6X`Sx9BpU)yCuHF3g(01JFh=i%{5hIYhm4K_oxqZsVYm(a?SLL zn_0t$^Qj+Q84jA(%~2cAjoM3A2{{p3IZ7#DcOWUC9z zG|c3)J~c&nE%jC#%h}`Qmsg$@c&;@hILM9Jcl4z?@1a-Z>MePau@Q{g=eO5%ho~>{ zs>?5GtBX8*QeZGc$yOMt4gL>cK0?I@9{?^ApdeR~GXS}rR^JPDB_bZ+X>;0(;X5Xi zlV5Dy6?0?Y=v$f7I(g4+f5!G!b7cD%%LoexHE{?NHf~hi`T3>7f_AvJ?n+KcJwt|tc;PBc6w*~UjJ%*s>P2AAr7H$n zUeQdKwPp8ya;Dtj>cJ|~HO&E@*S}RC&X<4BX1_Pju0f6^T=F>UQ!v`^R9H5qEUbP- zpR;zGw`zXqSKvnZN6)3G!(~^0IhOq^wkzBC66=-P-PcuX7Unc+y1Q9_G8Ri8?tahr zhkjkodBD7ORdwPelOH&t#!>hQ5UE{f+CPAC+RIR&?&-)yJh}bhZmxoN%5yJ$c@0Cl zEI#||)u}A0_yoo$4S6B{csA21k<49gw2of2K@es1Vb#{GiC{uya&y8#D|?HoNA6?F zech|qg1f)#QL;I=igQfG@yWMYYH6!&Wxiwb>z#U(Zja0epVm3)qB^5~0&qs3gum-?@zhqw2=hX` z64O*qx2NB)Z5AYwDGFf}H42B>{CJ;uY`!o0{Q zDt?)Lb0uge$+^fXRsjLL|8KNlafb>2rxzVy-AmvHl5_QNu+Gp0g|+oa;?QmYAf3f| zZ~-s8wId23VW9{vfaHuq8Qpa`Zk-iuMEvHGs$H3R7!zUlwvIae+a(Oz#YMr2D6=2H z4Tytq8aG&8cZTE6>_vmpQ+sEh=+n93xHh(SyOPLTeONW8>mr{=3(fltZ<+|QBo}bwIar22&R+KB=R(DojMAA{+tjbg9 zS?{Zft+!=?J$`dAXu?LpA0zdy=H6#EZAgV#r@fMif~z8OZEmNqB&+jid%@PE3%jdZ zG3FZmZz6*_=CiXnDAKf>%q7BRmY+u`k-$E&mG-H$!3%HFVoBORW>d>|h!=q}*W z%s$bKBp$SilMgtH)=dkPUTRk$KQe7RJi+JxAjD)i1)@<^U&{g)MgX6hBG9hCb_BZK z5eDiIpN9fBhw>bt+&eSFoe2o543et&fjh;QIf@NCtl++s|D(oKdOWIQaK8`c_qZlEYOd@Uf$GQ_|n zyNgRA{{tauVKHNVFUy>_s2e&I<<@_~pTH`K-HKo4LjiZ1D>Wy{HT= zA9t7lKu$bC0DlKJKyKON1@qfuJV1e-1jZ)`wtfoDd+*KmctTjnEfRP#Z`wHq8e{uj z7#D(Suq?=Nr!3j*=C#CgzIqHMVA0-z~(%1T;z_KdG_`U>Ce7wxt_bij_zV zAj^V=J8+WNo(Nbrgb8(SJ7+Rdzclp`keH~IsugMa;YNN^`j&5gB(+E#*RM05jv(o? zmv$mZjyfQ18m0}h(^=lW2XSqD#3Qoc&JIPL5qx^Dhg}lQlW4UV(#l&}ccox4^+(@$ zR2dpnaw4? zpAY**m=y5I67pZL&vcJ-6{(vg6Rc74kQ5C`a1 zxc2}0`}>TDr8^4cRn1Bq_h9eKl;$85Z{NvK4pVfc4Oz?MaF9vg%C|dN%^uI%IVFWa zH~SvxHxsIRuN?mDF91WhgU`Ev*j~}Q3bI#En0z{UE$Z#j$O4e)Njz|tT9j?n+Y1i> zo}%iuCz+?>PDXREkW_j~BbhY;F(wv`)DeQmkbg#kqps}hi(e`ml zjjvRY5ICa588=E#^tOU??NAhv!t=Dm&3LmD*J|LnR(4>0aT-lN2zqHwAW@0M7l!+! z1u)5hVD>tK_=V}H`s7od^>-{+#DCVM1^xYKlVSWlOR{vzxxhZwCyI-AuIP>2UOXW@sFQv&*dp)Aaa|}_K=7v>J`5v5 zRAm6bh-(A1mMDl*QUbV|9~Xeo7-Ad-X6e%VaYGM=%ir?>iM3c1(PGaS=E9uD27aj( z0o=ILA@zl)4`*M@WajtPGrUEB`6wcKnL6ktPAUx#i`-9!~>kDRYrmYG_vMY9%N3`Z<6R?-s&pHOK{ynv` z)YdiEU5%Q3S8LU~#$B9`O6tm*vggh@i1;{{GU^p*r*u}w<_*T+kL67dm95qjBCC4Qd z!)Hqw$*0~N>&PyD_R*6*b+OF{ZGu=6Aw`Sj8{Jg%MCK@`? zyt&>#yt&?9v$_6{wz;0xy~e)!D05q+#7%FrfuYUSGO4dE(fM#~GGZAY;#`9*o}A^* z+GE|R6EPJTj9+MpXctdDsfNn!Td(s9bi9}X1H)QN&Tz01wPR$ERo96GXbC4k3<*FG z(@@aa1dNyl9?3#c1y}-WS&$CMnE5iNU?!N4r zMs4Vwd--RM8TeFd8vvq3l{pyCFjw`8S01lw`{3EYe=%_E#aEKeN6+6HI_up*I%SSR z!7~=Fc-N~?9fMdt!PPZvD8k&ydVgABX!wM-Xu{ffxVBPf!q%7FKM^a8%Ew!qF?j66 z=;E743(XsohJbhx4%Bv70Sk-80v{Xbk41v{QxMRXg>aImEacbKVbE0eXVfYHB4vR; z69_{JT?ez`u=)KRAPA68yksM?vVr1RCoZBDHWb^4`~A|%JRtTt!eylufaa^c>2g5k zT5s(v{hp+j@7=o=)8kHDa`8zTgdwK7PF{%xQS#s+nsi%km^2Rj?8W@-X65bXp-^Yo8 zzyv6q|JBD`h9%A;Fmj;qI+%a~9hi_6DC8sF29;%RSRk?v*l$|oq@E07r;2_lf{tUN z&fZf`fVs>Sa$FJsX85Em zqj}-yv5!DBcG6!wNXP{W1DR~Za(jWEB4_;3SYl-+e^Qe0V8U)9sLZN|6P>)8=_25W z#zGS(xSarZV+D=(xq;w-I#|{K?~&vmdOmD(2G9wDovTovwkQZb0xksI_rkFf$G5SG z40C`)hRuDxCZyr7cna#sOm zk^6H(i+z_JNOZ~qr4Af_5tx&!Fti@1q4owyHvcNed!YflKES1eMr}tpS!l28E+nmon}Zb5T%xlPpvo!8qDNBAP;z-`}ZgTIDcON7o!JS$NL5dkhdvbCE+ zU^GGU52Yl_G;N+MX;U`Lw%<5h`raBzZuw9AWvvRb-DT?lG92@Y{Vgb5ja?r=m*@~* ze3ZGDJe{h9)p``A@{tR?2;u^R(F;3_1*PLl9&KO0@oS=%TR(Mt*%~>9--4k{n-D-_ zS)ZlfUJ|I9VMAe}WPQM#0Ws(I~}zE;WU1>k2;Ssa^mS>P6S48L(_co25EzZ?;f zMHOe}2Mk@Jko+aEEf2Gqn`X`0b3g~cKB8uX<)>YeUzhQsA%Uv`u861WAJi@I9~?kT zeB$+U@fQ&54u^OydndMo@L4ckf_!wG;Q{-K06#uSm%jfYc72p*nR_I_!K}M(V>KYg z$bC}_=H~`L@~QfuyLhQ?A0fZJvQ-7(7QzoVqntti70z$5qQwsyw@W0`8t(WRRJO(cEak6P2ijfT^bH18rT7? z3w|gDMNFZ2;lf{Y!s4wy7lDwy4SQqw7DLZhwYeqvNQ1Q6$TOhBO^$_KM>GHpc5?Cv zJQA=de?fgTKgfp$!1jhe&R(S9F4PctV2e;ciWt z9GBiAYwlQRHDH8f*Y1F7SK^dEDUH3`$R$Qc7LA0dSJQ%P9xwicxZpJVMf%hv_SJtP z$d}AoUSK^!;oQ<8Ts=FBB&|Xi#5xoFjF2F2kBZG@G->sjqKuQyya}CuFV?SzHTda1 zk0h~yvI;@ax>7F!XcUACC_FmGqVDA>QRUiuAw|c_!uq3ZLY7X_tE#g!!>v^nbt_K> zznkZu7jCoXv&2Ns#~_#NGYO~Vi$%=k3lj3G@15@wHNEcsI4|g9OHYi>q~~P(q-Wo- zf7!LskReTizCo#rzCmDHNazL6mWh?JfE8ruzfY`X*XNL-SALxcU449ieLbCT_LZJ& zZ-D%MnMZq)v+R2r683ZRy|dHyPUUluqoRCUI4Wxy>y)QC>Xcxh1|b-q2-DE0iY z9FN&Sibp+Lw*dH#Bq0F-?t%gvJS0AdIoIF0Bl@(WDf=k>%k|FMjkC=iW z0^~5j#QxXEeFO;HfD=RBF(q2T!$F!3J$zn$@y@1KN)Q#<77Poat0sC5f}b!V7Jh;z z^_UaT&LrpR&Pjaz>%rcTDUHuyXKfhdoh}&>Mz?0KIk24DwZ3gYn}lAM2FE zja|1-eqD~gml8LY%BI1`{{kcT_OtbIy^p<@NW5oXWk)?bVIuo5^JeM9XZtr*KSHHmQBB!mpo#@WbqVn}eeTRevP;?w`j z;^l_~7ey`h0@}54`a;v(jjR(){?Xe;qLLZ^Veo?!oc^)f#-dW!h(Gjh|G|%7qzWO)Qv7KoLE?+P_ zy4@-jaAmpYFyCy?@8&*Bi(yuSV`U4sEqK{n&m~-c^;vA!mGIx53jqX9%N<2^S^6>q z+V8lF*#~168aF1Y{OOh4!`w^q+ElHM8sA~;JZvNKKYzD&PU4)mbPGGsP5OF3_(Jk& z1e?VMg%r+=qdmS2<=J6bBif+5&J&`Q8W9ln&3Hto}yNpBXI?sMsXl9O;RM%V%$t_^q z)E~>dv6L|BS9AHF+uJR{ue;;)<+>2UWmK^#ep6!b{z6mGQX!3vQoZxk~BU=UyNUNkSUp_jiznx$H9) z${&ePS_SdWWMv!*Sijh(KtutloC}bunLs3zJ^lBEHERZFf)zxCptxa2WPdIHnB__IYAe zXl}qU2wL7c|M3UlJ4CRka}!;9|7Y@L1rWKY!CtAG$aLA^<;lSipG(Q3u4KZ0nBPFLJhFK=MPP< z3CWl!#Z$P5{yhP2loN5|Pk(mpfq$U_Cir|B2%U7es0k*sXEbA(r<6;dsT*vz`1fvq7;J&Efca!>BMot2?fN+a zw4e1U*BeuO!`BD1wi0CM!NPmeh9Nr=4_V3HtiTMSgiesSz#o0qxT{jiPwx;tp-o2? zF`(cQDBFjetp3TtB#I%=AO$WTibYOga0&0e;^44aU5jJ)>hu2C^|gsh%&3LZd8T)l zRqCHngNm?AgBP|a@3*vIE@Qit1hd7yx! z06CKdGuyNKi<3!I64QG_M;7o+I0XMqgerXanR!_*-%3@lO<&nZPG0!Sre;IFZg|J} zgk4jE$}ZY(1a5GD6jN13a0)n~Z+{*9JcGOiv^x-fpfOaO>|;P2bS3s>W5l4da^k3u zb_`tXoiU7ROSEFU#ZqX6>X$TPVSyVXSP}dzy5Ayl=lpBDX^GDX*3=vIO|%=mN>_h4 zpP{c5)#K(V9H5qZhe9daZZY5S-KAjxFyEp0)10LNy7PpKNjpOO9V*UU2)%Tb#D|PB zj^yn-Fdt653!1yCEO3Tm*0IO{=2T`oBeLtbT+{mb$=H0_txwK_YbYov-^*;LHG41& z3K*Th52}+ovzMN+7pul7(E%(0Qp)GofaL-#TZ}m*onsviV+Do>N>jPXxvy?(qeD&< zaWjU>R*LaW)@ zQl$!>*`ch{#=K|=A;m8NF%Wd0L7r6QMt=(>7RJC}EI(nmB&HK;f|yLUWh&r@zx+d7 zjkE4x4#Zfy62(PhnzzNKr@!uJp~o#sRO*Hn+Ds#cX&+{irU&D9bLQwl(-jN!bb-AS z&lq8M7`~1ELCm;HzR271aA_y*L~o{j+e~I-m|@8C3Lk(N@(~pzpc5C6QETwiniwNY zEVC%H3_921S*ohD%@%tK9!J%77REvk!DOt3sno9ojh#&yE82>!+&4?*wkZry=(V|f zRV##eK!hB7R>Y$6YfzZm8OH|>9rON9Yw}{8M^8J5Wy7#?(uc)?mlJA z2ph;?x&W*rFA_zN+kG%YrEw(u6~CE~8OH30-?y8$obSBt`K>3EnXm;pRVsA^O65k! zZmwD#W86PPx0l$HeEdcOV8p=JbX45DzsqZW=$Ne(fz$s8l+f0)OghNFjikFcKQq!* zgecPNn-0ezbTMc0kmXNtb$zYc=56m1B^yTKFFXn=$AyZpumZL9gH6~cxqHyb&Eqw| z)c{f2S5!FsE*)r;Bry?<1;~<^=(EQ`q%b?n1#zAal95nJnUPC7kFpRla$M?jiFat4 z)pZ*ALS_MtcGg*;y-SvXDVZMJV0Z?nQ=!1+NTF!ZP4x#^z=tg+5$kIqA2~g z_P|$kMb<}NDnD1Q|2crK!t63ZJJ~9y)=ZDLHf09rrI)wcD;LCU-ZvDe0bfA2u zw6!ZH5_7oY-RY5I31`nFtbMo56v6&nXnPJ-6ZyD7J!5(I^)ez(zw?R~aEWFEC5>}n zoT1$vAv9iggyn@VikN&A#sOzhHk1@Ph-@bXM-zR_&`&v!Ko8PHvLRWS^on-Bxj_$R zHqX@7rL2aj)kn-z-0?vZU4V#MGwp-6{n7>c)P#f=zA>O2MNDdm?BPJ92{8HZuON3A z)15`Hq5d~sZPRDgr$tKog2y6E>*<>!^sV~6JOX{eIgs{KWoMke$%iE*ZZs7bq)HzO zPcnn_iT8j#^s2P)L+Y~Gvirk{S> zF46X%2gAX%X~FqPQ0hmO>uP`q+fUW!S8RR?R6q!Z6tSycMXB4J#qLqLv33`xG6^)J z;E}q!y5-P;`aJZohKZJ}i`Jq2k#1$++?kCqND?W^NcMAr{>cnu4JhajIE@v}wkKqa z72UnbV3A>@g%T=E36{?YMu29Q+caS0)HX^l>C^eTJj2$IDQjzhcSHfJ=NS|y5Y&0 zQm;SE-18_ZQ*J>LiAMIw!-t~EMBo|{7+kk5>h@Tax-tFkP6O?p<}aeMpnBtBgb>Va zVkK2sN&Pgyuc54%7>>A2~3E`${{`bX=@)j0%U}`ISsLm2=sG#(ItY+Mfa< z?tK0%y{Rh@MpTDI_j3qg8{#Ayc_vu>?`E|Eol9fu6~%_lK%xPFJ{2mO=Rd?pNFj&ALTOe)uT+ z?9X@4@!Kn=cu$MA>5c307>-CB0K9NZrZ3!lW@xgg&o81)Bx|0L>v=Au-E2qRw&uXH zlZ8^ek}NgtfqI=c!|7N6kOOkVw6p6B4RH|Q>s-VHl|}e`b~}vie_sf?ufcAreVFa2 z7@dy2_&pM_>-C40>eWqx^%vzP>;9dxE?F9xO?gw$%~vg+L)hDOrX?n>*6;1stNS!! zOrg0Wu@)Gk@xvm1a>HqIXlj07s)gSffeK9-|d|vO!#CTYe}#_z$iya zqD5JFk?c|Kt>n7(o>+bPpI10P<4y}ZYY-BM ze{1fGuIX*i&V$Bg+R7|_VNwONZ*@m1JpsFIzW$q1Z?pLKa*F~9UoGl}bqbt$c5lCE zL3LHP`#zuSw&a)9FR>EmPR(?l%*k09!C0SrZbhlzI)*({N*mjwOE~IRwzF735Q;5I zLM9PY+Qw{)*5>~|C0utrRsa9Lwv4Wk6`6%Dvb$N`8=+73u5gWzz4y5GCW-7-wnEA( z*Ua9tOE$R(ab0yUF86o({?6n4dB*#^&+DA$^Zj}~pD({UI2-g62iE6a9wPEu-AwhP z&G(q>e!ML*?9fUYlh%j_gZMLQqW6b=B-@9rWF8N#83@u3USk?$(57~v1=-R4Z`a4$ zloA9q03qgOsOXr{w0e1E^01~*iS)F3X~g)h96rmO@Vndl?8NEU_jY5sd)M#lP{Q+Q zW8|2bBDkkN%E4lu1u)y`B-{uT+CHY>hSF#{_uBN4Y( zeTkKPyErwzDOn}n5-PNA#oJrC-MFiw`Z1lcKRx?KCBD1)QCkx8ayZri^5l#BU`;}?=o zdC4Pi&tAm9tyiYEh)kP_T^mY=7)cs>$l_HHACf4&s`3OQk>gPZma5|8rMz2bHKNBM zZiU2tVa`HIvt%w7yeT~&}nn3P-CcgNK(gvtK$P1)QnieB6 z1)p~9`EI@|CvI6w1A<6$x;DSY319E{5Bz})PF+z;f;$2U4$svk9g7ua7u1fC-(do(T9U~R z4`Du$7Y}(M=BjY2t`i05v1RlYR-}kl{Hixi#0^R&3tAaU?x?F|v;v&WnGj$U1$5-a zYVd5vDge8rO84K46br_vuE&b-ZpFu%to$I!Fp7`U<{{)evR!^+G>5?oY)UxXEMTDZ zB8vdr4~JHgU#LOW!v(RkZZSxe@b!rGZgDsjyfRYgv`%9;1WWl9`CIkU`(qVI|8s;6 z9@Z7Wm_^lcN9HOXB_f^jSesVC6|xIVBzcEJIT8OlIX!P z$$@j-TP!#qQ123vHzYiVPt5YY3P^ZaDs9{)U!?!=`-^SpQgGW95Ow52ayHg-w|Ill z?-V5G_gA^&_7nXDMcVy)yt2waJ&>KplB_0cK02`dd= z?*53iSy2d)IZwtL#0VLx(2DfuAzdenz@o>bn-5ivgjXNxb+l$*PC>qrM^pxM_6D$RlAu*}RgJf0_h*8X zKs=|v#5!C)`tDB6kVpK_vKR|8PFFujy~R-GuYyWF==eRAwm(TLUQaA#k=CSFo|mAn#Yt8;i={EK>@#-=wQiRon_*H>p>;Kl_-3s~ZI z@T(tuBt$juD{0%uItd)<`yJg7n>0Y4|1tZudK=nH@Q#|O_S}5_Zr1ISoGAuP#?Ouk z*!vfn8rol0<`Ig9Fz_oh{@p9_l8+?Mj%0&j4V@CjcRj+|?L{7R?8ePk$w}GpDU9JKt_f{_u5H(xPT4Ziz3_-m=Vhr0p@T>G)(K6cM4p!y#qdn(W^6W(t;@}WZv zMBcwWfiFzTJJz%XIC1B@QDDtS4jL@!6fT>3ny5OqnCtwbf70rDO9c!nV{b;C+Mam{ zl-I}IS2WItI$Ycf^unFyqb?!iRF>~^e|{r$A{Jd0J53;|dEShxJmUVKF6i0(o7w#( zeh^9(M0QPr67s3ahq|WOV|fsWUZ-pe*T}fMX+6&Acs>?!b98-k&gwyW(T~0Nz_1>d zVFDfl#zGTN7qI~pAxGk(>3S;Vk8Acd&9Ivp>`4fBfAuK5t_lrrn2~1o#^J4k-G+YA-09X*TdE@*IL8vlv? za&^^{{M`fPD%a|K@Brk?cQ3``D!U5k`n36B#{du%MbU9wsUgjdsAsA9MmLx_8J{)2 ztEhc_yTMKA@nd?;d((QMX{3AR?1I~gj{;bnnm+r_7K z(zgfFF6?rr0@^g1(30V(?NIHE-iOZf%8|c%1{q@`ZF++lN2q-0F2Awg?#iYfnCTYx zLAlP0ginh*0ZYpa%_qo{Y(J+2*V^lLE7kOQ{vCN@1 zqsk=zZszN(4O%k4M<7}t>0p%J&3BcyAn6D9`#t)jb}FoB=jE~1UgquM$Z{%)_U z)Wr-sd*)N~bWcoRj;9oQO{LAo|8Vm- zDbw=yz5)9uz>@h4wn;h~A4qju`ntS3&w6R;?b`>Uva)kth_33YG!)lZzR!;Xz_LSoceQ=wcSo2t+X2Z^3r#dle8%!2^HH6uTpc8{5Pd^*^nKFaE1<_H6o@~ zFs@67UJaHXab@5|QFwaAvQ)kdeNq)-1~dC4MaewL%TY;Mcx?EQJpY<8flgI~=@BQu zN~=nK&FlU35o%Y&IL-uCiHgM@PcH(#Z<+E&C!POl&GmiCHplXemNjfe=Sr9hZ&c)- zb|79h$Rh3KIssV_YZ@;vtFOu6k%j-Q_hsgT$YpAV^)^oXqns&>X&n>u=@us3dDoVszgzbr=dE~?>9iz&jRw%OBX+~9xnB`3vg}7r ze|JZTfOURU#L9_-sS6z@TI!zak9+`3%d4~eCO?gT-rxQ{u%yOXBh=sXCr_@F`dz%+ z>l|xsq?f{*vz{(Pjs#rcYL#@M(0C^j#}53K7;dP1x9xY_0?tgIWsM2GYm7#qF81xD zzeL-GRySb!2=de*H(ZkVc?W7$5nLjcBdMRJ7TYu;iD{#vuMcgq$!i+%wE!O_wN@x5 z)~cwLG_LZWWq+enONllwbI9%HP`6fFdZehp)=@ueZ&Zl|fNV3%E$gE6jnO)uM_1(w z7fCyJl_o;8_7hIxe7tk)%n4@M9ZCkfd%KciRpN>MTNGJTs!-$3OEzEu7bSkP#RzIf ztKGT*IkUut#X3LzyxuX%&p_#XFLJDpacj;Z)T+8-==O<6J}D{YTx)o=fk$ZWOMsQ4 z*YVBv%kPUPU4JzlyI+^fxgWF5HiSqzFMX}G4+mIJ$R9&JMDPWjY$ws28LGsw@#P0a zNzk)qd#ewOM}|^XU1>UxCf3`il0Q6IVfqv~&h{-a4Xi+d@{4mL1Gk^c+4kIYE-ch+ z%DZWJP$40DtNzuUHjH90?jV35kQL>@)k7!Eo4`Wp9yz8{VssC)ab6R=KUDc|1+?}D zTBZA$J?r^LPlsYP_V`VLv^#q6aDo*Sxa{1k<1XUvDH5AgNV>rURmx7;b_PfEAIONG z7tP$8k`B}8_TL{!bDJK&jMj9f!cnI)zA@ETg%vCIfm5Bh!cX61!G8iIdx_HBTb2X* zuBKpP{N*0m{mY-Pr1>UG)LG^{rt*SM_#R3gd?wkNW(t&kz|punVDLA$jK%-dBycbN zx3s29CyhF56Lj}4v{N<<0a?r|_I;8(8ID8~>IA58ltj)drx5)Qd>7Ig!h$&Zft&6aseGw2)*or#;OV4(YudxP1tQm2>tsQP`aCLm;9j2 zCmYiJ&-F#*ivU79uBZKJpI+igFwLrbnRQVFABE+soY4SAcEl9QYc_6VZ#uf;@~;vy zscQH*#58%OJ@keGcyyQ!RCba>nc5ucO)zpUm>u_aL)4`o?rm`*D%W$0xfhd{Mh~e# z%Ky8A%a7?#l0O?O_|z{LB5R(F?VDv{hyA1B7p4b0mj_A&r=B|lFZrOt)IqNlBD<AZ!Fi@ep8VX+FM_69k6sC%CUcc!-rIU+_D_RG3T++&thk> z4ua$7Cv|jV5BJKn>hm72#-wuA0MpolL+te}s2U6e}?{kGb){YJn*7OOZ&YCcUGpCAo&p=9#r@x-KgsFT0Ig{!o3-kq*2NhCg?;@- zH%7RK`5wfY!M*Aif8p+O@7}Zysul$SQa7QXUVYSt-v*9n6;%;>%pG2F2bz7PM4;VynSwu88H_0t8&ovDA}^(6aKk6rH%PBSGE>L~ry` z#x+*2XcG50;-%bBV_wkz1a9k;@ z*j|6+ww)vg15{|^8;oVF9*%BOl*sLF)|Y{kaboT{zxa`9>q%&@d(%xG>Un&}Wcp+q zjODBzo{J@ul!mPv4|oz=(`#N6+8+u6A)VAxU0%qBeKM6eP+$hMM&!p*&m=-ox8Rwb zL2z|J-r4jd?hi6aRz77Lew>Mc=0CV#z?@&6z&PZHlHpTkp}pMWy2`Ywsu3{C53_TdFoW* zNkKO7SztoG{B^9)fEwzhAe)D*mJ(#6!CHjZe4TEE#7YPB>9mA-j5h7`##x*>_+4#B zNk!k;T^#mK$#jJyQM$_a>_F{d<`oU?VGcf8cRf9yW;GoA>C4T6kB+!HxPHTi=4Uco zBGeNb4}KV*oz%IWIfd0>ac_64XWRL^ZLbv)+Tm5D9u$6ZPiL87aaz0sR`GRwS$3`= z9!|ndj?2BDRIXzc6OGqT&e)uggHoN~h{0Y8^U4wG#%3N7Ha1WUdzy!fYq7k{ZAyme zHN)NNF;#N!@3b>=TKz2zl<#YH$$S=ezrL-Ns94_^*zxUyZ{r3p2zzLTyY*mxEk`bH z98NL8^xx&(kU%5EYp)}#JL3A?3g%->EvLrk|JLo~61=|p98vqTu2j0jQ*O?;(@(2K z?U~xR&RIhd1w1U7A(%?;`fT=_&&W+{+!iY|($ZGWvb@|x%Poy+<~}l)Cl$9Ob*9x% zNH+_duL&EL%Sy8Umh+g3`K~R!N81jCjUVtBQ8&vVLZ1I8EU%R^P9(0B8YB!^_AbsQ z2NEBO@qrA0s&F;#y8PYdu+F;!pT>UkpccH9gyCl=} zubU#lkDll2m&Ww>U#`{<-R-~?(|nvw`p?0?!CG5Xxy-@B?r(>bG&B=Z7zA`fb^SgC ze)p1ax6;SrduJ0C?~*Lj8wPFOKU60o-?2ek@&nDkCJyL)00N(G7Zee`u^?D|d)ep9 zLYoRya@o6JMj`N9o6|_+wM~Lo*fXw3lbo3)0{5De=;+I0f|6`)ZbO*yL{|SQ7?Evz z#n$9dTz;El5{GlsH=2rme%{uSJt5?o@n8ux8APsJn~^{L3!Zg?suDsLEGyiRkPSf& zy6u!CS%rV-O+64^K_Yse3un=hcg4dAuOl5R>&>?G`9Ny%ef*I}vZC4dOxO zGTquFsIHFm|9b6SWrrX42|5_lREoze8e>D&&opKY z3|_X_^aheC*h%i3VsS@Fu0qLc=Qx*y_|}H@gqA`MY+z!!>gXW_e<;>E>uh($;;X|W z7J26Oo3OZKzW1zPIun_4lE2^G!kH50Fz?FTOZ;J`Bjs|}xI{{)GzT_d3aFwI+Ma6p z41%z0aW7X~I`C9FC*NZDLEqJ>L8Fq|+<6bjRJ2!yo_iKgjO%bzcs#`G$bY8Mc09(X zRJK+FgUB_*K1pn9C53@vs!JZI3!&LHzBF2iona9())UF_K4lQ_6%|Ey-sy{06;Y^$ z1{ia2RiGXUe@+9k-s|AjR507#GEI|=rR* z1(pR5W2d|r4mh@#EDQ=^Ymsso#Gb`fpOG}z{IA~O<+qTH&u8@>An$(`V&ArQmvSTS z{aB+_6#ObpLE|L^$JwSCi>47K!y-cSAwl84T*4Hy(9CfVBIli;tIDx}_UT*UR>qHK zFB4+}3j^6aiamQ5?<1nnUr4Dv0Vnp7N;pKNGVBmt9c$l~?X%Mxtqy_C6c93gSr%}9 zy77(7FT~)SQft!NhHsy~ir+*tE$tMf%v%6cT#%A7JdWDOuy0nqJ{+>j21QaQ*wC&1 z-r`}*gGytVJX2fmbf~akgK5oBZUDmgq58u33<={MC!b^wpZ>W{HRJuE;5~X2c?T|E zQ8;@Mv<9a|sfmrQT)a0~I7y>dYbh4$zvN^eJP2=_2)Kk!H zbM8QV@l>IpH!U)dX=vc(`#X|)BohIXY;;s-5Yu9H+Msar#G~>FGY2k}1CD?fGeWY{ zDdH51UvgW2svM!;HqIU^_LTAdd&uoXo9=#{ z{u#97VMe$TX2OWIPKj-OS`$rB=~qQbjF+_*3qg>=`hI=UvRNArHFO(zCN1n&SVq#< z?d`r|)~X*H-PgfhpG~N-E-CSWmc>r$${4&MA^)@{c!SJ>d80q2oDgCX6If`bCBQq|vDCNs@slX#c4u~vWRKY#8`#%sYs1fEp*QU5+dk4Yn?I~jxX!iujA7A5 zy+4IRJ(AOAbL>zJKlV=iu;}j=h{5G56(ebLg4-DEHDhd#4S6pfWwyrkyEHJ91h)Og V`gRq~qyPf8|I~FK)~Q-Y{U4=vt$qLi literal 0 HcmV?d00001 From ae99e765fad253f383ad6a0f448aa913445b0386 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:35 +0800 Subject: [PATCH 1103/1461] Declare the 1.21.5 pack formats --- minecraft-1.21.5/src/main/resources/pack.mcmeta | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 minecraft-1.21.5/src/main/resources/pack.mcmeta diff --git a/minecraft-1.21.5/src/main/resources/pack.mcmeta b/minecraft-1.21.5/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..066331ae --- /dev/null +++ b/minecraft-1.21.5/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "${mod_name} resources", + "pack_format": 55, + "supported_formats": [55, 71] + } +} From eefe0db30c1ce4ecc5aadf7049edd6637cec0886 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:36 +0800 Subject: [PATCH 1104/1461] Add ForgeClient.java to the Forge 1.21.5 glue --- .../jnhprojects/mcheli/forge/ForgeClient.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java diff --git a/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java new file mode 100644 index 00000000..f8b09fb4 --- /dev/null +++ b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -0,0 +1,121 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.client.CameraAngles; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.client.event.EntityRenderersEvent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.client.event.RegisterParticleProvidersEvent; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.client.event.RenderLivingEvent; +import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; + +/** Client events on Forge. Only loaded on a physical client. */ +final class ForgeClient { + private ForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(effect -> Minecraft.getInstance().gameRenderer.setPostEffect(effect)); + modBus.addListener((FMLClientSetupEvent event) -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); + modBus.addListener((EntityRenderersEvent.RegisterRenderers event) -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + modBus.addListener((EntityRenderersEvent.RegisterRenderers event) -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener((RegisterParticleProvidersEvent event) -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + modBus.addListener((RegisterKeyMappingsEvent event) -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener((RegisterClientReloadListenersEvent event) -> + ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); + modBus.addListener((AddGuiOverlayLayersEvent event) -> event.getLayeredDraw() + .add(ForgeLayeredDraw.POST_SLEEP_STACK, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + IEventBus forgeBus = MinecraftForge.EVENT_BUS; + forgeBus.addListener((TickEvent.ClientTickEvent event) -> { + if (event.phase == TickEvent.Phase.END) { + ClientHooks.clientTickEnd(); + } + }); + forgeBus.addListener((ViewportEvent.ComputeCameraAngles event) -> ClientHooks.computeCameraAngles(new Angles(event))); + forgeBus.addListener((ViewportEvent.ComputeFov event) -> event.setFOV(ClientHooks.fov((float) event.getFOV()))); + forgeBus.addListener((RenderHandEvent event) -> event.setCanceled(ClientHooks.hidesHand())); + forgeBus.addListener(EventPriority.NORMAL, false, RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getState(), event.getPoseStack(), ClientHooks.cameraPartialTicks())); + forgeBus.addListener(ForgeClient::onInteractionKey); + forgeBus.addListener((ClientPlayerNetworkEvent.LoggingIn event) -> ClientHooks.loggedIn(event.getPlayer())); + forgeBus.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } + + private record Angles(ViewportEvent.ComputeCameraAngles event) implements CameraAngles { + @Override + public Camera camera() { + return event.getCamera(); + } + + @Override + public float partialTick() { + return (float) event.getPartialTick(); + } + + @Override + public float yaw() { + return event.getYaw(); + } + + @Override + public float pitch() { + return event.getPitch(); + } + + @Override + public float roll() { + return event.getRoll(); + } + + @Override + public void setYaw(float yaw) { + event.setYaw(yaw); + } + + @Override + public void setPitch(float pitch) { + event.setPitch(pitch); + } + + @Override + public void setRoll(float roll) { + event.setRoll(roll); + } + } +} From fda19954c1d4a010fd15e171a0b5e2d1f168d941 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:36 +0800 Subject: [PATCH 1105/1461] Add ForgeNetwork.java to the Forge 1.21.5 glue --- .../mcheli/forge/ForgeNetwork.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java diff --git a/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java new file mode 100644 index 00000000..883dd91d --- /dev/null +++ b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.network.Channel; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.payload.PayloadFlow; + +/** + * Registers the shared packets on a Forge payload channel. Each packet travels under its own id, + * as on NeoForge and Fabric; the channel only adds Forge's version check. + */ +final class ForgeNetwork { + private static Channel channel; + + private ForgeNetwork() { + } + + static void register() { + PayloadFlow flow = ChannelBuilder + .named(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "main")) + .networkProtocolVersion(ModNetwork.PROTOCOL_VERSION) + .payloadChannel() + .play() + .serverbound(); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + flow = serverbound(flow, packet); + } + flow = flow.clientbound(); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + flow = clientbound(flow, packet); + } + channel = flow.build(); + } + + static Channel channel() { + return channel; + } + + private static PayloadFlow serverbound( + PayloadFlow flow, ModNetwork.Serverbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg, ctx.getSender())); + ctx.setPacketHandled(true); + }); + } + + private static PayloadFlow clientbound( + PayloadFlow flow, ModNetwork.Clientbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg)); + ctx.setPacketHandled(true); + }); + } +} From 728e3f894f428b94675d5d2a44cbefc4d4cd42fc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:37 +0800 Subject: [PATCH 1106/1461] Add ForgePlatform.java to the Forge 1.21.5 glue --- .../mcheli/forge/ForgePlatform.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java diff --git a/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java new file mode 100644 index 00000000..584079ac --- /dev/null +++ b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java @@ -0,0 +1,70 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.network.PacketDistributor; + +import java.nio.file.Path; +import java.util.Set; +import java.util.function.BiFunction; + +final class ForgePlatform implements Platform { + @Override + public String loaderName() { + return "forge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.SERVER.noArg()); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.PLAYER.with(player)); + } + + @Override + public BlockEntityType blockEntityType(BiFunction factory, Block block) { + return new BlockEntityType<>(factory::apply, Set.of(block)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From 6239ed2ab8382134b29213d5baed583979acaee7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:37 +0800 Subject: [PATCH 1107/1461] Add ForgeVehicleIngredient.java to the Forge 1.21.5 glue --- .../mcheli/forge/ForgeVehicleIngredient.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java diff --git a/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java new file mode 100644 index 00000000..51a8d1a9 --- /dev/null +++ b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraftforge.common.crafting.ingredients.AbstractIngredient; +import net.minecraftforge.common.crafting.ingredients.IIngredientSerializer; +import org.jspecify.annotations.Nullable; + +/** {@link VehicleIngredient} as a Forge ingredient, read from the {@code type} key. */ +final class ForgeVehicleIngredient extends AbstractIngredient { + static final MapCodec CODEC = VehicleIngredient.codec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final StreamCodec STREAM_CODEC = + VehicleIngredient.streamCodec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final IIngredientSerializer SERIALIZER = new IIngredientSerializer<>() { + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + public void write(RegistryFriendlyByteBuf buffer, ForgeVehicleIngredient value) { + STREAM_CODEC.encode(buffer, value); + } + + @Override + public ForgeVehicleIngredient read(RegistryFriendlyByteBuf buffer) { + return STREAM_CODEC.decode(buffer); + } + }; + + private final String vehicle; + + private ForgeVehicleIngredient(String vehicle) { + super(VehicleIngredient.kindItems()); + this.vehicle = vehicle; + } + + String vehicle() { + return vehicle; + } + + @Override + public boolean test(@Nullable ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IIngredientSerializer serializer() { + return SERIALIZER; + } + + @Override + public boolean equals(Object o) { + return o instanceof ForgeVehicleIngredient other && vehicle.equals(other.vehicle); + } + + @Override + public int hashCode() { + return vehicle.hashCode(); + } +} From 5a241cf7ef6342a78f80f486f0ff0ad79ee31be7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:38 +0800 Subject: [PATCH 1108/1461] Replace the Forge 1.21.5 placeholder entry point with the real one --- .../jnhprojects/mcheli/forge/McheliForge.java | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java index 8cb9b838..c878e1b4 100644 --- a/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java +++ b/forge-1.21.5/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java @@ -1,19 +1,76 @@ package com.jnhprojects.mcheli.forge; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegisterEvent; -/** - * Placeholder entry point for Minecraft 1.21.5. It only proves that the module builds and loads; - * the Forge glue from forge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +/** The Forge entry point: everything here only connects Forge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliForge(FMLJavaModLoadingContext context) { + IEventBus modBus = context.getModEventBus(); + Platform.install(new ForgePlatform()); + MCHeli.init(); - public McheliForge() { - LOGGER.info("MCHeli Rework placeholder loaded on Forge for Minecraft 1.21.5; no content yet"); + modBus.addListener((RegisterEvent event) -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(ForgeRegistries.Keys.INGREDIENT_SERIALIZERS, + helper -> helper.register(VehicleIngredient.ID, ForgeVehicleIngredient.SERIALIZER)); + }); + ForgeNetwork.register(); + modBus.addListener((FMLCommonSetupEvent event) -> event.enqueueWork(CommonHooks::commonSetup)); + modBus.addListener((AddPackFindersEvent event) -> event.addRepositorySource(CommonHooks.packSource())); + + IEventBus forgeBus = MinecraftForge.EVENT_BUS; + forgeBus.addListener((ServerAboutToStartEvent event) -> CommonHooks.serverAboutToStart(event.getServer())); + forgeBus.addListener((ServerStoppedEvent event) -> CommonHooks.serverStopped()); + forgeBus.addListener((TickEvent.ServerTickEvent event) -> { + if (event.phase == TickEvent.Phase.END) { + CommonHooks.serverTickEnd(event.getServer()); + } + }); + forgeBus.addListener((RegisterCommandsEvent event) -> CommonHooks.registerCommands(event.getDispatcher())); + forgeBus.addListener((PlayerEvent.PlayerLoggedInEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + forgeBus.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + ForgeClient.init(modBus); + } + } + + /** Only once Forge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } } } From d7ca7a241027af70c635a710331f86a3fd29e51f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:38 +0800 Subject: [PATCH 1109/1461] Open the post effect setter on Forge 1.21.5 for the thermal view --- forge-1.21.5/src/main/resources/META-INF/accesstransformer.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 forge-1.21.5/src/main/resources/META-INF/accesstransformer.cfg diff --git a/forge-1.21.5/src/main/resources/META-INF/accesstransformer.cfg b/forge-1.21.5/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 00000000..5033c20f --- /dev/null +++ b/forge-1.21.5/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,2 @@ +# Vanilla keeps the post effect setter private on 1.21.5; the thermal view needs it. +public net.minecraft.client.renderer.GameRenderer setPostEffect(Lnet/minecraft/resources/ResourceLocation;)V From a1b92606fc67fca14b2f298a180180a13ac51afd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:38 +0800 Subject: [PATCH 1110/1461] Drop the placeholder wording from the Forge 1.21.5 metadata --- forge-1.21.5/src/main/resources/META-INF/mods.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/forge-1.21.5/src/main/resources/META-INF/mods.toml b/forge-1.21.5/src/main/resources/META-INF/mods.toml index 47ec16fb..1fdc3b48 100644 --- a/forge-1.21.5/src/main/resources/META-INF/mods.toml +++ b/forge-1.21.5/src/main/resources/META-INF/mods.toml @@ -13,7 +13,6 @@ authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft Forge. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. From e0c7d6a6456941f01429737195d4f64fa30e01aa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:39 +0800 Subject: [PATCH 1111/1461] Define c:feathers on Forge 1.21.5, which only has forge:feathers --- .../src/main/resources/data/c/tags/item/feathers.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 forge-1.21.5/src/main/resources/data/c/tags/item/feathers.json diff --git a/forge-1.21.5/src/main/resources/data/c/tags/item/feathers.json b/forge-1.21.5/src/main/resources/data/c/tags/item/feathers.json new file mode 100644 index 00000000..4554f87e --- /dev/null +++ b/forge-1.21.5/src/main/resources/data/c/tags/item/feathers.json @@ -0,0 +1,5 @@ +{ + "values": [ + "#forge:feathers" + ] +} From c57c4d7935a5fec44e878efc89050ebd7714e23e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:39 +0800 Subject: [PATCH 1112/1461] Define c:gunpowders on Forge 1.21.5, which only has forge:gunpowder --- .../src/main/resources/data/c/tags/item/gunpowders.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 forge-1.21.5/src/main/resources/data/c/tags/item/gunpowders.json diff --git a/forge-1.21.5/src/main/resources/data/c/tags/item/gunpowders.json b/forge-1.21.5/src/main/resources/data/c/tags/item/gunpowders.json new file mode 100644 index 00000000..16c21708 --- /dev/null +++ b/forge-1.21.5/src/main/resources/data/c/tags/item/gunpowders.json @@ -0,0 +1,5 @@ +{ + "values": [ + "#forge:gunpowder" + ] +} From 0ed3ea5adb312951b8c8aae8e5e90048c2ff2dee Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:40 +0800 Subject: [PATCH 1113/1461] Set up the Forge 1.21.5 runs with the official mappings and its access transformer --- forge-1.21.5/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/forge-1.21.5/build.gradle b/forge-1.21.5/build.gradle index 51b53eb9..242d09c9 100644 --- a/forge-1.21.5/build.gradle +++ b/forge-1.21.5/build.gradle @@ -7,12 +7,18 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + minecraft { mappings channel: 'official', version: minecraft_version + useDefaultAccessTransformer() runs { configureEach { workingDir = rootProject.layout.projectDirectory.dir("run-${minecraft_version}") + systemProperty 'mcheli.legacyZip', legacyZip } register('client') From 2b120c4109b41a6d35e81186bc422db226677fa7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:40 +0800 Subject: [PATCH 1114/1461] Stop treating Forge 1.21.5 as a placeholder --- forge-1.21.5/gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/forge-1.21.5/gradle.properties b/forge-1.21.5/gradle.properties index 6afab053..41dc6daa 100644 --- a/forge-1.21.5/gradle.properties +++ b/forge-1.21.5/gradle.properties @@ -1,8 +1,7 @@ -# Minecraft 1.21.5 on Forge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 1.21.5 on Forge loader=forge minecraft_version=1.21.5 minecraft_version_range=[1.21.5,1.21.6) forge_version=55.1.18 loader_version_range=[55,) java_version=21 -scaffold=true From 309ef54ac35bac088dbe1ea2aa8ec08895768d34 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:41 +0800 Subject: [PATCH 1115/1461] Replace the NeoForge 1.21.5 placeholder entry point with the real one --- .../mcheli/neoforge/McheliNeoForge.java | 96 ++++++++++++++++--- 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java index fdc41c77..a1a02c70 100644 --- a/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java +++ b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java @@ -1,19 +1,93 @@ package com.jnhprojects.mcheli.neoforge; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Placeholder entry point for Minecraft 1.21.5. It only proves that the module builds and loads; - * the NeoForge glue from neoforge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.AddPackFindersEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import net.neoforged.neoforge.registries.RegisterEvent; + +/** The NeoForge entry point: everything here only connects NeoForge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliNeoForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliNeoForge(IEventBus modBus) { + Platform.install(new NeoForgePlatform()); + MCHeli.init(); + + modBus.addListener(RegisterEvent.class, event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(NeoForgeRegistries.Keys.INGREDIENT_TYPES, + helper -> helper.register(VehicleIngredient.ID, NeoForgeVehicleIngredient.TYPE)); + }); + modBus.addListener(RegisterPayloadHandlersEvent.class, McheliNeoForge::registerPackets); + modBus.addListener(FMLCommonSetupEvent.class, event -> event.enqueueWork(CommonHooks::commonSetup)); + modBus.addListener(AddPackFindersEvent.class, event -> event.addRepositorySource(CommonHooks.packSource())); + + NeoForge.EVENT_BUS.addListener(ServerAboutToStartEvent.class, event -> CommonHooks.serverAboutToStart(event.getServer())); + NeoForge.EVENT_BUS.addListener(ServerStoppedEvent.class, event -> CommonHooks.serverStopped()); + NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> CommonHooks.serverTickEnd(event.getServer())); + NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> CommonHooks.registerCommands(event.getDispatcher())); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedInEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedOutEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + NeoForgeClient.init(modBus); + } + } + + /** Only once NeoForge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } + } + + /** Handlers run on the main thread, which is NeoForge's default. */ + private static void registerPackets(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(Integer.toString(ModNetwork.PROTOCOL_VERSION)); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(registrar, packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(registrar, packet); + } + } + + private static void serverbound(PayloadRegistrar registrar, ModNetwork.Serverbound packet) { + registrar.playToServer(packet.type(), packet.codec(), + (msg, context) -> packet.handler().accept(msg, (ServerPlayer) context.player())); + } - public McheliNeoForge() { - LOGGER.info("MCHeli Rework placeholder loaded on NeoForge for Minecraft 1.21.5; no content yet"); + private static void clientbound(PayloadRegistrar registrar, ModNetwork.Clientbound packet) { + registrar.playToClient(packet.type(), packet.codec(), (msg, context) -> packet.handler().accept(msg)); } } From eb5eb88ad5d53e418720337a8d5601c8fb07e5b1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:41 +0800 Subject: [PATCH 1116/1461] Add NeoForgeClient.java to the NeoForge 1.21.5 glue --- .../mcheli/neoforge/NeoForgeClient.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java diff --git a/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java new file mode 100644 index 00000000..dd195bf7 --- /dev/null +++ b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.neoforge.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Minecraft; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; +import net.neoforged.neoforge.client.event.InputEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; +import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent; +import net.neoforged.neoforge.client.event.RenderHandEvent; +import net.neoforged.neoforge.client.event.RenderLivingEvent; +import net.neoforged.neoforge.client.event.ViewportEvent; +import net.neoforged.neoforge.client.gui.VanillaGuiLayers; +import net.neoforged.neoforge.common.NeoForge; + +/** + * Client events on NeoForge. Only loaded on a physical client. The camera hook is a mixin + * ({@code GameRendererMixin}): NeoForge's camera event fires before vanilla positions the camera, + * so a cockpit eye set from it would be overwritten. + */ +final class NeoForgeClient { + private NeoForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$setPostEffect(effect)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + modBus.addListener(RegisterMenuScreensEvent.class, event -> ClientHooks.registerMenuScreens(event::register)); + modBus.addListener(RegisterParticleProvidersEvent.class, event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + modBus.addListener(RegisterKeyMappingsEvent.class, event -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener(AddClientReloadListenersEvent.class, + event -> ClientHooks.reloadListeners().forEach(event::addListener)); + modBus.addListener(RegisterGuiLayersEvent.class, + event -> event.registerAbove(VanillaGuiLayers.SLEEP_OVERLAY, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post.class, event -> ClientHooks.clientTickEnd()); + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeFov.class, event -> event.setFOV(ClientHooks.fov((float) event.getFOV()))); + NeoForge.EVENT_BUS.addListener(RenderHandEvent.class, event -> event.setCanceled(ClientHooks.hidesHand())); + NeoForge.EVENT_BUS.addListener(RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getRenderState(), event.getPoseStack(), event.getPartialTick())); + NeoForge.EVENT_BUS.addListener(InputEvent.InteractionKeyMappingTriggered.class, NeoForgeClient::onInteractionKey); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingIn.class, event -> ClientHooks.loggedIn(event.getPlayer())); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingOut.class, event -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } +} From b1cffb2c36a86a682f6a20114a931954b98a6f9c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:42 +0800 Subject: [PATCH 1117/1461] Add NeoForgePlatform.java to the NeoForge 1.21.5 glue --- .../mcheli/neoforge/NeoForgePlatform.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java diff --git a/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java new file mode 100644 index 00000000..071dffd0 --- /dev/null +++ b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java @@ -0,0 +1,70 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.network.PacketDistributor; + +import java.nio.file.Path; +import java.util.Set; +import java.util.function.BiFunction; + +final class NeoForgePlatform implements Platform { + @Override + public String loaderName() { + return "neoforge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + PacketDistributor.sendToServer(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + + @Override + public BlockEntityType blockEntityType(BiFunction factory, Block block) { + return new BlockEntityType<>(factory::apply, Set.of(block)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From 56504e9d6d540e5e34c4146f10cadcf7ddda5bf4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:42 +0800 Subject: [PATCH 1118/1461] Add NeoForgeVehicleIngredient.java to the NeoForge 1.21.5 glue --- .../neoforge/NeoForgeVehicleIngredient.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java diff --git a/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java new file mode 100644 index 00000000..195be00a --- /dev/null +++ b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import net.minecraft.core.Holder; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.neoforged.neoforge.common.crafting.ICustomIngredient; +import net.neoforged.neoforge.common.crafting.IngredientType; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a NeoForge ingredient, read from the {@code neoforge:ingredient_type} key. */ +record NeoForgeVehicleIngredient(String vehicle) implements ICustomIngredient { + static final IngredientType TYPE = new IngredientType<>( + VehicleIngredient.codec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle), + VehicleIngredient.streamCodec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle)); + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> items() { + return VehicleIngredient.items(); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IngredientType getType() { + return TYPE; + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } +} From 4ee04f05a1c06c8096ff4024afe336cf9b40f231 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:42 +0800 Subject: [PATCH 1119/1461] Add CameraInvoker.java to the NeoForge 1.21.5 glue --- .../mcheli/neoforge/mixin/CameraInvoker.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java diff --git a/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java new file mode 100644 index 00000000..4c289d07 --- /dev/null +++ b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java @@ -0,0 +1,12 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** NeoForge adds a roll to {@code Camera#setRotation}, but leaves it protected. */ +@Mixin(Camera.class) +public interface CameraInvoker { + @Invoker("setRotation") + void mcheli$setRotation(float yRot, float xRot, float roll); +} From 083fdd7afe025006de3259c22371fe28d1b9a4a4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:43 +0800 Subject: [PATCH 1120/1461] Invoke the private post effect setter on NeoForge 1.21.5 --- .../mcheli/neoforge/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java diff --git a/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..b8646ada --- /dev/null +++ b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#setPostEffect} private on 1.21.5. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("setPostEffect") + void mcheli$setPostEffect(ResourceLocation effect); +} From ba23714a0b54cdb93add4112f176221c6a472811 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:43 +0800 Subject: [PATCH 1121/1461] Add GameRendererMixin.java to the NeoForge 1.21.5 glue --- .../neoforge/mixin/GameRendererMixin.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java diff --git a/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java new file mode 100644 index 00000000..2fbe5f3e --- /dev/null +++ b/neoforge-1.21.5/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, where Forge fires its camera angle event on 1.21.5: once the camera is placed + * for the frame, just before its rotation becomes the view matrix. NeoForge fires its own event + * before vanilla sets the camera position, which would undo a cockpit eye. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;rotation()Lorg/joml/Quaternionf;", + ordinal = 0)) + private void mcheli$beforeViewRotation(DeltaTracker deltaTracker, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, mainCamera.getPartialTickTime(), GameRendererMixin::setRotation); + } + + /** NeoForge turns the camera the other way round its roll axis than Forge does. */ + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((CameraInvoker) camera).mcheli$setRotation(yaw, pitch, -roll); + } +} From afe7f375d7a55239f790df23534fa6ddc574e5e7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:44 +0800 Subject: [PATCH 1122/1461] Drop the placeholder wording from the NeoForge 1.21.5 metadata --- .../src/main/resources/META-INF/neoforge.mods.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/neoforge-1.21.5/src/main/resources/META-INF/neoforge.mods.toml b/neoforge-1.21.5/src/main/resources/META-INF/neoforge.mods.toml index 2585f0b9..c8095914 100644 --- a/neoforge-1.21.5/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge-1.21.5/src/main/resources/META-INF/neoforge.mods.toml @@ -8,17 +8,19 @@ modId="${mod_id}" version="${mod_version}" displayName="${mod_name}" displayURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge" -logoFile="mcheli_logo.png" +iconFile="mcheli_logo.png" authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. ''' +[[mixins]] +config="mcheli.mixins.json" + [[dependencies.${mod_id}]] modId="neoforge" type="required" From 54ca9baec2f9d9347491a3aed52f2fb28e95e672 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:44 +0800 Subject: [PATCH 1123/1461] List the NeoForge 1.21.5 mixins --- .../src/main/resources/mcheli.mixins.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 neoforge-1.21.5/src/main/resources/mcheli.mixins.json diff --git a/neoforge-1.21.5/src/main/resources/mcheli.mixins.json b/neoforge-1.21.5/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..77522ff3 --- /dev/null +++ b/neoforge-1.21.5/src/main/resources/mcheli.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.neoforge.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "CameraInvoker", + "GameRendererInvoker", + "GameRendererMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From ff9b63ccb2eb7f65ce7e028f2aefad8c5db2bfdd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:45 +0800 Subject: [PATCH 1124/1461] Set up the NeoForge 1.21.5 runs --- neoforge-1.21.5/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neoforge-1.21.5/build.gradle b/neoforge-1.21.5/build.gradle index 6dded114..60829c34 100644 --- a/neoforge-1.21.5/build.gradle +++ b/neoforge-1.21.5/build.gradle @@ -7,12 +7,17 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + neoForge { version = neoforge_version runs { configureEach { gameDirectory = rootProject.file("run-${minecraft_version}") + systemProperty 'mcheli.legacyZip', legacyZip } client { client() From 36c59f7fff0e24230da4c5c8cd6ad7d4995d512c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:45 +0800 Subject: [PATCH 1125/1461] Stop treating NeoForge 1.21.5 as a placeholder and use the jspecify it pins --- neoforge-1.21.5/gradle.properties | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neoforge-1.21.5/gradle.properties b/neoforge-1.21.5/gradle.properties index 359852a0..99c716db 100644 --- a/neoforge-1.21.5/gradle.properties +++ b/neoforge-1.21.5/gradle.properties @@ -1,8 +1,9 @@ -# Minecraft 1.21.5 on NeoForge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 1.21.5 on NeoForge loader=neoforge minecraft_version=1.21.5 minecraft_version_range=[1.21.5,1.21.6) neoforge_version=21.5.101 loader_version_range=[21.5,) java_version=21 -scaffold=true +# NeoForge 1.21.5 pins this version strictly. +jspecify_version=1.0.0 From 4aeb06efa39c6f3112fb9b32294794a106f54d43 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:46 +0800 Subject: [PATCH 1126/1461] Add FabricPlatform.java to the Fabric 1.21.5 glue --- .../mcheli/fabric/FabricPlatform.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java new file mode 100644 index 00000000..707dd713 --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import java.nio.file.Path; +import java.util.function.BiFunction; + +final class FabricPlatform implements Platform { + @Override + public String loaderName() { + return "fabric"; + } + + @Override + public Path gameDir() { + return FabricLoader.getInstance().getGameDir(); + } + + @Override + public Path configDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + public String modVersion() { + return FabricLoader.getInstance().getModContainer(MCHeli.MOD_ID) + .map(c -> c.getMetadata().getVersion().getFriendlyString()).orElse(""); + } + + @Override + public boolean isClient() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPlayNetworking.send(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ServerPlayNetworking.send(player, payload); + } + + /** Fabric orders tabs by registration, which already puts ours one after another. */ + @Override + public BlockEntityType blockEntityType(BiFunction factory, Block block) { + return FabricBlockEntityTypeBuilder.create(factory::apply, block).build(); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return FabricItemGroup.builder(); + } +} From d4b4422f5db27ede2fea2065fce66f48eae5b3af Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:46 +0800 Subject: [PATCH 1127/1461] Add FabricVehicleIngredient.java to the Fabric 1.21.5 glue --- .../fabric/FabricVehicleIngredient.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java new file mode 100644 index 00000000..5267068a --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Holder; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a Fabric ingredient, read from the {@code fabric:type} key. */ +record FabricVehicleIngredient(String vehicle) implements CustomIngredient { + static final CustomIngredientSerializer SERIALIZER = new CustomIngredientSerializer<>() { + private final MapCodec codec = + VehicleIngredient.codec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + private final StreamCodec streamCodec = + VehicleIngredient.streamCodec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + + @Override + public ResourceLocation getIdentifier() { + return VehicleIngredient.ID; + } + + @Override + public MapCodec getCodec() { + return codec; + } + + @Override + public StreamCodec getPacketCodec() { + return streamCodec; + } + }; + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> getMatchingItems() { + return VehicleIngredient.items(); + } + + @Override + public boolean requiresTesting() { + return true; + } + + @Override + public CustomIngredientSerializer getSerializer() { + return SERIALIZER; + } + + @Override + public SlotDisplay toDisplay() { + return VehicleIngredient.display(vehicle); + } +} From 12b8dc4456c32dc29519ceb19e983377f94bba5b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:47 +0800 Subject: [PATCH 1128/1461] Replace the Fabric 1.21.5 placeholder entry point with the real one --- .../mcheli/fabric/McheliFabric.java | 69 +++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java index d3f6badd..31233345 100644 --- a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java @@ -1,19 +1,74 @@ package com.jnhprojects.mcheli.fabric; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; import net.fabricmc.api.ModInitializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; /** - * Placeholder entry point for Minecraft 1.21.5. It only proves that the module builds and loads; - * the Fabric glue from fabric-26.3 replaces it when this version is ported for a final release. + * The Fabric entry point: everything here only connects Fabric to the shared code. The imported + * content pack is added by {@code PackRepositoryMixin}, since Fabric has no pack finder event. */ public final class McheliFabric implements ModInitializer { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); - @Override public void onInitialize() { - LOGGER.info("MCHeli Rework placeholder loaded on Fabric for Minecraft 1.21.5; no content yet"); + Platform.install(new FabricPlatform()); + MCHeli.init(); + + for (ResourceKey> registry : Registration.registries()) { + register(Registration.typed(registry)); + } + CustomIngredientSerializer.register(FabricVehicleIngredient.SERIALIZER); + registerPackets(); + CommonHooks.commonSetup(); + + ServerLifecycleEvents.SERVER_STARTING.register(CommonHooks::serverAboutToStart); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> CommonHooks.serverStopped()); + ServerTickEvents.END_SERVER_TICK.register(CommonHooks::serverTickEnd); + CommandRegistrationCallback.EVENT.register((dispatcher, context, selection) -> CommonHooks.registerCommands(dispatcher)); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> CommonHooks.playerLoggedIn(handler.getPlayer())); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> CommonHooks.playerLoggedOut(handler.getPlayer())); + } + + @SuppressWarnings("unchecked") + private static void register(ResourceKey> key) { + Registry registry = (Registry) BuiltInRegistries.REGISTRY.getValue(key.location()); + if (registry == null) { + throw new IllegalStateException("No registry " + key.location()); + } + Registration.registerAll(key, (id, value) -> Registry.register(registry, id, value)); + } + + /** Client handlers are registered by {@link McheliFabricClient}; handlers run on the main thread. */ + private static void registerPackets() { + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + } + + private static void clientbound(ModNetwork.Clientbound packet) { + PayloadTypeRegistry.playS2C().register(packet.type(), packet.codec()); + } + + private static void serverbound(ModNetwork.Serverbound packet) { + PayloadTypeRegistry.playC2S().register(packet.type(), packet.codec()); + ServerPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg, context.player())); } } From 07742d36bc96fb144111bebe897f3377a5054861 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:47 +0800 Subject: [PATCH 1129/1461] Add McheliFabricClient.java to the Fabric 1.21.5 glue --- .../mcheli/fabric/McheliFabricClient.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java new file mode 100644 index 00000000..435a1dbf --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.fabric.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ResourceManager; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * Client side of the Fabric entry point. The camera, hand, key and rider hooks are mixins (see + * {@code com.jnhprojects.mcheli.fabric.mixin}), since Fabric API has no events for them. + */ +public final class McheliFabricClient implements ClientModInitializer { + + @Override + public void onInitializeClient() { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$setPostEffect(effect)); + ClientHooks.registerEntityRenderers(EntityRendererRegistry::register); + ClientHooks.registerBlockEntityRenderers(BlockEntityRenderers::register); + ClientHooks.registerMenuScreens(MenuScreens::register); + ParticleFactoryRegistry.getInstance().register(ModParticles.GLOW.get(), GlowParticle.Provider::new); + ParticleFactoryRegistry.getInstance().register(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + ClientHooks.keyMappings().forEach(KeyBindingHelper::registerKeyBinding); + ResourceManagerHelper resources = ResourceManagerHelper.get(PackType.CLIENT_RESOURCES); + ClientHooks.reloadListeners().forEach((id, listener) -> resources.registerReloadListener(new Identified(id, listener))); + // Drawn after the vanilla HUD, which is where Forge's post sleep layer ends up too. + HudRenderCallback.EVENT.register(ClientHooks::drawHud); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + + ClientTickEvents.END_CLIENT_TICK.register(client -> ClientHooks.clientTickEnd()); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> ClientHooks.loggedIn(client.player)); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ClientHooks.loggedOut()); + } + + private static void clientbound(ModNetwork.Clientbound packet) { + ClientPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg)); + } + + /** Fabric wants an id on every reload listener. */ + private record Identified(ResourceLocation id, PreparableReloadListener listener) implements IdentifiableResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return id; + } + + @Override + public CompletableFuture reload(PreparationBarrier barrier, ResourceManager manager, Executor background, Executor main) { + return listener.reload(barrier, manager, background, main); + } + } +} From 2dfbb7488170095626cbc9d9609485ff2e357441 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:48 +0800 Subject: [PATCH 1130/1461] Add RollableCamera.java to the Fabric 1.21.5 glue --- .../java/com/jnhprojects/mcheli/fabric/RollableCamera.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java new file mode 100644 index 00000000..ff30f0ff --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java @@ -0,0 +1,7 @@ +package com.jnhprojects.mcheli.fabric; + +/** Implemented on {@code Camera} by {@code CameraMixin}. Outside the mixin package, which Mixin refuses to load classes from. */ +public interface RollableCamera { + /** Like vanilla's {@code setRotation}, with a roll in degrees as Forge applies it. */ + void mcheli$setRotation(float yRot, float xRot, float roll); +} From e96c0d99d655fa8c39855ee91c689677f40c56ba Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:48 +0800 Subject: [PATCH 1131/1461] Add CameraMixin.java to the Fabric 1.21.5 glue --- .../mcheli/fabric/mixin/CameraMixin.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java new file mode 100644 index 00000000..d16eb926 --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +/** + * Vanilla's camera has no roll. This is {@code Camera#setRotation} with the roll Forge patches in, + * so a banked aircraft tilts the view the same on every loader. + */ +@Mixin(Camera.class) +abstract class CameraMixin implements RollableCamera { + @Shadow + @Final + private static Vector3f FORWARDS; + @Shadow + @Final + private static Vector3f UP; + @Shadow + @Final + private static Vector3f LEFT; + + @Shadow + private float xRot; + @Shadow + private float yRot; + @Shadow + @Final + private Vector3f forwards; + @Shadow + @Final + private Vector3f up; + @Shadow + @Final + private Vector3f left; + @Shadow + @Final + private Quaternionf rotation; + + @Override + public void mcheli$setRotation(float yRot, float xRot, float roll) { + this.xRot = xRot; + this.yRot = yRot; + this.rotation.rotationYXZ((float) Math.PI - yRot * (float) (Math.PI / 180.0), -xRot * (float) (Math.PI / 180.0), + roll * (float) (Math.PI / 180.0)); + FORWARDS.rotate(this.rotation, this.forwards); + UP.rotate(this.rotation, this.up); + LEFT.rotate(this.rotation, this.left); + } +} From 32cf236440427c2900435206df40886a1b1d625d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:48 +0800 Subject: [PATCH 1132/1461] Invoke the private post effect setter on Fabric 1.21.5 --- .../mcheli/fabric/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..758f2ba0 --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#setPostEffect} private on 1.21.5. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("setPostEffect") + void mcheli$setPostEffect(ResourceLocation effect); +} From 71f64883af5c0a5bd5ae3763a05dc94442a557ba Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:49 +0800 Subject: [PATCH 1133/1461] Add GameRendererMixin.java to the Fabric 1.21.5 glue --- .../fabric/mixin/GameRendererMixin.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java new file mode 100644 index 00000000..a74fad9c --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -0,0 +1,48 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, where Forge fires its camera angle event on 1.21.5 (the camera is placed for + * the frame, and its rotation is about to become the view matrix), and the first person hand, + * which is hidden in some seats. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;rotation()Lorg/joml/Quaternionf;", + ordinal = 0)) + private void mcheli$beforeViewRotation(CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, mainCamera.getPartialTickTime(), GameRendererMixin::setRotation); + } + + @Inject(method = "renderItemInHand", at = @At("HEAD"), cancellable = true) + private void mcheli$renderItemInHand(CallbackInfo ci) { + if (ClientHooks.hidesHand()) { + ci.cancel(); + } + } + + /** Zoom through a sight, where Forge fires its field of view event. */ + @Inject(method = "getFov", at = @At("RETURN"), cancellable = true) + private void mcheli$getFov(Camera camera, float partialTicks, boolean useFovSetting, CallbackInfoReturnable cir) { + cir.setReturnValue(ClientHooks.fov(cir.getReturnValueF())); + } + + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((RollableCamera) camera).mcheli$setRotation(yaw, pitch, roll); + } +} From 1fa228ea6bf3f639374c369fd8a1cbcbbc4790fe Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:49 +0800 Subject: [PATCH 1134/1461] Add the Fabric rider pose mixin for the 1.21.5 render state --- .../mixin/LivingEntityRendererMixin.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java new file mode 100644 index 00000000..e0897e6a --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java @@ -0,0 +1,22 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +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.callback.CallbackInfo; + +/** Tilts riders with their vehicle, where Forge fires its living entity render event. */ +@Mixin(LivingEntityRenderer.class) +abstract class LivingEntityRendererMixin { + + @Inject(method = "render(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;" + + "Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At("HEAD")) + private void mcheli$beforeRender(LivingEntityRenderState state, PoseStack pose, MultiBufferSource buffers, int light, CallbackInfo ci) { + ClientHooks.beforeRenderLiving(state, pose, ClientHooks.cameraPartialTicks()); + } +} From 2c1dd553835fadc8fe12fdb13d17909b1e193971 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:50 +0800 Subject: [PATCH 1135/1461] Add MinecraftMixin.java to the Fabric 1.21.5 glue --- .../mcheli/fabric/mixin/MinecraftMixin.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java new file mode 100644 index 00000000..b4685f81 --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.HitResult; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** The use and pick block keys, which Forge and NeoForge let mods take through an input event. */ +@Mixin(Minecraft.class) +abstract class MinecraftMixin { + @Shadow + public @Nullable LocalPlayer player; + @Shadow + public @Nullable HitResult hitResult; + @Shadow + private int rightClickDelay; + + @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) + private void mcheli$startUseItem(CallbackInfo ci) { + if (player == null) { + return; + } + ClientHooks.UseKey result = ClientHooks.onUseKey(InteractionHand.MAIN_HAND); + if (result != ClientHooks.UseKey.PASS) { + // Vanilla sets the same delay before it looks at the hands. + rightClickDelay = 4; + if (result == ClientHooks.UseKey.CONSUME_AND_SWING) { + player.swing(InteractionHand.MAIN_HAND); + } + ci.cancel(); + } + } + + @Inject(method = "pickBlock", at = @At("HEAD"), cancellable = true) + private void mcheli$pickBlock(CallbackInfo ci) { + if (hitResult != null && hitResult.getType() != HitResult.Type.MISS && ClientHooks.onPickBlockKey()) { + ci.cancel(); + } + } +} From 5e078aa0aa892078e4a97c6abdda6e73997ca0e3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:50 +0800 Subject: [PATCH 1136/1461] Add PackRepositoryMixin.java to the Fabric 1.21.5 glue --- .../fabric/mixin/PackRepositoryMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java new file mode 100644 index 00000000..965719b9 --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.CommonHooks; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.Arrays; + +/** + * Adds the imported original content to every pack repository, client resources and server data + * alike, which is what Forge and NeoForge do through their pack finder event. + */ +@Mixin(PackRepository.class) +abstract class PackRepositoryMixin { + + @ModifyVariable(method = "", at = @At("HEAD"), argsOnly = true) + private static RepositorySource[] mcheli$addImportedContent(RepositorySource[] sources) { + RepositorySource[] withOurs = Arrays.copyOf(sources, sources.length + 1); + withOurs[sources.length] = CommonHooks.packSource(); + return withOurs; + } +} From 2cb8ad3d65634db1269ac1abd938fb3afe24b126 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:51 +0800 Subject: [PATCH 1137/1461] Add our particle render types to the list vanilla draws on Fabric 1.21.5 --- .../fabric/mixin/ParticleEngineMixin.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java new file mode 100644 index 00000000..87ebfeb7 --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java @@ -0,0 +1,32 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.google.common.collect.ImmutableList; +import com.jnhprojects.mcheli.client.render.Glow; +import net.minecraft.client.particle.ParticleEngine; +import net.minecraft.client.particle.ParticleRenderType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +/** + * Vanilla only draws the particle render types on its own list, which leaves our glow and smoke + * particles invisible. Forge and NeoForge draw every render type in use. + */ +@Mixin(ParticleEngine.class) +abstract class ParticleEngineMixin { + @Shadow + @Final + @Mutable + private static List RENDER_ORDER; + + @Inject(method = "", at = @At("RETURN")) + private static void mcheli$addRenderTypes(CallbackInfo ci) { + RENDER_ORDER = ImmutableList.builder().addAll(RENDER_ORDER).add(Glow.PARTICLE_LAYER, Glow.SMOKE_LAYER).build(); + } +} From f4dec89775903f7e8b5270da168554e162c09ded Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:51 +0800 Subject: [PATCH 1138/1461] Add RuntimeSoundInstanceMixin.java to the Fabric 1.21.5 glue --- .../mixin/RuntimeSoundInstanceMixin.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java diff --git a/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java new file mode 100644 index 00000000..35f4032b --- /dev/null +++ b/fabric-1.21.5/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java @@ -0,0 +1,20 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.concurrent.CompletableFuture; + +/** Streams content pack sounds from disk through Fabric's sound instance hook. */ +@Mixin(RuntimeSoundInstance.class) +abstract class RuntimeSoundInstanceMixin implements FabricSoundInstance { + + @Override + public CompletableFuture getAudioStream(SoundBufferLibrary loader, ResourceLocation id, boolean repeatInstantly) { + return ((RuntimeSoundInstance) (Object) this).openStream(loader, id, repeatInstantly); + } +} From 433641324bf4569265b0baca421256ec9b9c82c9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:52 +0800 Subject: [PATCH 1139/1461] Drop the placeholder wording from the Fabric 1.21.5 metadata --- fabric-1.21.5/src/main/resources/fabric.mod.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fabric-1.21.5/src/main/resources/fabric.mod.json b/fabric-1.21.5/src/main/resources/fabric.mod.json index 1b87b793..8346f4c2 100644 --- a/fabric-1.21.5/src/main/resources/fabric.mod.json +++ b/fabric-1.21.5/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "${mod_id}", "version": "${mod_version}", "name": "${mod_name}", - "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", + "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", "authors": ["JayMar921 (rework)", "EMB4 (original author)", "Fureniku (CurseForge maintainer)"], "contact": { "homepage": "https://github.com/JnH-Projects/MCHeli-Rework-Forge", @@ -14,8 +14,10 @@ "icon": "mcheli_logo.png", "environment": "*", "entrypoints": { - "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"] + "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"], + "client": ["com.jnhprojects.mcheli.fabric.McheliFabricClient"] }, + "mixins": ["mcheli.mixins.json"], "depends": { "fabricloader": "${loader_version_range}", "fabric-api": "*", From 1b7b2abbe604d7ed4442080e8c815bf75da5d1c4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:52 +0800 Subject: [PATCH 1140/1461] List the Fabric 1.21.5 mixins --- .../src/main/resources/mcheli.mixins.json | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 fabric-1.21.5/src/main/resources/mcheli.mixins.json diff --git a/fabric-1.21.5/src/main/resources/mcheli.mixins.json b/fabric-1.21.5/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..22cab45a --- /dev/null +++ b/fabric-1.21.5/src/main/resources/mcheli.mixins.json @@ -0,0 +1,21 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "PackRepositoryMixin" + ], + "client": [ + "CameraMixin", + "GameRendererInvoker", + "GameRendererMixin", + "LivingEntityRendererMixin", + "MinecraftMixin", + "ParticleEngineMixin", + "RuntimeSoundInstanceMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From c68b3bba7b26724cdde9167bfab777c8c9c0ef8f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:53 +0800 Subject: [PATCH 1141/1461] Set up the Fabric 1.21.5 runs --- fabric-1.21.5/build.gradle | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fabric-1.21.5/build.gradle b/fabric-1.21.5/build.gradle index 2cb25151..18ea8484 100644 --- a/fabric-1.21.5/build.gradle +++ b/fabric-1.21.5/build.gradle @@ -7,10 +7,15 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + loom { runs { configureEach { runDir = "../run-${minecraft_version}" + vmArg "-Dmcheli.legacyZip=${legacyZip}" } server { programArgs '--nogui' @@ -20,7 +25,7 @@ loom { dependencies { minecraft "com.mojang:minecraft:${minecraft_version}" - // 1.21.5 is obfuscated; Mojang's names keep the code the same as on Forge and NeoForge. + // 1.21.5 is obfuscated; Mojang's names keep the shared code the same as on Forge and NeoForge. mappings loom.officialMojangMappings() modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" From f9258e0af2f7fdcdeabc4f435ba2f80d36ced52f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:53 +0800 Subject: [PATCH 1142/1461] Stop treating Fabric 1.21.5 as a placeholder --- fabric-1.21.5/gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fabric-1.21.5/gradle.properties b/fabric-1.21.5/gradle.properties index 987d2f24..d12bc0d5 100644 --- a/fabric-1.21.5/gradle.properties +++ b/fabric-1.21.5/gradle.properties @@ -1,4 +1,4 @@ -# Minecraft 1.21.5 on Fabric (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 1.21.5 on Fabric loader=fabric minecraft_version=1.21.5 minecraft_version_range=~1.21.5 @@ -6,4 +6,3 @@ fabric_loader_version=0.20.1 loader_version_range=>=0.16.0 fabric_api_version=0.128.2+1.21.5 java_version=21 -scaffold=true From b9ab21350bee33ba273322fef4bb941ad56f397e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:53 +0800 Subject: [PATCH 1143/1461] Build the 1.21.5 modules --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 30aac210..f1948a43 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mod_license=MCHeli Rework Licence 1.0 (see LICENSE) # Platform modules built by this project, one per Minecraft version. Each has its own # gradle.properties with the Minecraft, loader and Java versions. -platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-26.2,neoforge-26.2,fabric-26.2,forge-26.1,neoforge-26.1,fabric-26.1,forge-1.21.11,neoforge-1.21.11,fabric-1.21.11,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 +platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-26.2,neoforge-26.2,fabric-26.2,forge-26.1,neoforge-26.1,fabric-26.1,forge-1.21.11,neoforge-1.21.11,fabric-1.21.11,forge-1.21.5,neoforge-1.21.5,fabric-1.21.5,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 # Libraries the common module compiles against. Every supported Minecraft version ships them. slf4j_version=2.0.17 From 93b1bdd0dcea98a3904fa1694fc82bd2681c9b36 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:54 +0800 Subject: [PATCH 1144/1461] Note the 1.21.5 port in the changelog --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 034c2ca8..9d318f5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,13 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). the vehicle screen, the drafting table, team manager, UAV stations and the workshop. On 1.21.1 the PVP button of the team manager switches the server's PVP setting until a restart, since that version has no game rule for it, and target markers ignore view bobbing. -- Minecraft 26.2, 26.1 and 1.21.11 on Forge, NeoForge and Fabric, with everything 26.3 has. On 26.1 and - 1.21.11 teams pick from the sixteen chat colours, since those versions have no separate team colours. +- Minecraft 26.2, 26.1, 1.21.11 and 1.21.5 on Forge, NeoForge and Fabric, with everything 26.3 has. On 26.1, + 1.21.11 and 1.21.5 teams pick from the sixteen chat colours, since those versions have no separate team + colours. On 1.21.5 the PVP button of the team manager switches the server's PVP setting until a restart. ### Changed -- Forge, NeoForge and Fabric on 26.3, 26.2, 26.1, 1.21.11 and 1.21.1 are all built on every pull request. +- Forge, NeoForge and Fabric on 26.3, 26.2, 26.1, 1.21.11, 1.21.5 and 1.21.1 are all built on every pull request. - Clients and servers on different loaders cannot play together, which the roadmap used to promise. Each loader refuses the other at its own handshake, before the mod runs. A pack zip still works on every loader when copied by hand. From a0afdbde9cc695fee44fd380325cde2efbbd8a49 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:54 +0800 Subject: [PATCH 1145/1461] List Minecraft 1.21.5 in the README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee522e0c..bf70014d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ MCHeli Rework icon Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3**, **26.2**, **26.1**, -**1.21.11** and **1.21.1**, on **Forge**, **NeoForge** and **Fabric**. +**1.21.11**, **1.21.5** and **1.21.1**, on **Forge**, **NeoForge** and **Fabric**. MCHeli Rework is a ground-up port of MCHeli 1.0.3, a mod that stopped at Minecraft 1.7.10. Every system is rewritten for the modern game, while the original vehicle definitions, models, @@ -90,13 +90,14 @@ Reserved original, the code is source available rather than open source. | 26.2 | 65.1.5 or newer in 65.x | 26.2.0.93 or newer | Loader 0.20 and Fabric API | 25 | | 26.1 | 62.0.9 or newer in 62.x | 26.1.2.126 or newer | Loader 0.20 and Fabric API | 25 | | 1.21.11 | 61.2.4 or newer in 61.x | 21.11.48 or newer | Loader 0.16 and Fabric API | 21 | +| 1.21.5 | 55.1.18 or newer in 55.x | 21.5.101 or newer | Loader 0.16 and Fabric API | 21 | | 1.21.1 | 52.1.0 or newer | 21.1.200 or newer | Loader 0.16 and Fabric API | 21 | The launcher bundles the right Java for each version. ## Installing -1. Install Forge, NeoForge or Fabric for Minecraft 26.3, 26.2, 26.1, 1.21.11 or 1.21.1. On Fabric, also install +1. Install Forge, NeoForge or Fabric for Minecraft 26.3, 26.2, 26.1, 1.21.11, 1.21.5 or 1.21.1. On Fabric, also install [Fabric API](https://modrinth.com/mod/fabric-api). 2. Download the jar for your loader from the [releases](https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases): From faa0a0c40d129cef6b6c1a339030d7479b6ecc65 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:55 +0800 Subject: [PATCH 1146/1461] Tick the 1.21.5 port on the roadmap --- docs/ROADMAP.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0b2bb4c5..7c4b661b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -309,12 +309,12 @@ added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest - [x] Every resource the repository holds ships in the jar (betas 4 to 8 lacked recipes, the thermal shader, the smoke particle and the drafting table model) - [x] Phases 8 to 12 ported to all three loaders on 1.21.1 -- [ ] Every module in `platforms` (26.3, 26.2, 26.1, 1.21.11 and 1.21.1 are in) +- [ ] Every module in `platforms` (26.3, 26.2, 26.1, 1.21.11, 1.21.5 and 1.21.1 are in) - [x] `minecraft-26.2` with its Forge, NeoForge and Fabric glue - [x] `minecraft-26.1` with its Forge, NeoForge and Fabric glue - [x] `minecraft-1.21.11` with its Forge, NeoForge and Fabric glue -- [ ] `minecraft-1.21.5`, - `minecraft-1.20.6`, `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its +- [x] `minecraft-1.21.5` with its Forge, NeoForge and Fabric glue +- [ ] `minecraft-1.20.6`, `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its placeholder modules with real glue - [ ] `common` compiles for Java 17 (no pattern matching `switch`, no `SequencedCollection` methods) From b5d61fc45e6d04e02c39370c860c49c55ee4e029 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:55 +0800 Subject: [PATCH 1147/1461] Describe what differs on 1.21.5 --- docs/ARCHITECTURE.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2a5d12ed..2e287851 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -51,7 +51,7 @@ loader. The top layer is a thin module per loader and Minecraft version, which c lower layers together with its own glue into one jar. See [DEVELOPMENT.md](DEVELOPMENT.md#three-layers). -Only 26.3, 26.2, 26.1, 1.21.11 and 1.21.1 have the middle layer so far. The other supported versions have placeholder +Only 26.3, 26.2, 26.1, 1.21.11, 1.21.5 and 1.21.1 have the middle layer so far. The other supported versions have placeholder loader modules (`scaffold=true`) that compile neither lower layer: just an entry point that logs, against that version's loader and toolchain. Porting a version means adding its `minecraft-` folder, copying the loader glue in and dropping the flag. @@ -139,6 +139,26 @@ Forge sets the official mappings and Fabric uses the remapping Loom plugin. - **Conventional tags.** Forge on 1.21.x has `forge:gunpowder` and `forge:feathers` but not the `c:` names our recipes use, so its modules define `c:gunpowders` and `c:feathers` from those. +### Minecraft 1.21.5 + +`minecraft-1.21.5` takes entities, items, recipes and networking from `minecraft-1.21.11`, and its +screens' input handling and the workshop from `minecraft-1.21.1`. What differs: + +- **Rendering.** There are render states but no submit step. `render` draws straight into a + `MultiBufferSource`, and `Geometry.submit` keeps those calls reading the same as elsewhere. +- **Save data.** `CompoundTag` has `store`, `read` and `getIntOr`, so it stands in for `ValueOutput` + and `ValueInput`. Anything holding items needs the registries passed along. +- **Interpolation.** A packet with only a rotation interpolates to where the entity is now, dropping + the target, so `VehicleInterpolation` keeps the target in that case. +- **Thermal view.** Post effects use plain uniforms: `InSize` rather than a `SamplerInfo` block, and + a `uniforms` list in the JSON. +- **`blit`.** It takes a render type function, then position, texels and size in that order, so a + call from 1.21.1 with its size first still compiles and draws the wrong region. +- **Glow.** Our render types come from a nested subclass, the only place the shards they need can be + seen, and vanilla's `itemEntityTranslucentCull` draws translucent rounds. +- **Hull outline.** The render step has no entity, so the extract step copies the boxes into the state. +- **Teams.** There is no PVP game rule, so the team manager switches the server's setting, as on 1.21.1. + ### Minecraft 1.21.1 `minecraft-1.21.1` is a copy of `minecraft-26.3` ported back, so the two read side by side. During From 1be94a9f248d3844c8341b89831b68dd4195e0c4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:56 +0800 Subject: [PATCH 1148/1461] List the 1.21.5 modules --- docs/DEVELOPMENT.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 5c47f1de..346827f5 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -21,6 +21,9 @@ | Forge | 61.2.4 for Minecraft 1.21.11 | Built with ForgeGradle 7 and the official mappings. | | NeoForge | 21.11.48 for Minecraft 1.21.11 | | | Fabric | Fabric API 0.141.6+1.21.11 | Loom remaps the jar, which is obfuscated on 1.21.11. | +| Forge | 55.1.18 for Minecraft 1.21.5 | Built with ForgeGradle 7 and the official mappings. | +| NeoForge | 21.5.101 for Minecraft 1.21.5 | | +| Fabric | Fabric API 0.128.2+1.21.5 | Loom remaps the jar, which is obfuscated on 1.21.5. | | Forge | 52.1.20 for Minecraft 1.21.1 | Built with ForgeGradle 7 and the official mappings. | | NeoForge | 21.1.265 for Minecraft 1.21.1 | | | Fabric | Fabric API 0.116.17+1.21.1 | Loom remaps the jar, which is obfuscated on 1.21.1. | @@ -50,7 +53,7 @@ Shared properties live in the root `gradle.properties`: `mod_version`, the mod m of loader modules (`platforms`) and the library versions `common` compiles against. Each loader module has its own `gradle.properties` with its loader, Minecraft and Java versions. During the betas up to beta.8, `platforms` listed only `forge-26.3`. Roadmap phase 13 adds modules back as each -one catches up, currently the three each on 26.3, 26.2, 26.1, 1.21.11 and 1.21.1; the others stay in the repository +one catches up, currently the three each on 26.3, 26.2, 26.1, 1.21.11, 1.21.5 and 1.21.1; the others stay in the repository until they are ported. `mods.toml`, `neoforge.mods.toml`, `fabric.mod.json` and `pack.mcmeta` are filled in from them at build time. What every loader module shares (source folders, jar contents, manifest, file @@ -75,6 +78,8 @@ minecraft-26.1/ the same shared code on Minecraft 26.1 forge-26.1/, neoforge-26.1/, fabric-26.1/ the loader glue for 26.1 minecraft-1.21.11/ the same shared code on Minecraft 1.21.11 forge-1.21.11/, neoforge-1.21.11/, fabric-1.21.11/ the loader glue for 1.21.11 +minecraft-1.21.5/ the same shared code on Minecraft 1.21.5 +forge-1.21.5/, neoforge-1.21.5/, fabric-1.21.5/ the loader glue for 1.21.5 minecraft-1.21.1/ the same shared code, backported to Minecraft 1.21.1 forge-1.21.1/, neoforge-1.21.1/, fabric-1.21.1/ the loader glue for 1.21.1 -/ placeholders for the other supported versions (see below) @@ -121,8 +126,8 @@ of them on the command line. After pulling the split, re-import the Gradle proje ### Placeholder modules Every [supported version](ROADMAP.md#supported-versions) has a loader module, but only 26.3, 26.2, -26.1, 1.21.11 and 1.21.1 are ported. The other 10 (`forge-`, `neoforge-` and `fabric-` for -1.21.5 and 1.20.6, plus `forge-` and `fabric-` for 1.20.2 and 1.19.4) are placeholders: each pins +26.1, 1.21.11, 1.21.5 and 1.21.1 are ported. The other 7 (`forge-`, `neoforge-` and `fabric-` for +1.20.6, plus `forge-` and `fabric-` for 1.20.2 and 1.19.4) are placeholders: each pins its loader, mappings and Java version, and holds only an entry point that logs a line at startup and the mod metadata. Their `gradle.properties` sets `scaffold=true`, which tells `gradle/loader.gradle` to leave out `common` and `minecraft-`, so they build before that From b2fb5cd48902f3cc06304be38c647c9485b0ee46 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:04:56 +0800 Subject: [PATCH 1149/1461] Add 1.21.5 checks for jars, interpolation, save data and textures --- docs/TESTING.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/TESTING.md b/docs/TESTING.md index 43ac9ec3..b707e62f 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -48,6 +48,15 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi another loader is refused by the loaders themselves, with a message naming the one it needs. - [ ] A `serverconfig/mcheli-server.toml` written by 1.1.0-beta.2 (Forge) keeps its values. +### 1.21.5 only + +- [ ] From the built jars on a launcher profile: all three loaders start, and thermal turns on and off. +- [ ] A vehicle that turns on the spot stays where the server has it, seen from a second client. +- [ ] Inventory, cargo and a UAV station's item survive a world reload. +- [ ] F3+B outlines the hull boxes next to vanilla's box, and they follow a moving vehicle. +- [ ] The vehicle screen, drafting table and workshop textures sit where they should. +- [ ] The team manager's PVP button works, and PVP goes back to `server.properties` after a restart. + ### 1.21.11 only - [ ] From the built jars on a launcher profile: all three loaders start, which proves Fabric's remapped From efbbebba7fd854a2031f10bf4c440492cf42c02e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:28:59 +0800 Subject: [PATCH 1150/1461] Add the 1.20.6 import target, whose recipe folder has its plural name --- .../com/jnhprojects/mcheli/content/ImportTarget.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/ImportTarget.java b/common/src/main/java/com/jnhprojects/mcheli/content/ImportTarget.java index 0753c4fc..a68acc34 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/ImportTarget.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/ImportTarget.java @@ -15,5 +15,14 @@ public enum ImportTarget { * the vehicle's model by {@code custom_model_data} (see {@link LegacyImporter#ICONS_FILE}), and * recipe ingredients are {@code {"item": ...}} and {@code {"tag": ...}} objects. */ - V1_21_1 + V1_21_1, + /** + * Minecraft 1.20.6: the 1.21.1 layout, except that data folders still have their plural names. + */ + V1_20_6; + + /** The folder under {@code data/} that holds recipes on this target. */ + public String recipeFolder() { + return this == V1_20_6 ? "recipes" : "recipe"; + } } From ee721a29a5a22cc7834d57578fac7913de207246 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:28:59 +0800 Subject: [PATCH 1151/1461] Write recipes to the folder the import target names --- .../java/com/jnhprojects/mcheli/content/LegacyImporter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java b/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java index 2db0f324..1f87a033 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java @@ -53,7 +53,7 @@ public final class LegacyImporter { public static final int FORMAT = 5; /** - * Written at the pack root for {@link ImportTarget#V1_21_1}: each original vehicle's + * Written at the pack root for every target but {@link ImportTarget#MODERN}: each original vehicle's * {@code custom_model_data}, as {@code name=index}, which the mod puts on the vehicle's stack. */ public static final String ICONS_FILE = "item_icons.properties"; @@ -78,7 +78,7 @@ private LegacyImporter(Path packRoot, ImportTarget target) { this.target = target; this.packRoot = packRoot; this.assetsOut = packRoot.resolve("assets/mcheli"); - this.recipesOut = packRoot.resolve("data/mcheli/recipe"); + this.recipesOut = packRoot.resolve("data/mcheli/" + target.recipeFolder()); } /** @@ -117,7 +117,7 @@ private Summary importFrom(Path zipPath) throws IOException { writeTranslations(); ContentRegistry content = ContentRegistry.load(assetsOut); - Map icons = target == ImportTarget.V1_21_1 ? writeKindModels(content) : Map.of(); + Map icons = target != ImportTarget.MODERN ? writeKindModels(content) : Map.of(); LegacyRecipe.Generated recipes = LegacyRecipe.generate(content, target, icons); for (Map.Entry e : recipes.files().entrySet()) { write(recipesOut.resolve(e.getKey()), e.getValue()); From 3f3bdcaf691210c41a92c75b1d352b31bb46b248 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:28:59 +0800 Subject: [PATCH 1152/1461] Treat the 1.20.6 import target like 1.21.1 in generated recipes --- .../com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java b/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java index 9f4169fb..a027206b 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java @@ -199,7 +199,7 @@ private static void appendResult(StringBuilder json, Vehicle result, int count, private static String model(Vehicle result, ImportTarget target) { return switch (target) { case MODERN -> "\"minecraft:item_model\": \"mcheli:" + result.name() + '"'; - case V1_21_1 -> "\"minecraft:custom_model_data\": " + result.icon(); + case V1_21_1, V1_20_6 -> "\"minecraft:custom_model_data\": " + result.icon(); }; } From de3c6f78e581a6f39eb8695b5dfed30a5c611bc2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:00 +0800 Subject: [PATCH 1153/1461] Keep imported recipes out of jars under the plural folder name too --- gradle/loader.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/loader.gradle b/gradle/loader.gradle index f79e9b48..dcd721e6 100644 --- a/gradle/loader.gradle +++ b/gradle/loader.gradle @@ -88,7 +88,7 @@ tasks.named('jar', Jar) { // resource output, which can still have copies from older builds and from the IDE. exclude { FileTreeElement element -> String path = element.relativePath.pathString - !element.directory && (path.startsWith('assets/mcheli/') || path.startsWith('data/mcheli/recipe/')) && + !element.directory && (path.startsWith('assets/mcheli/') || path.startsWith('data/mcheli/recipe/') || path.startsWith('data/mcheli/recipes/')) && !ownResources.contains(path) } includeEmptyDirs = false From c34eba767c367867e233add6d7b7a1172dd44f88 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:01 +0800 Subject: [PATCH 1154/1461] Add ClientConfig.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/ClientConfig.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ClientConfig.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ClientConfig.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ClientConfig.java new file mode 100644 index 00000000..ff258847 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ClientConfig.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Per player look and feel, in {@code config/mcheli-client.toml}. Nothing here changes how the + * game plays, so each player picks for themselves and the server never sees it. + */ +public final class ClientConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue NIGHT_GLOW; + private static final ConfigSpec.IntValue PACK_CACHE_SIZE; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("effects"); + NIGHT_GLOW = builder + .comment("Whether tracers, rocket motors, flares and explosions glow. The glow is faint by day", + "and strongest at night, in caves and under water.") + .define("nightGlow", true); + builder.pop(); + + builder.push("packs"); + PACK_CACHE_SIZE = builder + .comment("How much the content packs downloaded from servers may take up in mcheli/cache, in megabytes.", + "The packs used longest ago are deleted first; the current server's packs are always kept.", + "A single pack larger than this is not downloaded.") + .defineInRange("cacheSize", 256, 16, 16384); + builder.pop(); + + SPEC = builder.build(); + } + + private ClientConfig() { + } + + /** Reads {@code config/mcheli-client.toml}, writing it with the defaults if it is new. */ + public static void load(Path configDir) { + Path file = configDir.resolve(MCHeli.MOD_ID + "-client.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + public static boolean nightGlow() { + return SPEC.isLoaded() ? NIGHT_GLOW.get() : NIGHT_GLOW.getDefault(); + } + + public static long packCacheBytes() { + return (long) (SPEC.isLoaded() ? PACK_CACHE_SIZE.get() : PACK_CACHE_SIZE.getDefault()) << 20; + } +} From 3959b464bca936c6f6dabedd4b6f4fac5a1a8ad6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:01 +0800 Subject: [PATCH 1155/1461] Add CommonHooks.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/CommonHooks.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/CommonHooks.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/CommonHooks.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/CommonHooks.java new file mode 100644 index 00000000..c699c4e8 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/CommonHooks.java @@ -0,0 +1,83 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.network.PackSync; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.repository.RepositorySource; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.level.storage.LevelResource; + +import java.util.List; + +/** + * What the mod does on game events that both sides see. Each loader module calls these from its + * own events; nothing here knows which loader that is. + */ +public final class CommonHooks { + private CommonHooks() { + } + + /** Once the registries are frozen, on the main thread: what vanilla keeps in its own tables. */ + public static void commonSetup() { + DispenserBlock.registerBehavior(ModItems.THROWABLE.get(), new ThrowableItem.DispenseBehavior()); + } + + /** + * Adds the imported original content to the resource packs (client) and data packs (server). + * Without the original zip there is nothing to add. + */ + public static RepositorySource packSource() { + return onLoad -> { + // Fabric builds some pack repositories before our initializer has run. + LegacyContent content = MCHeli.legacyContent(); + if (content != null) { + content.pack().ifPresent(onLoad); + } + }; + } + + /** Before the worlds load: read the world's server config, then load the content packs it allows. */ + public static void serverAboutToStart(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + MCHeli.content().replaceWith(MCHeli.packs().load(true)); + } + + public static void serverStopped() { + ServerConfig.unload(); + } + + public static void serverTickEnd(MinecraftServer server) { + PackSync.pump(server); + } + + public static void registerCommands(CommandDispatcher dispatcher) { + McheliCommand.register(dispatcher); + } + + /** + * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack. Every + * MCHeli recipe goes into a player's recipe book when they join: the drafting table lists them + * from there, and they can be found in the crafting table's book too. + */ + public static void playerLoggedIn(ServerPlayer player) { + PackSync.onLogin(player); + // A remote pilot whose drone did not come back with them is shown and made mortal again. + if (!(player.getVehicle() instanceof AircraftEntity uav && uav.isRemotePilot(player))) { + AircraftEntity.clearRemotePilot(player); + } + List> ours = player.level().getServer().getRecipeManager().getRecipes().stream() + .filter(holder -> holder.id().getNamespace().equals(MCHeli.MOD_ID)) + .toList(); + player.awardRecipes(ours); + } + + public static void playerLoggedOut(ServerPlayer player) { + PackSync.onLogout(player); + } +} From a685a88e1f1638b861abdf924bfc573749c9ec9d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:01 +0800 Subject: [PATCH 1156/1461] Add ContentPacks.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/ContentPacks.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ContentPacks.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ContentPacks.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ContentPacks.java new file mode 100644 index 00000000..d3bfdee5 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ContentPacks.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackFolder; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * The content packs in {@code /mcheli/packs}, loaded together with the imported 1.0.3 + * content, which always comes first. + * + *

At startup every pack in the folder is loaded, so the vehicles exist before any world does. + * When a server starts (single player included) and on {@code /mcheli reload}, the folder is + * scanned again and the server config applied: the allow list, the size limit per pack and the + * limit for all packs together. The packs that pass are what players get sent. + * + *

A zip pack is read in place, so packs stay open while their definitions and assets are in + * use. The set a reload replaces is closed on the reload after that, so a client in the same game + * that is still drawing from it never reads from a closed zip. + */ +public final class ContentPacks { + private static final int PROBLEMS_IN_CHAT = 8; + private static final String SOUNDS = "assets/mcheli/sounds/"; + + private final Path folder; + private final PendingPacks pending; + private final LegacyContent legacy; + private volatile List active = List.of(); + private volatile List problems = List.of(); + private volatile Set sounds = Set.of(); + private List retired = List.of(); + + ContentPacks(Path gameDir, LegacyContent legacy) { + this.folder = gameDir.resolve(MCHeli.MOD_ID).resolve("packs"); + this.pending = new PendingPacks(gameDir.resolve(MCHeli.MOD_ID).resolve("pending")); + this.legacy = legacy; + } + + /** {@code /mcheli/packs}. */ + public Path folder() { + return folder; + } + + /** The packs whose definitions are loaded, in load order. The import is not among them. */ + public List active() { + return active; + } + + /** Everything that went wrong in the last load, packs left out included. */ + public List problems() { + return problems; + } + + /** + * Scans the folder and loads the import and the packs. Never throws; whatever goes wrong is + * logged and kept in {@link #problems()}. + * + * @param serverRules whether to apply the server config; off at startup, when no world exists + * @return the new content, for the caller to swap in + */ + public synchronized ContentRegistry load(boolean serverRules) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not create {}", folder, e); + } + + PackFolder.Scan scan = PackFolder.scan(folder); + List found = new ArrayList<>(scan.problems()); + List accepted = new ArrayList<>(); + long total = 0; + for (ContentPack pack : scan.packs()) { + long size; + String refusal; + try { + size = pack.archiveSize(); + refusal = serverRules ? refusal(pack, size, total) : null; + } catch (IOException e) { + size = 0; + refusal = "left out, could not be read: " + e; + } + if (refusal == null) { + accepted.add(pack); + total += size; + } else { + found.add(new PackProblem(pack.id(), refusal)); + close(List.of(pack)); + } + } + for (PackProblem problem : found) { + MCHeli.LOGGER.warn("Content pack {}", problem); + } + + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, legacy.assets())); + for (ContentPack pack : accepted) { + sources.add(pack.contentSource()); + } + ContentRegistry registry = ContentRegistry.load(sources); + found.addAll(registry.problems()); + + close(retired); + PendingPacks.cleanReplaced(folder); + retired = active; + active = List.copyOf(accepted); + problems = List.copyOf(found); + sounds = soundsOf(accepted); + MCHeli.LOGGER.info("Content packs in use: {}", active.isEmpty() ? "none" : active); + return registry; + } + + /** Sound names as {@code RuntimeAssets.soundFile} finds them: {@code heli.ogg} and {@code heli1.ogg} are both {@code heli}. */ + private static Set soundsOf(List packs) { + Set names = new HashSet<>(); + for (ContentPack pack : packs) { + for (String file : pack.files()) { + if (file.startsWith(SOUNDS) && file.endsWith(".ogg")) { + String name = file.substring(SOUNDS.length(), file.length() - ".ogg".length()); + names.add(name); + if (name.endsWith("1")) { + names.add(name.substring(0, name.length() - 1)); + } + } + } + } + return Set.copyOf(names); + } + + /** Whether a pack in use brought this sound, so that the sound manager has not heard of it. */ + public boolean hasSound(String name) { + return sounds.contains(name); + } + + /** What players have sent this server, waiting for an operator. */ + public PendingPacks pending() { + return pending; + } + + private static @Nullable String refusal(ContentPack pack, long size, long total) { + if (!ServerConfig.isPackAllowed(pack.id())) { + return "left out, not on the server's allow list (packs.allowList in mcheli-server.toml)"; + } + if (size > ServerConfig.maxPackBytes()) { + return "left out, %.1f MB is more than the server allows for one pack (packs.maxPackSize, %d MB)" + .formatted(size / 1048576.0, ServerConfig.maxPackBytes() >> 20); + } + if (total + size > ServerConfig.maxTotalPackBytes()) { + return "left out, it would take the packs past the server's total limit (packs.maxTotalSize, %d MB)" + .formatted(ServerConfig.maxTotalPackBytes() >> 20); + } + return null; + } + + /** Sends the problems of the last load as chat lines, the first few of them. */ + public void reportProblems(Consumer out) { + List list = problems; + for (int i = 0; i < Math.min(list.size(), PROBLEMS_IN_CHAT); i++) { + out.accept(Component.translatable("message.mcheli.pack.problem", list.get(i).pack(), list.get(i).message())); + } + if (list.size() > PROBLEMS_IN_CHAT) { + out.accept(Component.translatable("message.mcheli.pack.more_problems", list.size() - PROBLEMS_IN_CHAT)); + } + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } +} From 589b58829754297b4f6cef6cd344208f0b1c28ca Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:02 +0800 Subject: [PATCH 1157/1461] Add ContentReload.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/ContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ContentReload.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ContentReload.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ContentReload.java new file mode 100644 index 00000000..7846b7bf --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.network.PackSync; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +/** + * Reads the vehicle and weapon definitions from disk again while the game runs, for + * {@code /mcheli reload}: the server config, then the imported content and the content packs. + * Vehicles in the world pick up their new definition on their next tick (see + * {@code AircraftEntity#definition}). Every player gets the new pack list, downloads what changed + * and rebuilds their content the same way. + */ +public final class ContentReload { + private ContentReload() { + } + + /** @return the newly loaded content, already in use */ + public static ContentRegistry reload(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + ContentRegistry fresh = MCHeli.packs().load(true); + MCHeli.content().replaceWith(fresh); + MCHeli.LOGGER.info("Content reloaded: {} vehicles, {} weapons", fresh.all().size(), fresh.weapons().size()); + PackSync.sendListToAll(server); + return fresh; + } +} From b1b5f057790160eddb6215766fc51f33827085ec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:02 +0800 Subject: [PATCH 1158/1461] Import the original content in the 1.20.6 layout --- .../com/jnhprojects/mcheli/LegacyContent.java | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/LegacyContent.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/LegacyContent.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/LegacyContent.java new file mode 100644 index 00000000..ec74f04a --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/LegacyContent.java @@ -0,0 +1,275 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ImportTarget; +import com.jnhprojects.mcheli.content.LegacyImporter; +import net.minecraft.network.chat.Component; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PathPackResources; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackCompatibility; +import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.world.flag.FeatureFlagSet; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Properties; +import java.util.stream.Stream; + +/** + * The original vehicles, models, textures and sounds, imported from the player's own copy of + * MCHeli 1.0.3. + * + *

That content is EMB4's and is not redistributed with this mod. Players download the original + * zip from the MCHeli CurseForge page and put it in {@code /mcheli}. On start the zip is + * converted once by {@link LegacyImporter} into {@code mcheli/imported}, which then serves as a + * required resource pack and data pack. The zip is converted again only when it changes or the + * importer's output format does; once imported it may be deleted. + * + *

Development runs point the {@code mcheli.legacyZip} system property at the zip instead. + */ +public final class LegacyContent { + /** The file name the original download has. Any zip with the same layout works. */ + public static final String ZIP_NAME = "MC1.7.10_mcheli_1.0.3.zip"; + + private static final String PACK_ID = "mcheli_content"; + private static final String MARKER = "import.properties"; + /** The pack layout this Minecraft version reads. */ + private static final ImportTarget TARGET = ImportTarget.V1_20_6; + + private final Path folder; + private final Path packRoot; + private @Nullable Component problem; + private volatile @Nullable Map icons; + + private LegacyContent(Path folder) { + this.folder = folder; + this.packRoot = folder.resolve("imported"); + } + + /** Imports the zip if needed. Never throws; a missing or broken zip leaves the mod without content. */ + static LegacyContent prepare(Path gameDir) { + LegacyContent content = new LegacyContent(gameDir.resolve(MCHeli.MOD_ID)); + try { + content.importIfNeeded(); + } catch (IOException | UncheckedIOException e) { + MCHeli.LOGGER.error("Could not import the original MCHeli content", e); + content.problem = Component.translatable("message.mcheli.content.import_failed", e.getMessage()); + } + return content; + } + + /** The imported {@code assets/mcheli} folder. It does not exist when nothing was imported. */ + public Path assets() { + return packRoot.resolve("assets").resolve(MCHeli.MOD_ID); + } + + /** + * The {@code custom_model_data} that shows this original vehicle's icon, from the list the + * import wrote ({@link LegacyImporter#ICONS_FILE}); empty for a vehicle it did not import. + */ + public OptionalInt iconIndex(String vehicle) { + Map map = icons; + if (map == null) { + map = readIcons(); + icons = map; + } + Integer index = map.get(vehicle); + return index != null ? OptionalInt.of(index) : OptionalInt.empty(); + } + + private Map readIcons() { + Properties list = new Properties(); + try (InputStream in = Files.newInputStream(packRoot.resolve(LegacyImporter.ICONS_FILE))) { + list.load(in); + } catch (IOException e) { + return Map.of(); + } + Map map = new HashMap<>(); + for (String name : list.stringPropertyNames()) { + try { + map.put(name, Integer.parseInt(list.getProperty(name).trim())); + } catch (NumberFormatException ignored) { + // A hand edited list; that vehicle shows its kind's model. + } + } + return Map.copyOf(map); + } + + /** Why the vehicles are missing, for the player; empty when the content is in place. */ + public Optional problem() { + return Optional.ofNullable(problem); + } + + /** + * The imported folder as a pack, for both the client resource packs and the server data packs. + * Empty when nothing was imported. + */ + public Optional pack() { + if (!Files.isDirectory(packRoot)) { + return Optional.empty(); + } + PackLocationInfo location = new PackLocationInfo(PACK_ID, + Component.translatable("pack.mcheli.content"), PackSource.BUILT_IN, Optional.empty()); + Pack.Metadata metadata = new Pack.Metadata(Component.translatable("pack.mcheli.content.description"), + PackCompatibility.COMPATIBLE, FeatureFlagSet.of(), List.of()); + // Required, so it is enabled in every world and cannot be switched off. It sits at the + // bottom so that regular resource packs can still replace its textures and models. + return Optional.of(new Pack(location, new PathPackResources.PathResourcesSupplier(packRoot), metadata, + new PackSelectionConfig(true, Pack.Position.BOTTOM, false))); + } + + private void importIfNeeded() throws IOException { + Path zip = findZip(); + if (zip == null) { + if (Files.isDirectory(packRoot)) { + MCHeli.LOGGER.info("Using the MCHeli content imported earlier into {}", packRoot); + } else { + MCHeli.LOGGER.error("MCHeli content not found. Download {} from the MCHeli CurseForge page and put it in {}", + ZIP_NAME, folder.toAbsolutePath()); + problem = Component.translatable("message.mcheli.content.missing", ZIP_NAME, folder.toAbsolutePath().toString()); + } + return; + } + + Properties source = describe(zip); + if (source.equals(readMarker())) { + return; + } + + MCHeli.LOGGER.info("Importing MCHeli content from {}", zip); + Path staging = folder.resolve("imported.tmp"); + deleteTree(staging); + LegacyImporter.Summary summary = LegacyImporter.run(zip, staging, TARGET); + try (OutputStream out = Files.newOutputStream(staging.resolve(MARKER))) { + source.store(out, "Written by MCHeli Rework. Delete this folder to import again."); + } + swapIn(staging); + + MCHeli.LOGGER.info("Imported {} files, {} sounds, {} item models and {} recipes into {}", + summary.files(), summary.sounds(), summary.itemModels(), summary.recipes(), packRoot); + for (String skipped : summary.skippedRecipes()) { + MCHeli.LOGGER.info("Recipe left out: {}", skipped); + } + } + + /** + * Replaces the previous import with the fresh one in {@code staging}. + * + *

On Windows a folder that was just deleted keeps its name for a moment while another + * program (a file watcher, a virus scanner) still has a handle in it, and renaming onto that + * name fails with "access denied". So the old import is moved out of the way rather than + * deleted, the renames are retried for a few seconds, and the old copy is deleted last, on a + * best effort basis; a copy left behind is removed on the next import. + */ + private void swapIn(Path staging) throws IOException { + Path old = folder.resolve("imported.old"); + deleteQuietly(old); + if (Files.exists(packRoot)) { + try { + moveWithRetry(packRoot, old); + } catch (IOException e) { + deleteTree(packRoot); + } + } + moveWithRetry(staging, packRoot); + deleteQuietly(old); + } + + private static void moveWithRetry(Path from, Path to) throws IOException { + for (int attempt = 1; ; attempt++) { + try { + Files.move(from, to); + return; + } catch (FileSystemException e) { + if (attempt >= 15) { + throw e; + } + try { + Thread.sleep(50L * attempt); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw e; + } + } + } + } + + private static void deleteQuietly(Path root) { + try { + deleteTree(root); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not delete {}; it is removed on the next import", root, e); + } + } + + private @Nullable Path findZip() throws IOException { + String configured = System.getProperty("mcheli.legacyZip"); + if (configured != null && !configured.isBlank()) { + Path zip = Path.of(configured); + if (Files.isRegularFile(zip)) { + return zip; + } + MCHeli.LOGGER.warn("mcheli.legacyZip points to {}, which does not exist", zip); + } + + Files.createDirectories(folder); + if (Files.isRegularFile(folder.resolve(ZIP_NAME))) { + return folder.resolve(ZIP_NAME); + } + // Browsers rename repeated downloads, so take any zip, the same way every start. + try (Stream files = Files.list(folder)) { + return files.filter(p -> p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) + .filter(Files::isRegularFile) + .sorted() + .findFirst() + .orElse(null); + } + } + + /** What the current import was made from. Comparing size and time is enough to notice a new zip. */ + private static Properties describe(Path zip) throws IOException { + Properties p = new Properties(); + p.setProperty("format", Integer.toString(LegacyImporter.FORMAT)); + p.setProperty("target", TARGET.name()); + p.setProperty("source", zip.toAbsolutePath().normalize().toString()); + p.setProperty("size", Long.toString(Files.size(zip))); + p.setProperty("modified", Long.toString(Files.getLastModifiedTime(zip).toMillis())); + return p; + } + + private Properties readMarker() throws IOException { + Properties p = new Properties(); + Path marker = packRoot.resolve(MARKER); + if (Files.isRegularFile(marker)) { + try (InputStream in = Files.newInputStream(marker)) { + p.load(in); + } + } + return p; + } + + private static void deleteTree(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + try (Stream paths = Files.walk(root)) { + for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) { + Files.delete(p); + } + } + } +} From dd2f6ff809ff4d65f2bd213e855a122c3e28b7db Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:03 +0800 Subject: [PATCH 1159/1461] Add MCHeli.java to the 1.20.6 shared sources --- .../java/com/jnhprojects/mcheli/MCHeli.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/MCHeli.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/MCHeli.java new file mode 100644 index 00000000..1a890651 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/MCHeli.java @@ -0,0 +1,71 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.registry.ModParticles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The mod, minus the loader. Each loader module's entry point installs its {@link Platform}, calls + * {@link #init()}, registers what {@code Registration} collected and wires its events to + * {@link CommonHooks} (and, on a client, {@code ClientHooks}). + */ +public final class MCHeli { + public static final String MOD_ID = "mcheli"; + public static final Logger LOGGER = LoggerFactory.getLogger(MCHeli.class); + + private static LegacyContent legacyContent; + private static ContentPacks packs; + private static ContentRegistry content; + + private MCHeli() { + } + + /** + * Imports the original content if needed, loads the definitions and declares the registry + * entries. The vehicle list must be known here: 1.0.0 had an item per vehicle, and those items + * are still registered so old worlds keep them. + */ + public static void init() { + Platform platform = Platform.get(); + LOGGER.info("MCHeli Rework {} on {}", platform.modVersion(), platform.loaderName()); + legacyContent = LegacyContent.prepare(platform.gameDir()); + packs = new ContentPacks(platform.gameDir(), legacyContent); + content = packs.load(false); + ModBlocks.init(); + ModBlockEntities.init(); + ModDataComponents.init(); + ModEntities.init(); + ModParticles.init(); + ModMenus.init(); + ModItems.init(content); + ModCreativeTabs.init(); + } + + /** + * Vehicle and weapon definitions, from the imported original content and the content packs. + * Available from mod construction onwards. Always the same object; a reload changes what it + * holds. + */ + public static ContentRegistry content() { + return content; + } + + /** Where the original vehicle content came from, and why it is missing if it is. */ + public static LegacyContent legacyContent() { + return legacyContent; + } + + /** The content packs in {@code mcheli/packs} and the ones in use. */ + public static ContentPacks packs() { + return packs; + } +} From bb1bbbf0390db29e2563c1bf52e6143576979e78 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:03 +0800 Subject: [PATCH 1160/1461] Add McheliCommand.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/McheliCommand.java | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/McheliCommand.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/McheliCommand.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/McheliCommand.java new file mode 100644 index 00000000..1384fcc4 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/McheliCommand.java @@ -0,0 +1,262 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.Definition; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.network.PackSubmissions; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * {@code /mcheli}: what content is loaded, and operator tools to hand out vehicles and reload the + * definitions and content packs. + *

+ *   /mcheli                              content source and counts
+ *   /mcheli list [helicopter|plane|...]  loaded vehicles
+ *   /mcheli packs                        content packs in use, and their problems
+ *   /mcheli give <players> <vehicle> [count]   (operators)
+ *   /mcheli reload                       re-read the definitions and packs (operators)
+ *   /mcheli pending                      packs players have sent, waiting (operators)
+ *   /mcheli approve <pack>               put a waiting pack in use (operators)
+ *   /mcheli reject <pack>                throw a waiting pack away (operators)
+ * 
+ */ +public final class McheliCommand { + private static final List KINDS = List.of("helicopter", "plane", "tank", "vehicle"); + private static final DynamicCommandExceptionType UNKNOWN_VEHICLE = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.unknown_vehicle", name)); + private static final DynamicCommandExceptionType UNKNOWN_PENDING = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.pending.unknown", name)); + + private McheliCommand() { + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal(MCHeli.MOD_ID) + .executes(McheliCommand::status) + .then(Commands.literal("list") + .executes(ctx -> list(ctx, null)) + .then(Commands.argument("kind", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest(KINDS, builder)) + .executes(ctx -> list(ctx, StringArgumentType.getString(ctx, "kind"))))) + .then(Commands.literal("packs") + .executes(McheliCommand::packs)) + .then(Commands.literal("give") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("targets", EntityArgument.players()) + .then(Commands.argument("vehicle", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest( + MCHeli.content().all().stream().map(AircraftDefinition::name), builder)) + .executes(ctx -> give(ctx, 1)) + .then(Commands.argument("count", IntegerArgumentType.integer(1, 64)) + .executes(ctx -> give(ctx, IntegerArgumentType.getInteger(ctx, "count"))))))) + .then(Commands.literal("reload") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::reload)) + .then(Commands.literal("pending") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::pending)) + .then(Commands.literal("approve") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::approve))) + .then(Commands.literal("reject") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("pack", StringArgumentType.word()) + .suggests(McheliCommand::suggestPending) + .executes(McheliCommand::reject)))); + } + + private static CompletableFuture suggestPending(CommandContext ctx, SuggestionsBuilder builder) { + try { + return SharedSuggestionProvider.suggest(MCHeli.packs().pending().list().stream().map(PendingPacks.Submission::id), builder); + } catch (IOException e) { + return builder.buildFuture(); + } + } + + private static int pending(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List waiting; + try { + waiting = MCHeli.packs().pending().list(); + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + if (waiting.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.none"), false); + } + for (PendingPacks.Submission submission : waiting) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.entry", submission.id(), submission.manifest().version(), + submission.manifest().author(), submission.submitter(), "%.1f".formatted(submission.size() / 1048576.0), + String.join(", ", submission.definitions())), false); + List replaced = replaced(submission); + if (!replaced.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.pending.replaces", String.join(", ", replaced)), false); + } + } + return waiting.size(); + } + + /** + * What going live would take the place of: a pack with the same id, and vehicles or weapons of + * another pack with the same file name. That is allowed, so it is the operator's call to make, + * but it should not come as a surprise. + */ + private static List replaced(PendingPacks.Submission submission) { + List names = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.id().equals(submission.id())) { + names.add("pack " + pack.id()); + } + } + ContentRegistry content = MCHeli.content(); + for (String path : submission.definitions()) { + String name = path.substring(path.lastIndexOf('/') + 1, path.length() - ".txt".length()); + Definition existing = path.startsWith("weapons/") ? content.weapon(name) : content.any(name); + if (existing != null && !existing.pack().equals(submission.id())) { + names.add(name + " (" + existing.pack() + ")"); + } + } + return names; + } + + private static PendingPacks.Submission waiting(CommandContext ctx) throws CommandSyntaxException { + String id = StringArgumentType.getString(ctx, "pack"); + PendingPacks.Submission submission; + try { + submission = MCHeli.packs().pending().find(id); + } catch (IOException e) { + submission = null; + } + if (submission == null) { + throw UNKNOWN_PENDING.create(id); + } + return submission; + } + + private static int approve(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + if (PackSubmissions.install(source.getServer(), submission)) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.approve.success", submission.id(), submission.submitter()), true); + return 1; + } + source.sendFailure(Component.translatable("commands.mcheli.approve.left_out", submission.id())); + MCHeli.packs().reportProblems(source::sendFailure); + } catch (IOException | PackException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.getMessage())); + } + return 0; + } + + private static int reject(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + PendingPacks.Submission submission = waiting(ctx); + try { + PackSubmissions.reject(source.getServer(), submission); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reject.success", submission.id(), submission.submitter()), true); + return 1; + } catch (IOException e) { + source.sendFailure(Component.translatable("commands.mcheli.pending.failed", e.toString())); + return 0; + } + } + + private static int status(CommandContext ctx) { + ContentRegistry content = MCHeli.content(); + CommandSourceStack source = ctx.getSource(); + MCHeli.legacyContent().problem().ifPresent(source::sendFailure); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status", + content.helicopters().size(), content.planes().size(), content.tanks().size(), content.vehicles().size(), + content.weapons().size(), MCHeli.legacyContent().assets().toString()), false); + List packs = MCHeli.packs().active(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status.packs", packs.size(), + packs.stream().map(ContentPack::id).collect(Collectors.joining(", "))), false); + return content.all().size(); + } + + private static int packs(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List packs = MCHeli.packs().active(); + if (packs.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.none", MCHeli.packs().folder().toString()), false); + } else { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.header", packs.size(), MCHeli.packs().folder().toString()), false); + } + ContentRegistry content = MCHeli.content(); + for (ContentPack pack : packs) { + long vehicles = content.all().stream().filter(def -> def.pack().equals(pack.id())).count(); + long weapons = content.weapons().stream().filter(def -> def.pack().equals(pack.id())).count(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.entry", pack.id(), pack.manifest().version(), + pack.manifest().name(), vehicles, weapons, ContentPack.shortHash(pack.hash())), false); + if (!pack.manifest().author().isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.author", pack.manifest().author()), false); + } + } + MCHeli.packs().reportProblems(source::sendFailure); + return packs.size(); + } + + private static int list(CommandContext ctx, String kind) { + List defs = MCHeli.content().all().stream() + .filter(def -> kind == null || def.kind().equals(kind)) + .toList(); + String names = defs.stream().map(AircraftDefinition::name).collect(Collectors.joining(", ")); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.list", defs.size(), names), false); + return defs.size(); + } + + private static int give(CommandContext ctx, int count) throws CommandSyntaxException { + String name = StringArgumentType.getString(ctx, "vehicle"); + AircraftDefinition def = MCHeli.content().any(name); + if (def == null) { + throw UNKNOWN_VEHICLE.create(name); + } + Collection targets = EntityArgument.getPlayers(ctx, "targets"); + for (ServerPlayer player : targets) { + // Vehicle items do not stack, so each one is a separate stack. + for (int i = 0; i < count; i++) { + player.getInventory().placeItemBackInInventory(AircraftItem.stackOf(def)); + } + } + Component vehicle = AircraftItem.stackOf(def).getHoverName(); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.give", count, vehicle, targets.size()), true); + return targets.size(); + } + + private static int reload(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + ContentRegistry fresh = ContentReload.reload(source.getServer()); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reload.success", + fresh.all().size(), fresh.weapons().size(), MCHeli.packs().active().size()), true); + MCHeli.packs().reportProblems(source::sendFailure); + return fresh.all().size(); + } +} From b57da1893d9004839d846c2fe1b6c66f7deb9071 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:04 +0800 Subject: [PATCH 1161/1461] Add ModSounds.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/ModSounds.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ModSounds.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ModSounds.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ModSounds.java new file mode 100644 index 00000000..f0536646 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ModSounds.java @@ -0,0 +1,64 @@ +package com.jnhprojects.mcheli; + +import net.minecraft.core.Holder; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Sound events from {@code assets/mcheli/sounds.json}. + * + *

The events are not put in the registry. Content files name their sounds freely and a content + * pack may bring its own, so they are created on demand as direct holders, which the sound packet + * and the sound manager both accept. + * + *

A sound a pack brought is in no {@code sounds.json}, so the sound manager would drop it as + * unknown. For those the server sends our own packet, and the client streams the file. + */ +public final class ModSounds { + /** How far a sound carries at a volume of one, and per volume above that, as in vanilla. */ + private static final float RANGE = 16.0F; + + private static final Map EVENTS = new ConcurrentHashMap<>(); + + private ModSounds() { + } + + public static SoundEvent get(String name) { + return EVENTS.computeIfAbsent(name, n -> SoundEvent.createVariableRangeEvent(new ResourceLocation(MCHeli.MOD_ID, n))); + } + + /** Plays {@code name} at the entity for everyone nearby. Call on the server. */ + public static void playAt(Entity entity, String name, float volume, float pitch) { + playAt(entity.level(), entity.getX(), entity.getY(), entity.getZ(), name, volume, pitch); + } + + /** Plays {@code name} at a position for everyone nearby. Call on the server. */ + public static void playAt(Level level, double x, double y, double z, String name, float volume, float pitch) { + if (level instanceof ServerLevel server && MCHeli.packs().hasSound(name)) { + ModNetwork.sendNear(server, x, y, z, RANGE * Math.max(volume, 1.0F), new PackSoundPacket(name, x, y, z, volume, pitch)); + } else { + level.playSound(null, x, y, z, get(name), SoundSource.NEUTRAL, volume, pitch); + } + } + + /** Plays {@code name} for one player only, such as a hit confirmation. */ + public static void playTo(ServerPlayer player, String name, float volume, float pitch) { + if (MCHeli.packs().hasSound(name)) { + ModNetwork.sendToPlayer(player, new PackSoundPacket(name, player.getX(), player.getY(), player.getZ(), volume, pitch)); + } else { + player.connection.send(new ClientboundSoundPacket(Holder.direct(get(name)), SoundSource.PLAYERS, + player.getX(), player.getY(), player.getZ(), volume, pitch, player.getRandom().nextLong())); + } + } +} From bf91495cfa5caa76bc9ef1dc9d5ea6bd1591ff0e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:04 +0800 Subject: [PATCH 1162/1461] Add ServerConfig.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/ServerConfig.java | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ServerConfig.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ServerConfig.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ServerConfig.java new file mode 100644 index 00000000..9db31738 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/ServerConfig.java @@ -0,0 +1,172 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Server rules, the part of the original {@code mcheli.cfg} that decides how the game plays. + * The file lives per world in {@code serverconfig/mcheli-server.toml}, where Forge kept it, and is + * never sent to clients (only the server applies these rules). It is read when the world starts + * and again on {@code /mcheli reload}. + * + *

The values only exist while a world is loaded. Before that, and on clients, the getters return + * the defaults, which match 1.0.0. + */ +public final class ServerConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue EXPLOSIONS_BREAK_BLOCKS; + private static final ConfigSpec.BooleanValue EXPLOSIONS_START_FIRES; + private static final ConfigSpec.DoubleValue VEHICLE_DAMAGE_FACTOR; + private static final ConfigSpec.BooleanValue INFINITE_AMMO; + private static final ConfigSpec.BooleanValue INFINITE_FUEL; + private static final ConfigSpec.StringListValue DISABLED_VEHICLES; + private static final ConfigSpec.StringListValue PACK_ALLOW_LIST; + private static final ConfigSpec.IntValue MAX_PACK_SIZE; + private static final ConfigSpec.IntValue MAX_TOTAL_SIZE; + private static final ConfigSpec.BooleanValue ALLOW_SUBMISSIONS; + private static final ConfigSpec.BooleanValue REQUIRE_APPROVAL; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("explosions"); + EXPLOSIONS_BREAK_BLOCKS = builder + .comment("Whether weapons and crashing vehicles break blocks. The mobGriefing game rule also applies.", + "Original: Explosion_DestroyBlock") + .define("breakBlocks", true); + EXPLOSIONS_START_FIRES = builder + .comment("Whether incendiary weapons and crashes set blocks on fire. Original: Explosion_FlamingBlock") + .define("startFires", true); + builder.pop(); + + builder.push("vehicles"); + VEHICLE_DAMAGE_FACTOR = builder + .comment("Multiplies the damage every vehicle takes, after armour. 0.5 makes them twice as tough.") + .defineInRange("damageFactor", 1.0, 0.0, 100.0); + DISABLED_VEHICLES = builder + .comment("Vehicles that cannot be placed, by definition name (the file name, such as \"ah-64\").", + "Vehicles already in the world stay.") + .defineList("disabled", List.of()); + INFINITE_FUEL = builder + .comment("Whether vehicles never run out of fuel. Original: InfinityFuel") + .define("infiniteFuel", false); + builder.pop(); + + builder.push("weapons"); + INFINITE_AMMO = builder + .comment("Whether vehicle weapons never run out of ammunition. Original: InfinityAmmo") + .define("infiniteAmmo", false); + builder.pop(); + + builder.comment("Content packs from /mcheli/packs. Players get the packs from the server when they join.", + "Changes apply when the world starts or after /mcheli reload.") + .push("packs"); + PACK_ALLOW_LIST = builder + .comment("Pack ids the server loads, such as \"rework-example\". Empty loads every pack in the folder.") + .defineList("allowList", List.of()); + MAX_PACK_SIZE = builder + .comment("Largest pack the server loads and sends to players, in megabytes.") + .defineInRange("maxPackSize", 8, 1, 512); + MAX_TOTAL_SIZE = builder + .comment("Most the server loads and sends in all, in megabytes. Packs are taken in id order until", + "the next one would go over.") + .defineInRange("maxTotalSize", 64, 1, 4096); + ALLOW_SUBMISSIONS = builder + .comment("Whether players may send this server packs they made in the workshop. They wait in mcheli/pending", + "until an operator runs /mcheli approve. Off, players can only get packs from the server's owner.") + .define("allowSubmissions", false); + REQUIRE_APPROVAL = builder + .comment("Whether a submitted pack waits for an operator. Off, any player can add vehicles and weapons for", + "everyone, and even replace existing ones, so only turn it off among people you trust. A pack taking", + "the id of a pack already in use always waits.") + .define("requireApproval", true); + builder.pop(); + + SPEC = builder.build(); + } + + private ServerConfig() { + } + + /** Reads the world's file, writing it with the defaults if it is new. */ + public static void load(Path worldDir) { + Path file = worldDir.resolve("serverconfig").resolve(MCHeli.MOD_ID + "-server.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + /** The world closed: back to the defaults. */ + public static void unload() { + SPEC.unload(); + } + + public static boolean explosionsBreakBlocks() { + return SPEC.isLoaded() ? EXPLOSIONS_BREAK_BLOCKS.get() : EXPLOSIONS_BREAK_BLOCKS.getDefault(); + } + + public static boolean explosionsStartFires() { + return SPEC.isLoaded() ? EXPLOSIONS_START_FIRES.get() : EXPLOSIONS_START_FIRES.getDefault(); + } + + public static float vehicleDamageFactor() { + double factor = SPEC.isLoaded() ? VEHICLE_DAMAGE_FACTOR.get() : VEHICLE_DAMAGE_FACTOR.getDefault(); + return (float) factor; + } + + public static boolean infiniteAmmo() { + return SPEC.isLoaded() ? INFINITE_AMMO.get() : INFINITE_AMMO.getDefault(); + } + + public static boolean infiniteFuel() { + return SPEC.isLoaded() ? INFINITE_FUEL.get() : INFINITE_FUEL.getDefault(); + } + + /** Whether the allow list lets the server load this pack. An empty list allows every pack. */ + public static boolean isPackAllowed(String id) { + if (!SPEC.isLoaded() || PACK_ALLOW_LIST.get().isEmpty()) { + return true; + } + for (String allowed : PACK_ALLOW_LIST.get()) { + if (allowed.trim().toLowerCase(Locale.ROOT).equals(id)) { + return true; + } + } + return false; + } + + public static long maxPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_PACK_SIZE.get() : MAX_PACK_SIZE.getDefault()) << 20; + } + + public static long maxTotalPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_TOTAL_SIZE.get() : MAX_TOTAL_SIZE.getDefault()) << 20; + } + + public static boolean allowSubmissions() { + return SPEC.isLoaded() ? ALLOW_SUBMISSIONS.get() : ALLOW_SUBMISSIONS.getDefault(); + } + + public static boolean requireApproval() { + return SPEC.isLoaded() ? REQUIRE_APPROVAL.get() : REQUIRE_APPROVAL.getDefault(); + } + + public static boolean isDisabled(String vehicle) { + if (!SPEC.isLoaded()) { + return false; + } + for (String name : DISABLED_VEHICLES.get()) { + if (name.trim().toLowerCase(Locale.ROOT).equals(vehicle)) { + return true; + } + } + return false; + } +} From d7cbcd771bdce6fa56c8e2716901250be8188ae7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:05 +0800 Subject: [PATCH 1163/1461] Add block/DraftingTableBlock.java to the 1.20.6 shared sources --- .../mcheli/block/DraftingTableBlock.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java new file mode 100644 index 00000000..7c4dbab5 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlock.java @@ -0,0 +1,100 @@ +package com.jnhprojects.mcheli.block; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +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.block.state.properties.IntegerProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jspecify.annotations.Nullable; + +/** + * The drafting table, {@code MCH_DraftingTableBlock}: right-click opens the recipe browser, and + * Shift and right-click switches its lamp. It turns in eighths of a circle to face whoever placed + * it, and its block entity draws the original model. + */ +public class DraftingTableBlock extends BaseEntityBlock { + public static final IntegerProperty ROTATION = IntegerProperty.create("rotation", 0, 7); + public static final BooleanProperty LIT = BooleanProperty.create("lit"); + private static final VoxelShape SHAPE = Block.box(0.0, 0.0, 0.0, 16.0, 15.0, 16.0); + private static final MapCodec CODEC = simpleCodec(DraftingTableBlock::new); + private static final Component TITLE = Component.translatable("block.mcheli.drafting_table"); + + public DraftingTableBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(ROTATION, 0).setValue(LIT, false)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ROTATION, LIT); + } + + /** {@code onBlockPlacedBy}: the nearest eighth of the placer's heading. */ + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + float yaw = context.getPlayer() != null ? context.getPlayer().getYRot() : 0.0F; + int rotation = (int) ((Mth.positiveModulo(yaw, 360.0F) + 22.5F) / 45.0F) % 8; + return defaultBlockState().setValue(ROTATION, rotation); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive()) { + level.setBlock(pos, state.cycle(LIT), Block.UPDATE_ALL); + level.playSound(null, pos, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.BLOCKS, 0.3F, 0.5F); + } else { + player.openMenu(state.getMenuProvider(level, pos)); + } + return InteractionResult.SUCCESS; + } + + @Override + protected @Nullable MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { + return new SimpleMenuProvider((id, inventory, player) -> new DraftingTableMenu(id, inventory, ContainerLevelAccess.create(level, pos)), TITLE); + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + /** The block entity renderer draws the table. */ + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new DraftingTableBlockEntity(pos, state); + } +} From 9d8afc8fcd13e1806179e8b48e7b955828eb9642 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:05 +0800 Subject: [PATCH 1164/1461] Add block/DraftingTableBlockEntity.java to the 1.20.6 shared sources --- .../mcheli/block/DraftingTableBlockEntity.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java new file mode 100644 index 00000000..56238f3f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/block/DraftingTableBlockEntity.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.block; + +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** {@code MCH_DraftingTableTileEntity}: holds nothing; it is there so the table's model can be drawn. */ +public class DraftingTableBlockEntity extends BlockEntity { + public DraftingTableBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.DRAFTING_TABLE.get(), pos, state); + } +} From 1fe311561933a37728bdc7b384727c0040168da6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:05 +0800 Subject: [PATCH 1165/1461] Add client/CameraAngles.java to the 1.20.6 shared sources --- .../mcheli/client/CameraAngles.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java new file mode 100644 index 00000000..ab62e120 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.client; + +import net.minecraft.client.Camera; + +/** + * The camera's angles for this frame, after vanilla placed the camera and before it renders, as + * the loader module hands them to {@link ClientHooks#computeCameraAngles}. Forge and NeoForge + * back this with their viewport event; Fabric with a mixin at the same spot. + */ +public interface CameraAngles { + Camera camera(); + + float partialTick(); + + float yaw(); + + float pitch(); + + float roll(); + + void setYaw(float yaw); + + void setPitch(float pitch); + + void setRoll(float roll); +} From 98f4a411ad8ad398bcbd819f9e56c6bd17e2398d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:06 +0800 Subject: [PATCH 1166/1461] Add client/ClientContentReload.java to the 1.20.6 shared sources --- .../mcheli/client/ClientContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java new file mode 100644 index 00000000..70bcc23e --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.CreativeModeTab; + +/** + * Makes the client show the content it now holds: after the server's pack list was applied, which + * also happens after {@code /mcheli reload}. + */ +final class ClientContentReload { + private ClientContentReload() { + } + + /** Drops cached models and textures so edited and new files load, and rebuilds the creative tabs. */ + static void refresh() { + Minecraft mc = Minecraft.getInstance(); + MeshCache.INSTANCE.clear(); + RuntimeAssets.refreshTextures(); + if (mc.level != null && mc.player != null) { + boolean operator = mc.options.operatorItemsTab().get() && mc.player.canUseGameMasterBlocks(); + CreativeModeTab.ItemDisplayParameters params = + new CreativeModeTab.ItemDisplayParameters(mc.level.enabledFeatures(), operator, mc.level.registryAccess()); + ModCreativeTabs.all().forEach(tab -> tab.buildContents(params)); + } + } +} From 6a1896bcf5f2f30ed468326959e9a0dbdc48870d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:06 +0800 Subject: [PATCH 1167/1461] Add client/ClientHooks.java to the 1.20.6 shared sources --- .../mcheli/client/ClientHooks.java | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java new file mode 100644 index 00000000..3a0dafca --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -0,0 +1,316 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.client.hud.HudScripts; +import com.jnhprojects.mcheli.client.render.A10Renderer; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import com.jnhprojects.mcheli.client.render.ChainRenderer; +import com.jnhprojects.mcheli.client.render.DraftingTableRenderer; +import com.jnhprojects.mcheli.client.render.EquipmentRenderer; +import com.jnhprojects.mcheli.client.render.FlareRenderer; +import com.jnhprojects.mcheli.client.render.GltdRenderer; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.render.ParachuteRenderer; +import com.jnhprojects.mcheli.client.render.ProjectileRenderer; +import com.jnhprojects.mcheli.client.render.RiderPose; +import com.jnhprojects.mcheli.client.render.ThrowableRenderer; +import com.jnhprojects.mcheli.client.render.UavStationRenderer; +import com.jnhprojects.mcheli.client.screen.DraftingTableScreen; +import com.jnhprojects.mcheli.client.screen.TeamManagerScreen; +import com.jnhprojects.mcheli.client.screen.UavStationScreen; +import com.jnhprojects.mcheli.client.screen.VehicleScreen; +import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; +import com.jnhprojects.mcheli.client.workshop.WorkshopClient; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModBlockEntities; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.team.Markers; +import com.jnhprojects.mcheli.team.TeamManager; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.NoopRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import java.util.List; +import java.util.Map; + +/** + * Client side counterpart of {@code CommonHooks}: what the mod does on client events. The loader + * module calls these from its own events. Kept out of the shared entry point so dedicated servers + * never load these classes. + */ +public final class ClientHooks { + /** The weapon HUD's layer, drawn above the vanilla HUD. */ + public static final ResourceLocation HUD_LAYER = new ResourceLocation(MCHeli.MOD_ID, "weapons"); + + private ClientHooks() { + } + + /** Loads a post effect into the game renderer, whose own method is private on some loaders. */ + public interface PostEffects { + void load(ResourceLocation effect); + } + + private static PostEffects postEffects = effect -> { + }; + + static PostEffects postEffects() { + return postEffects; + } + + /** During mod construction, on a client only. */ + public static void init(PostEffects effects) { + postEffects = effects; + ClientConfig.load(Platform.get().configDir()); + ClientAccess.install(() -> Minecraft.getInstance().player, ClientPacks.INSTANCE); + } + + /** How a loader registers an entity renderer. */ + public interface EntityRenderers { + void register(EntityType type, EntityRendererProvider provider); + } + + public static void registerEntityRenderers(EntityRenderers renderers) { + renderers.register(ModEntities.HELICOPTER.get(), AircraftRenderer::new); + renderers.register(ModEntities.PLANE.get(), AircraftRenderer::new); + renderers.register(ModEntities.TANK.get(), AircraftRenderer::new); + renderers.register(ModEntities.VEHICLE.get(), AircraftRenderer::new); + renderers.register(ModEntities.BULLET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.BOMB.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AA_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AS_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AT_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TV_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TORPEDO.get(), ProjectileRenderer::new); + renderers.register(ModEntities.MARKER_ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.DISPENSED_ITEM.get(), ProjectileRenderer::new); + renderers.register(ModEntities.FLARE.get(), FlareRenderer::new); + renderers.register(ModEntities.A10.get(), A10Renderer::new); + renderers.register(ModEntities.PARACHUTE.get(), ParachuteRenderer::new); + renderers.register(ModEntities.THROWABLE.get(), ThrowableRenderer::new); + renderers.register(ModEntities.GLTD.get(), GltdRenderer::new); + renderers.register(ModEntities.CHAIN.get(), ChainRenderer::new); + renderers.register(ModEntities.CONTAINER.get(), context -> new EquipmentRenderer<>(context, "container", 0.2F, 0xFFBFBFBF, 1.0F)); + renderers.register(ModEntities.UAV_STATION.get(), UavStationRenderer::new); + renderers.register(ModEntities.RAPPEL.get(), NoopRenderer::new); + } + + /** How a loader registers a block entity renderer. */ + public interface BlockEntityRenderers { + void register(BlockEntityType type, BlockEntityRendererProvider provider); + } + + public static void registerBlockEntityRenderers(BlockEntityRenderers renderers) { + renderers.register(ModBlockEntities.DRAFTING_TABLE.get(), DraftingTableRenderer::new); + } + + /** How a loader registers the screen of a menu: vanilla's own method is not public on all of them. */ + public interface MenuScreenRegistrar { + > void register(MenuType type, + MenuScreens.ScreenConstructor screen); + } + + /** On the main thread during client setup, once our menu types are registered. */ + public static void registerMenuScreens(MenuScreenRegistrar screens) { + screens.register(ModMenus.VEHICLE.get(), VehicleScreen::new); + screens.register(ModMenus.UAV_STATION.get(), UavStationScreen::new); + screens.register(ModMenus.DRAFTING_TABLE.get(), DraftingTableScreen::new); + } + + public static List keyMappings() { + return KeyBindings.ALL; + } + + /** Client resource reload listeners, by the id Fabric wants for each. */ + public static Map reloadListeners() { + return Map.of(new ResourceLocation(MCHeli.MOD_ID, "meshes"), MeshCache.INSTANCE, + new ResourceLocation(MCHeli.MOD_ID, "hud_scripts"), HudScripts.INSTANCE); + } + + public static void clientTickEnd() { + CrewController.onClientTick(); + GltdController.onClientTick(); + RangefinderController.onClientTick(); + LauncherController.onClientTick(); + VehicleCamera.tick(); + WeaponController.onClientTick(); + HudRenderer.tick(); + EngineSoundPlayer.tick(); + TargetMarkers.tick(); + WorkshopClient.tick(); + while (KeyBindings.TEAM_MANAGER.consumeClick()) { + openTeamManager(); + } + } + + /** Operators get the team manager; anyone else is told it is theirs to open. */ + private static void openTeamManager() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.screen != null) { + return; + } + if (TeamManager.mayManage(mc.player)) { + mc.setScreen(new TeamManagerScreen()); + } else { + mc.player.displayClientMessage(Component.translatable("message.mcheli.team.operators_only"), true); + } + } + + /** Tilts the view with the hull and moves the eye into the cockpit or onto a TV missile. */ + public static void computeCameraAngles(CameraAngles angles) { + CrewController.onCameraAngles(angles); + GltdController.onCameraAngles(angles); + } + + /** Sets the camera's rotation, roll included, the way Forge's {@code Camera#setRotation} does. */ + public interface CameraRotation { + void set(Camera camera, float yaw, float pitch, float roll); + } + + /** + * For loaders without Forge's camera angle event: called where Forge fires it, right after + * vanilla placed the camera for this frame. Starts from the camera's own angles and no roll, + * and only touches the rotation when something changed it. + */ + public static void computeCameraAngles(Camera camera, float partialTick, CameraRotation rotation) { + Angles angles = new Angles(camera, partialTick, camera.getYRot(), camera.getXRot()); + computeCameraAngles(angles); + if (angles.yaw != camera.getYRot() || angles.pitch != camera.getXRot() || angles.roll != 0.0F) { + rotation.set(camera, angles.yaw, angles.pitch, angles.roll); + } + } + + private static final class Angles implements CameraAngles { + private final Camera camera; + private final float partialTick; + private float yaw; + private float pitch; + private float roll; + + Angles(Camera camera, float partialTick, float yaw, float pitch) { + this.camera = camera; + this.partialTick = partialTick; + this.yaw = yaw; + this.pitch = pitch; + } + + @Override + public Camera camera() { + return camera; + } + + @Override + public float partialTick() { + return partialTick; + } + + @Override + public float yaw() { + return yaw; + } + + @Override + public float pitch() { + return pitch; + } + + @Override + public float roll() { + return roll; + } + + @Override + public void setYaw(float yaw) { + this.yaw = yaw; + } + + @Override + public void setPitch(float pitch) { + this.pitch = pitch; + } + + @Override + public void setRoll(float roll) { + this.roll = roll; + } + } + + /** The field of view for this frame, narrowed while the player zooms through a sight. */ + public static float fov(float fov) { + return LauncherController.fov(RangefinderController.fov(VehicleCamera.fov(fov))); + } + + /** Whether to skip drawing the first person hand this frame. */ + public static boolean hidesHand() { + return CrewController.hidesHand() || GltdController.hidesHand() || RangefinderController.isScoped() || LauncherController.isRaised(); + } + + /** What happens to a press of the use key. */ + public enum UseKey { + /** Vanilla handles it. */ + PASS, + /** Taken, and the hand does not swing: it is a weapon trigger. */ + CONSUME, + /** Taken, and the hand swings as for a vanilla click: the player boards a vehicle. */ + CONSUME_AND_SWING + } + + public static UseKey onUseKey(InteractionHand hand) { + if (WeaponController.onUseItem() || GltdController.onUseItem()) { + return UseKey.CONSUME; + } + return VehiclePicker.onUseItem(hand) ? UseKey.CONSUME_AND_SWING : UseKey.PASS; + } + + /** The pick block key switches weapons in a seat that has any; true when it was taken. */ + public static boolean onPickBlockKey() { + return WeaponController.onPickBlock(); + } + + public static void beforeRenderLiving(LivingEntity entity, PoseStack pose, float partialTicks) { + RiderPose.beforeRender(entity, pose, partialTicks); + } + + public static void drawHud(GuiGraphics graphics, float delta) { + WeaponHud.draw(graphics, delta); + GltdController.drawHud(graphics, delta); + RangefinderController.drawHud(graphics, delta); + LauncherController.drawHud(graphics, delta); + TargetMarkers.draw(graphics, delta); + } + + /** Without the original zip there are no vehicles, so say so where the player will see it. */ + public static void loggedIn(LocalPlayer player) { + MCHeli.legacyContent().problem().ifPresent(player::sendSystemMessage); + } + + public static void loggedOut() { + ClientPacks.INSTANCE.loggedOut(); + WorkshopClient.loggedOut(); + Markers.clear(); + } +} From 19499d88bad233757824da46d833c84bd2708ef8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:07 +0800 Subject: [PATCH 1168/1461] Add client/ClientPacks.java to the 1.20.6 shared sources --- .../mcheli/client/ClientPacks.java | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java new file mode 100644 index 00000000..d933e518 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java @@ -0,0 +1,338 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackRequestPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import com.jnhprojects.mcheli.platform.Platform; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * The client half of the content pack sync (the server half is {@code PackSync}). + * + *

When the server sends its pack list, every pack this client has neither in its own + * {@code mcheli/packs} nor in {@code mcheli/cache} is requested. Each arrives in chunks, is checked + * against its hash and kept as {@code mcheli/cache/.zip}. Once all are there, the content is + * rebuilt from the client's own import plus exactly the server's packs, in the server's order, so + * the vehicle a player flies (which their client simulates) has the server's definition. + * + *

In single player, and for the host of a LAN game, client and server share one registry, so + * only the client's view of the server's packs is refreshed. + * + *

The cache is capped by {@code packs.cacheSize} in the client config: after a download, the + * packs used longest ago go first. The current server's packs are always kept. + */ +public final class ClientPacks implements ClientAccess.PackReceiver { + public static final ClientPacks INSTANCE = new ClientPacks(); + + private static final Pattern CACHED = Pattern.compile("[0-9a-f]{64}\\.zip"); + + private final Path cache = Platform.get().gameDir().resolve(MCHeli.MOD_ID).resolve("cache"); + private List wanted = List.of(); + private final Map downloads = new LinkedHashMap<>(); + /** Packs this class opened for the current server; closed when the next list replaces them. */ + private List mounted = List.of(); + private int downloaded; + + private ClientPacks() { + } + + @Override + public void onPackList(PackListPacket packet) { + if (Minecraft.getInstance().hasSingleplayerServer()) { + useRoots(MCHeli.packs().active()); + ClientContentReload.refresh(); + return; + } + + wanted = packet.packs(); + downloads.clear(); + downloaded = 0; + long cap = ClientConfig.packCacheBytes(); + List request = new ArrayList<>(); + long bytes = 0; + for (PackListPacket.Entry entry : wanted) { + if (find(entry) != null) { + continue; + } + if (entry.size() > cap || entry.size() <= 0) { + say(Component.translatable("message.mcheli.pack.too_large", entry.id(), mb(entry.size()), cap >> 20)); + continue; + } + downloads.put(entry.hash(), new Download(entry)); + request.add(entry.hash()); + bytes += entry.size(); + } + + if (request.isEmpty()) { + apply(); + } else { + say(Component.translatable("message.mcheli.pack.downloading", request.size(), mb(bytes))); + ModNetwork.sendToServer(new PackRequestPacket(request)); + } + } + + @Override + public void onPackData(PackDataPacket packet) { + Download download = downloads.get(packet.hash()); + if (download == null) { + return; + } + if (!download.accept(packet)) { + fail(download, "the server sent a chunk out of order"); + return; + } + if (!download.complete()) { + return; + } + downloads.remove(packet.hash()); + try { + store(download); + downloaded++; + } catch (IOException | PackException e) { + fail(download, e.getMessage()); + return; + } + if (downloads.isEmpty()) { + apply(); + } + } + + @Override + public void onPackSound(PackSoundPacket packet) { + Minecraft.getInstance().getSoundManager().play( + new OneShotSound(packet.sound(), packet.x(), packet.y(), packet.z(), packet.volume(), packet.pitch())); + } + + /** The packs the server sent or named, for the package browser. Empty when hosting the game. */ + public List serverPacks() { + return mounted; + } + + /** Leaving a server: its packs are no longer needed. */ + void loggedOut() { + downloads.clear(); + wanted = List.of(); + RuntimeAssets.setRoots(List.of()); + close(mounted); + mounted = List.of(); + } + + private void fail(Download download, String reason) { + downloads.remove(download.entry.hash()); + MCHeli.LOGGER.error("Content pack {} could not be downloaded: {}", download.entry.id(), reason); + say(Component.translatable("message.mcheli.pack.failed", download.entry.id(), reason)); + if (downloads.isEmpty()) { + apply(); + } + } + + /** Writes a finished download to the cache once its content matches the hash it was listed with. */ + private void store(Download download) throws IOException, PackException { + String hash = download.entry.hash(); + Files.createDirectories(cache); + Path part = cache.resolve(hash + ".download.zip"); + Files.write(part, download.data); + try (ContentPack pack = ContentPack.open(part)) { + if (!pack.hash().equals(hash)) { + throw new PackException("its files do not match the hash the server listed"); + } + } catch (IOException | PackException e) { + Files.deleteIfExists(part); + throw e; + } + Files.move(part, cache.resolve(hash + ".zip"), StandardCopyOption.REPLACE_EXISTING); + } + + /** Rebuilds the content from the import and the server's packs that are now on disk. */ + private void apply() { + List opened = new ArrayList<>(); + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, MCHeli.legacyContent().assets())); + for (PackListPacket.Entry entry : wanted) { + Path file = find(entry); + if (file == null) { + continue; + } + try { + ContentPack pack = ContentPack.open(file); + if (!pack.hash().equals(entry.hash())) { + pack.close(); + MCHeli.LOGGER.warn("Content pack {} changed on disk and no longer matches the server's", file); + continue; + } + opened.add(pack); + sources.add(pack.contentSource()); + if (file.startsWith(cache)) { + Files.setLastModifiedTime(file, FileTime.from(Instant.now())); + } + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not open content pack {}", file, e); + } + } + + ContentRegistry registry = ContentRegistry.load(sources); + MCHeli.content().replaceWith(registry); + List old = mounted; + mounted = List.copyOf(opened); + useRoots(mounted); + close(old); + ClientContentReload.refresh(); + trimCache(); + if (downloaded > 0) { + say(Component.translatable("message.mcheli.pack.ready", downloaded)); + downloaded = 0; + } + } + + /** The pack on disk with the entry's content: one of the player's own packs, or a cached download. */ + private @Nullable Path find(PackListPacket.Entry entry) { + for (ContentPack own : MCHeli.packs().active()) { + if (own.hash().equals(entry.hash())) { + return own.source(); + } + } + Path cached = cache.resolve(entry.hash() + ".zip"); + return Files.isRegularFile(cached) ? cached : null; + } + + /** Deletes the cached packs used longest ago until the cache fits its cap. */ + private void trimCache() { + Set keep = new HashSet<>(); + for (PackListPacket.Entry entry : wanted) { + keep.add(entry.hash() + ".zip"); + } + List files; + try (Stream list = Files.list(cache)) { + files = list.filter(p -> CACHED.matcher(p.getFileName().toString()).matches()) + .sorted(Comparator.comparing(ClientPacks::lastUsed)) + .toList(); + } catch (IOException e) { + return; + } + long total = 0; + for (Path file : files) { + total += size(file); + } + long cap = ClientConfig.packCacheBytes(); + for (Path file : files) { + if (total <= cap) { + break; + } + if (keep.contains(file.getFileName().toString())) { + continue; + } + try { + long size = size(file); + Files.delete(file); + total -= size; + MCHeli.LOGGER.info("Removed {} from the content pack cache", file.getFileName()); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not remove {} from the content pack cache", file, e); + } + } + } + + private static FileTime lastUsed(Path file) { + try { + return Files.getLastModifiedTime(file); + } catch (IOException e) { + return FileTime.fromMillis(0); + } + } + + private static long size(Path file) { + try { + return Files.size(file); + } catch (IOException e) { + return 0; + } + } + + /** The pack loaded last overrides the others, so it is searched first. */ + private static void useRoots(List packs) { + List roots = new ArrayList<>(); + for (ContentPack pack : packs) { + roots.addFirst(pack.assets()); + } + RuntimeAssets.setRoots(roots); + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } + + private static void say(Component message) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null) { + mc.player.sendSystemMessage(message); + } else { + MCHeli.LOGGER.info(message.getString()); + } + } + + private static String mb(long bytes) { + return "%.1f".formatted(bytes / 1048576.0); + } + + /** One pack arriving from the server. */ + private static final class Download { + private final PackListPacket.Entry entry; + private byte @Nullable [] data; + private int received; + + Download(PackListPacket.Entry entry) { + this.entry = entry; + } + + boolean accept(PackDataPacket packet) { + if (packet.total() != entry.size() || packet.offset() != received + || received + packet.data().length > entry.size()) { + return false; + } + if (data == null) { + data = new byte[entry.size()]; + } + System.arraycopy(packet.data(), 0, data, received, packet.data().length); + received += packet.data().length; + return true; + } + + boolean complete() { + return data != null && received == data.length; + } + } +} From c813c9ef7e0af404a7d6d0ef0541d8c6a9eec8d4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:07 +0800 Subject: [PATCH 1169/1461] Add client/CrewController.java to the 1.20.6 shared sources --- .../mcheli/client/CrewController.java | 482 ++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/CrewController.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/CrewController.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/CrewController.java new file mode 100644 index 00000000..54382add --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/CrewController.java @@ -0,0 +1,482 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.CameraPosition; +import com.jnhprojects.mcheli.content.aircraft.SeatDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PilotStatePacket; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.Input; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +/** + * Turns local player input into vehicle control, for whichever seat the player occupies. + * + *

Keys are sampled once per client tick. The mouse is handled every frame, the way the + * original did it: for helicopters and planes, vanilla first turns the player's view, then the + * change is fed to the aircraft and the view is snapped back onto the aircraft's nose. Everyone + * else looks around freely, within the limits of their seat, and turrets follow their view. + * + *

Gunner seats and camera positions move the first person eye to the point the definition + * gives, which is how the original's camera entity behaved. Every other seat keeps its eye above + * the seat along the hull's up axis, so it tilts with the vehicle. + */ +final class CrewController { + /** Two presses of jump within this many ticks eject the player. */ + private static final int EJECT_WINDOW = 10; + + private static boolean jumpWasDown; + private static int lastJumpPress = -100; + private static int ticks; + private static boolean tracking; + private static float lastYaw; + private static float lastPitch; + + private static @Nullable AircraftEntity viewVehicle; + private static int viewSeat = -1; + private static int cameraIndex; + private static boolean gunnerSight; + + private static @Nullable MethodHandle setCameraPosition; + private static boolean cameraLookupFailed; + + private CrewController() { + } + + /** + * Like the original, the first person hand stays hidden in a vehicle: always for the pilot, + * and for anyone else who looks through a camera or has a weapon to aim. + */ + static boolean hidesHand() { + LocalPlayer player = Minecraft.getInstance().player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + return false; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return false; + } + VehicleWeapons weapons = vehicle.weapons(); + boolean armed = weapons != null && weapons.selected(seat) >= 0; + return seat == 0 || armed || tvCamera(vehicle, seat) != null || eyePosition(vehicle, def, seat) != null; + } + + private static @Nullable AircraftEntity vehicleOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof AircraftEntity vehicle && vehicle.definition() != null ? vehicle : null; + } + + static void onClientTick() { + ticks++; + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + jumpWasDown = false; + tracking = false; + viewVehicle = null; + viewSeat = -1; + // The GLTD's sight shares the zoom and camera mode; only leaving it resets them. + if (GltdController.gltdOf(player) == null) { + VehicleCamera.reset(); + } + return; + } + int seat = vehicle.seatOf(player); + if (vehicle != viewVehicle || seat != viewSeat) { + // New vehicle or seat: start from the default view. + viewVehicle = vehicle; + viewSeat = seat; + cameraIndex = 0; + gunnerSight = false; + tracking = false; + VehicleCamera.reset(); + } + + Input keys = player.input; + boolean jumpPressed = keys.jumping && !jumpWasDown; + jumpWasDown = keys.jumping; + boolean pilot = seat == 0 && vehicle.getControllingPassenger() == player; + + if (pilot) { + AircraftEntity.ControlInput input = vehicle.input(); + input.throttleUp = keys.up; + input.throttleDown = keys.down; + input.left = keys.left; + input.right = keys.right; + input.freeLook = isFreeLook(vehicle); + input.brake = vehicle instanceof TankEntity && keys.jumping; + if (input.brake) { + // The hand brake also stops pivot turns once the vehicle has come to rest. + input.throttleUp = false; + input.throttleDown = false; + double dx = vehicle.getX() - vehicle.xo; + double dz = vehicle.getZ() - vehicle.zo; + if (vehicle.visibleThrottle() <= 0.03F && dx * dx + dz * dz < 0.01) { + input.left = false; + input.right = false; + } + } + if (jumpPressed && vehicle instanceof HelicopterEntity heli) { + heli.toggleHovering(); + } + ModNetwork.sendToServer(new PilotStatePacket(vehicle.getId(), vehicle.visibleThrottle(), vehicle.reverse(), + vehicle.roll(), vehicle.isHoveringMode(), input.write())); + } + + // Eject with a double tap, except for helicopter pilots whose jump key toggles hover. + if (jumpPressed && vehicle.canEject(player) && !(pilot && vehicle instanceof HelicopterEntity)) { + if (ticks - lastJumpPress <= EJECT_WINDOW) { + send(vehicle, CrewActionPacket.Action.EJECT); + lastJumpPress = -100; + } else { + lastJumpPress = ticks; + } + } + + while (KeyBindings.SWITCH_SEAT.consumeClick()) { + send(vehicle, CrewActionPacket.Action.SWITCH_SEAT); + } + while (KeyBindings.DROP_CREW.consumeClick()) { + send(vehicle, pilot ? CrewActionPacket.Action.DROP_CREW : CrewActionPacket.Action.JUMP_OUT); + } + while (KeyBindings.SWITCH_CAMERA.consumeClick()) { + switchCamera(vehicle, seat); + } + while (KeyBindings.LANDING_GEAR.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.LANDING_GEAR); + } + } + while (KeyBindings.FOLD.consumeClick()) { + if (looksThroughSight(vehicle, seat)) { + VehicleCamera.onZoomKey(player, vehicle); + } else if (pilot) { + send(vehicle, CrewActionPacket.Action.FOLD); + } + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + // As in the original, the key works the search light in vehicles that have one. + if (vehicle.canSwitchSearchLight(player)) { + send(vehicle, CrewActionPacket.Action.SEARCH_LIGHT); + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 1.0F); + } else { + VehicleCamera.onCameraModeKey(player, vehicle); + } + } + while (KeyBindings.VEHICLE_SCREEN.consumeClick()) { + if (!vehicle.isDestroyed()) { + send(vehicle, CrewActionPacket.Action.VEHICLE_SCREEN); + } + } + while (KeyBindings.VTOL.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.VTOL); + } + } + while (KeyBindings.RACK_LOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_LOAD); + } + } + while (KeyBindings.RACK_UNLOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_UNLOAD); + } + } + } + + /** + * Whether this seat sees through a sight that can zoom: a camera position or gunner sight, the + * pilot in gunner mode, or the pilot of a UAV. The zoom key folds the wings everywhere else. + */ + private static boolean looksThroughSight(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount() || def.cameraZoom <= 1) { + return false; + } + return eyePosition(vehicle, def, seat) != null || vehicle.isGunnerMode() || def.uav; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + /** Pilot cycles the camera positions; gunners that may leave their sight toggle it. */ + private static void switchCamera(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount()) { + return; + } + SeatDefinition s = def.seats.get(seat); + if (seat == 0) { + switchPilotView(vehicle, def); + } else if (s.gunner() && s.switchGunner()) { + gunnerSight = !gunnerSight; + } + } + + /** + * The pilot's switch mode key, after {@code KeySwitchMode}: leave gunner mode, otherwise step + * through the extra camera positions, otherwise enter gunner mode where the vehicle allows it, + * otherwise go to the first extra camera. Unlike the original, leaving gunner mode never + * depends on the plane still meeting the conditions for entering it. + */ + private static void switchPilotView(AircraftEntity vehicle, AircraftDefinition def) { + int cameras = def.cameraPositions.size(); + if (vehicle.isGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + cameraIndex = 0; + } else if (cameraIndex > 0) { + cameraIndex = (cameraIndex + 1) % cameras; + } else if (vehicle.canSwitchGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + } else if (cameras >= 2) { + cameraIndex = 1; + } + } + + /** + * Free look for the pilot. Helicopters and planes look around while the key is held; tanks + * start from their {@code DefaultFreelook} setting and the key swaps it while held. + */ + private static boolean isFreeLook(AircraftEntity vehicle) { + boolean held = KeyBindings.FREE_LOOK.isDown(); + if (vehicle instanceof TankEntity) { + AircraftDefinition def = vehicle.definition(); + return def != null && def.defaultFreelook != held; + } + return held; + } + + static void onCameraAngles(CameraAngles event) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null || mc.isPaused()) { + return; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return; + } + float partialTicks = event.partialTick(); + boolean firstPerson = mc.options.getCameraType().isFirstPerson(); + + if (seat == 0 && vehicle.steersWithMouse() && !vehicle.isGunnerMode() && vehicle.getControllingPassenger() == player) { + steerWithMouse(vehicle, player, event, partialTicks, firstPerson); + } else { + tracking = false; + constrainView(vehicle, def, def.seats.get(seat), seat, player); + } + + TvMissileEntity missile = tvCamera(vehicle, seat); + if (!(firstPerson && missile != null)) { + // Every seat of every kind tilts with the hull, measured along the way the rider looks. + event.setRoll(AircraftRotation.cameraRoll(vehicle.getViewYRot(partialTicks), vehicle.getViewXRot(partialTicks), + vehicle.roll(partialTicks), event.yaw(), event.pitch())); + } + if (firstPerson && missile != null) { + placeCamera(event.camera(), missile.getPosition(partialTicks)); + } else if (firstPerson) { + Vector3 eye = eyePosition(vehicle, def, seat); + moveCamera(event.camera(), vehicle, def, eye != null ? eye : seatEye(vehicle, def, seat, player), seat, partialTicks); + } + } + + /** + * The normal eye, in hull space. Vanilla puts it straight above the rider in world space, + * which leaves the head outside the cockpit once the hull banks or pitches; measured along the + * hull's own up axis it stays where the rider model's head is drawn. + */ + private static Vector3 seatEye(AircraftEntity vehicle, AircraftDefinition def, int seat, LocalPlayer player) { + Vector3 pos = def.seats.get(seat).position(); + double up = player.getEyeHeight() - player.getVehicleAttachmentPoint(vehicle).y; + return new Vector3(pos.x(), pos.y() + up, pos.z()); + } + + /** The phase one pilot model: mouse movement becomes aircraft rotation, the view stays on the nose. */ + private static void steerWithMouse(AircraftEntity vehicle, LocalPlayer player, CameraAngles event, + float partialTicks, boolean firstPerson) { + boolean freeLook = KeyBindings.FREE_LOOK.isDown(); + boolean ownPitch = vehicle.isHovering(); + if (!freeLook) { + if (tracking) { + float dYaw = Mth.wrapDegrees(player.getYRot() - lastYaw); + float dPitch = player.getXRot() - lastPitch; + vehicle.applyPilotLook(dYaw, dPitch); + HudRenderer.onPilotLook(dYaw, dPitch); + } + lockView(player, vehicle.getYRot(), vehicle.yRotO, ownPitch ? null : vehicle.getXRot(), vehicle.xRotO); + lastYaw = player.getYRot(); + lastPitch = player.getXRot(); + tracking = true; + } else { + // Re-acquire the nose smoothly once free look is released. + tracking = false; + } + if (firstPerson && !freeLook) { + event.setYaw(vehicle.getViewYRot(partialTicks)); + if (!ownPitch) { + event.setPitch(vehicle.getViewXRot(partialTicks)); + } + } + } + + /** Seat limits for everyone who does not fly with the mouse. */ + private static void constrainView(AircraftEntity vehicle, AircraftDefinition def, SeatDefinition seat, int index, LocalPlayer player) { + boolean freeLookHeld = KeyBindings.FREE_LOOK.isDown(); + if (seat.fixedRotation() && !freeLookHeld) { + lockView(player, vehicle.getYRot() + seat.fixedYaw(), vehicle.yRotO + seat.fixedYaw(), + vehicle.getXRot() + seat.fixedPitch(), vehicle.xRotO + seat.fixedPitch()); + return; + } + boolean pilot = index == 0; + if (pilot && vehicle instanceof TankEntity && !isFreeLook(vehicle)) { + lockView(player, vehicle.getYRot(), vehicle.yRotO, vehicle.getXRot(), vehicle.xRotO); + return; + } + float min; + float max; + if (pilot && (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity)) { + min = def.minRotationPitch; + max = def.maxRotationPitch; + } else if (seat.gunner() || (pilot && vehicle.isGunnerMode())) { + min = seat.minPitch(); + max = seat.maxPitch(); + } else { + return; + } + // Limits are relative to the hull's tilt along the view direction. + double rel = Math.toRadians(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot())); + float tilt = (float) (vehicle.getXRot() * Math.cos(rel) + vehicle.roll() * Math.sin(rel)); + float pitch = Mth.clamp(player.getXRot(), tilt + min, tilt + max); + pitch = Mth.clamp(pitch, -90.0F, 90.0F); + if (pitch != player.getXRot()) { + player.setXRot(pitch); + player.xRotO = pitch; + } + } + + private static void lockView(LocalPlayer player, float yaw, float yawO, @Nullable Float pitch, float pitchO) { + player.setYRot(yaw); + player.yRotO = yawO; + player.setYHeadRot(yaw); + player.setYBodyRot(yaw); + if (pitch != null) { + player.setXRot(pitch); + player.xRotO = pitchO; + } + } + + /** + * Where the first person eye goes, in hull space, or {@code null} for the normal eye. Mirrors + * the original's choice between the pilot's camera positions and a gunner seat's sight. + */ + private static @Nullable Vector3 eyePosition(AircraftEntity vehicle, AircraftDefinition def, int seat) { + SeatDefinition s = def.seats.get(seat); + boolean sight = s.gunner() && (!s.switchGunner() || gunnerSight); + CameraPosition camera = null; + if (seat == 0) { + if (cameraIndex > 0 && cameraIndex < def.cameraPositions.size()) { + camera = def.cameraPositions.get(cameraIndex); + } else if (def.uav || def.alwaysCameraView || sight || vehicle.isGunnerMode() || vehicle instanceof GroundVehicleEntity) { + camera = def.cameraPositions.getFirst(); + } + } else if (sight) { + camera = s.camera() != null && s.camera() != CameraPosition.DEFAULT ? s.camera() : def.cameraPositions.getFirst(); + } + // A camera at the origin means the file never set one; the normal eye is better there. + if (camera == null || camera.position().equals(Vector3.ZERO)) { + return null; + } + return camera.position(); + } + + private static void moveCamera(Camera camera, AircraftEntity vehicle, AircraftDefinition def, Vector3 eye, int seat, float partialTicks) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + SeatDefinition s = def.seats.get(seat); + Vector3 local = s.rotatingSeat() ? vehicle.onTurret(def, eye, partialTicks) : eye; + Vector3f offset = AircraftRotation.toWorld(vehicle.getViewYRot(partialTicks), vehicle.getViewXRot(partialTicks), vehicle.roll(partialTicks), + local.x(), local.y(), local.z()); + placeCamera(camera, new Vec3(Mth.lerp(partialTicks, vehicle.xo, vehicle.getX()) + offset.x, + Mth.lerp(partialTicks, vehicle.yo, vehicle.getY()) + offset.y, + Mth.lerp(partialTicks, vehicle.zo, vehicle.getZ()) + offset.z)); + } + + /** + * The TV missile this seat is flying, whose nose camera replaces the eye. Like the original, + * only while the seat has a TV missile selected in its guided mode. + */ + static @Nullable TvMissileEntity tvCamera(AircraftEntity vehicle, int seat) { + TvMissileEntity missile = vehicle.tvMissile(); + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + if (missile == null || set == null || set.definition().type != WeaponType.TV_MISSILE || set.mode() != 0) { + return null; + } + return missile; + } + + static void placeCamera(Camera camera, Vec3 at) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + try { + setter.invoke(camera, at); + } catch (Throwable t) { + MCHeli.LOGGER.warn("Could not move the camera; the first person eye stays where vanilla puts it", t); + setCameraPosition = null; + cameraLookupFailed = true; + } + } + + private static Method accessible(Method method) { + method.setAccessible(true); + return method; + } + + /** {@code Camera.setPosition} is protected. Looked up once; on failure the feature turns itself off. */ + private static @Nullable MethodHandle cameraSetter() { + if (setCameraPosition == null && !cameraLookupFailed) { + try { + setCameraPosition = MethodHandles.lookup().unreflect( + accessible(Camera.class.getDeclaredMethod("setPosition", Vec3.class))); + } catch (Exception e) { + MCHeli.LOGGER.warn("Camera.setPosition is not accessible; the first person eye stays where vanilla puts it", e); + cameraLookupFailed = true; + } + } + return setCameraPosition; + } +} From e19aadbbb622af0b68dab5107f1a2f383e095072 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:08 +0800 Subject: [PATCH 1170/1461] Add client/GltdController.java to the 1.20.6 shared sources --- .../mcheli/client/GltdController.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/GltdController.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/GltdController.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/GltdController.java new file mode 100644 index 00000000..f755c9ae --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/GltdController.java @@ -0,0 +1,104 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.network.EquipmentActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +/** + * The local player at a GLTD, {@code MCH_ClientGLTDTickHandler} and {@code MCH_GuiGLTD}: the + * view moves to the sight, the zoom and camera mode keys work as in a vehicle's sight, the use + * key calls the A-10 on the spot under the cross, and a small readout says when it can be called + * again. + */ +final class GltdController { + /** The original zoomed smoothly up to ten; the vehicle sights double each press. */ + private static final int MAX_ZOOM = 8; + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int CROSS = 0xC0FF4040; + + /** Edge of the use key, so holding it calls one strike rather than one per tick. */ + private static boolean useWasDown; + + private GltdController() { + } + + static @Nullable GltdEntity gltdOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof GltdEntity gltd ? gltd : null; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + GltdEntity gltd = gltdOf(player); + if (gltd == null) { + useWasDown = false; + return; + } + while (KeyBindings.FOLD.consumeClick()) { + VehicleCamera.zoomStep(player, MAX_ZOOM); + } + while (KeyBindings.CAMERA_MODE.consumeClick()) { + VehicleCamera.nextMode(player, on -> send(gltd, + on ? EquipmentActionPacket.Action.NIGHT_VISION_ON : EquipmentActionPacket.Action.NIGHT_VISION_OFF)); + } + boolean useDown = mc.screen == null && mc.options.keyUse.isDown(); + if (useDown && !useWasDown) { + send(gltd, EquipmentActionPacket.Action.CALL_STRIKE); + } + useWasDown = useDown; + } + + /** The use key belongs to the sight while sitting at a GLTD. */ + static boolean onUseItem() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + /** First person looks out of the sight rather than from behind the tripod. */ + static void onCameraAngles(CameraAngles angles) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd != null && mc.options.getCameraType().isFirstPerson()) { + CrewController.placeCamera(angles.camera(), gltd.sightPosition(angles.partialTick())); + } + } + + static boolean hidesHand() { + return gltdOf(Minecraft.getInstance().player) != null; + } + + static void drawHud(GuiGraphics g, float delta) { + Minecraft mc = Minecraft.getInstance(); + GltdEntity gltd = gltdOf(mc.player); + if (gltd == null || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int cx = g.guiWidth() / 2; + int cy = g.guiHeight() / 2; + g.fill(cx - 12, cy, cx - 3, cy + 1, CROSS); + g.fill(cx + 4, cy, cx + 13, cy + 1, CROSS); + g.fill(cx, cy - 12, cx + 1, cy - 3, CROSS); + g.fill(cx, cy + 4, cx + 1, cy + 13, CROSS); + + Font font = mc.font; + int x = cx + 20; + int y = cy + 20; + int wait = gltd.waitTicks(); + if (wait > 0) { + g.drawString(font, Component.translatable("hud.mcheli.gltd.wait", (wait + 19) / 20), x, y, AMBER); + } else { + g.drawString(font, Component.translatable("hud.mcheli.gltd.ready"), x, y, WHITE); + } + g.drawString(font, Component.translatable("hud.mcheli.zoom", Math.round(VehicleCamera.zoom())), x, y + 10, WHITE); + } + + private static void send(GltdEntity gltd, EquipmentActionPacket.Action action) { + ModNetwork.sendToServer(new EquipmentActionPacket(gltd.getId(), action)); + } +} From c97ca88959e8a16a9101c0774b9979e2bfdf93e1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:08 +0800 Subject: [PATCH 1171/1461] Add client/KeyBindings.java to the 1.20.6 shared sources --- .../mcheli/client/KeyBindings.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java new file mode 100644 index 00000000..dc594e26 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.KeyMapping; + +import java.util.List; + +/** + * Mod key bindings. Throttle, steering, hover, brake and eject reuse the vanilla movement and jump + * keys (W/S, A/D and Space by default), which matches the original defaults. Firing uses the + * vanilla use key and the pick block button switches weapons, the right and middle mouse buttons + * of the original. The rest keep the original's letters where vanilla leaves them free. + */ +public final class KeyBindings { + public static final String CATEGORY = "key.category." + MCHeli.MOD_ID + ".main"; + + /** Hold to look around without steering. The original used Left Ctrl, which now clashes with sprint. */ + public static final KeyMapping FREE_LOOK = new KeyMapping("key.mcheli.free_look", InputConstants.KEY_LALT, CATEGORY); + /** Cycles the pilot's camera positions, or toggles the gunner sight in seats that allow it. */ + public static final KeyMapping SWITCH_CAMERA = new KeyMapping("key.mcheli.switch_camera", InputConstants.KEY_H, CATEGORY); + public static final KeyMapping LANDING_GEAR = new KeyMapping("key.mcheli.landing_gear", InputConstants.KEY_B, CATEGORY); + /** Zooms through a sight, as the original Z key did; otherwise folds hatches and wings, sweeps variable geometry wings. */ + public static final KeyMapping FOLD = new KeyMapping("key.mcheli.fold", InputConstants.KEY_Z, CATEGORY); + /** The original used F, which vanilla now uses to swap hands. */ + public static final KeyMapping VTOL = new KeyMapping("key.mcheli.vtol", InputConstants.KEY_V, CATEGORY); + public static final KeyMapping SWITCH_SEAT = new KeyMapping("key.mcheli.switch_seat", InputConstants.KEY_R, CATEGORY); + public static final KeyMapping RACK_LOAD = new KeyMapping("key.mcheli.rack_load", InputConstants.KEY_J, CATEGORY); + public static final KeyMapping RACK_UNLOAD = new KeyMapping("key.mcheli.rack_unload", InputConstants.KEY_U, CATEGORY); + /** The second weapon switch key of the original; the first is the vanilla pick block button. */ + public static final KeyMapping SWITCH_WEAPON = new KeyMapping("key.mcheli.switch_weapon", InputConstants.KEY_G, CATEGORY); + public static final KeyMapping WEAPON_MODE = new KeyMapping("key.mcheli.weapon_mode", InputConstants.KEY_X, CATEGORY); + /** The original used V, which VTOL took over. */ + public static final KeyMapping FLARE = new KeyMapping("key.mcheli.flare", InputConstants.KEY_C, CATEGORY); + /** Fuel and parachute slots, reloading and the cargo chest. The original used R, which switches seats here. */ + public static final KeyMapping VEHICLE_SCREEN = new KeyMapping("key.mcheli.vehicle_screen", InputConstants.KEY_I, CATEGORY); + /** Night vision, thermal view and back in vehicles with {@code EnableNightVision}. The original used C, now the flare key. */ + public static final KeyMapping CAMERA_MODE = new KeyMapping("key.mcheli.camera_mode", InputConstants.KEY_K, CATEGORY); + /** The pilot lets the troops down; anyone else aboard slides down a rope or jumps. The original used Y for the pilot's part. */ + public static final KeyMapping DROP_CREW = new KeyMapping("key.mcheli.drop_crew", InputConstants.KEY_Y, CATEGORY); + /** Operators open the team manager, as the original's multiplayer manager key M did. */ + public static final KeyMapping TEAM_MANAGER = new KeyMapping("key.mcheli.team_manager", InputConstants.KEY_M, CATEGORY); + + public static final List ALL = List.of(FREE_LOOK, SWITCH_CAMERA, LANDING_GEAR, FOLD, VTOL, SWITCH_SEAT, RACK_LOAD, RACK_UNLOAD, + SWITCH_WEAPON, WEAPON_MODE, FLARE, VEHICLE_SCREEN, CAMERA_MODE, DROP_CREW, TEAM_MANAGER); + + private KeyBindings() { + } +} From 3879cfa34e1c2d3ef8ae88f11fc27e2b4291e0ff Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:09 +0800 Subject: [PATCH 1172/1461] Add client/LauncherController.java to the 1.20.6 shared sources --- .../mcheli/client/LauncherController.java | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java new file mode 100644 index 00000000..ae6ea4b5 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/LauncherController.java @@ -0,0 +1,235 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.network.LauncherPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The Stinger and Javelin in the player's hands, {@code MCH_ClientLightWeaponTickHandler} and + * {@code MCH_GuiLightWeapon}. Raised, the launcher's sight covers the view and its guidance locks + * onto what the cross holds; the attack key fires once the lock is complete. Z steps the zoom, X + * the Javelin's direct and top attack modes. An empty launcher held down reloads itself from a + * missile in the inventory. + */ +final class LauncherController { + /** Ticks of raising before the sight and the lock start, as in the original. */ + private static final int RAISE_TICKS = 10; + private static final int RELOAD_TICKS = 40; + /** Top attack climbs first and needs this much room, measured flat. */ + private static final double TOP_ATTACK_MIN_DISTANCE = 40.0; + private static final int GREEN = 0xFF2AD848; + private static final int RED = 0xFFDF0808; + private static final int WHITE = 0xFFFFFFFF; + + private static @Nullable String weapon; + private static @Nullable Guidance guidance; + private static int zoomIndex; + private static int mode; + private static int reloadTicks; + private static int lockSoundTicks; + private static boolean attackWasDown; + + private LauncherController() { + } + + private static ItemStack launcher(@Nullable LocalPlayer player) { + if (player == null || !LauncherItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getItemInHand(InteractionHand.MAIN_HAND); + return stack.getItem() instanceof LauncherItem ? stack : ItemStack.EMPTY; + } + + /** Whether the player holds a launcher up to the eye. */ + static boolean isRaised() { + LocalPlayer player = Minecraft.getInstance().player; + ItemStack stack = launcher(player); + return !stack.isEmpty() && player.isUsingItem() && player.getUseItem() == stack && player.getTicksUsingItem() > RAISE_TICKS; + } + + private static @Nullable WeaponDefinition definition(ItemStack stack) { + return MCHeli.content().weapon(((LauncherItem) stack.getItem()).weapon()); + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = launcher(player); + WeaponDefinition def = stack.isEmpty() ? null : definition(stack); + if (def == null) { + weapon = null; + guidance = null; + reloadTicks = 0; + attackWasDown = false; + return; + } + String name = ((LauncherItem) stack.getItem()).weapon(); + if (!name.equals(weapon) || guidance == null) { + weapon = name; + boolean air = def.type != WeaponType.AT_MISSILE; + guidance = new Guidance(air, !air, def.ridableOnly, def.lockTime, e -> e != player && !e.isAlliedTo(player)); + zoomIndex = 0; + mode = 0; + } + boolean raised = isRaised(); + boolean attackDown = raised && mc.screen == null && mc.options.keyAttack.isDown(); + boolean attackPressed = attackDown && !attackWasDown; + attackWasDown = attackDown; + + if (LauncherItem.isLoaded(stack)) { + reloadTicks = 0; + if (raised) { + if (guidance.lock(player, player.getEyePosition())) { + lockSoundTicks = 0; + } + playLockSound(player); + } else { + guidance.clear(); + } + } else { + guidance.clear(); + reload(player, stack); + } + guidance.tick(); + + if (!raised) { + return; + } + while (KeyBindings.FOLD.consumeClick()) { + if (def.zoom.length > 1) { + zoomIndex = (zoomIndex + 1) % def.zoom.length; + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + if (def.modeNum > 1) { + mode = (mode + 1) % def.modeNum; + player.level().playLocalSound(player, ModSounds.get("pi"), SoundSource.PLAYERS, 0.5F, 0.9F); + } + } + if (attackPressed) { + Entity target = guidance.lastLocked(); + if (LauncherItem.isLoaded(stack) && guidance.isLocked() && target != null && canFire(player, target)) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.FIRE, target.getId(), mode)); + guidance.clear(); + } else { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + } + } + + /** Top attack needs room to climb and dive. */ + private static boolean canFire(LocalPlayer player, @Nullable Entity target) { + if (mode == 0 || target == null) { + return true; + } + double dx = target.getX() - player.getX(); + double dz = target.getZ() - player.getZ(); + return Math.sqrt(dx * dx + dz * dz) >= TOP_ATTACK_MIN_DISTANCE; + } + + /** The tone quickens as the lock builds. */ + private static void playLockSound(LocalPlayer player) { + if (guidance == null || guidance.lockCount() <= 0) { + lockSoundTicks = 0; + return; + } + if (lockSoundTicks > 0) { + lockSoundTicks--; + return; + } + lockSoundTicks = Math.max(2, (int) (7 * (1.0 - (double) guidance.lockCount() / guidance.lockCountMax()))); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + + /** Held down and empty, with a missile to hand: two seconds to load it. */ + private static void reload(LocalPlayer player, ItemStack stack) { + LauncherItem item = (LauncherItem) stack.getItem(); + boolean hasMissile = player.getAbilities().instabuild || player.getInventory().findSlotMatchingItem(new ItemStack(item.missile())) >= 0; + if (!hasMissile || player.isUsingItem()) { + reloadTicks = 0; + return; + } + reloadTicks++; + if (reloadTicks == 10) { + player.level().playLocalSound(player, ModSounds.get("fim92_reload"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (reloadTicks == RELOAD_TICKS) { + ModNetwork.sendToServer(new LauncherPacket(LauncherPacket.Action.RELOAD, 0, 0)); + } + } + + static float fov(float fov) { + if (!isRaised()) { + return fov; + } + WeaponDefinition def = definition(launcher(Minecraft.getInstance().player)); + if (def == null || def.zoom.length == 0) { + return fov; + } + float zoom = def.zoom[Math.min(zoomIndex, def.zoom.length - 1)]; + return zoom > 1.0F ? fov / zoom : fov; + } + + static void drawHud(GuiGraphics g, float delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (!mc.options.getCameraType().isFirstPerson()) { + return; + } + ItemStack stack = launcher(player); + if (stack.isEmpty()) { + return; + } + Font font = mc.font; + if (!isRaised()) { + if (!LauncherItem.isLoaded(stack) && reloadTicks > 0) { + g.drawString(font, Component.translatable("hud.mcheli.launcher.reloading"), g.guiWidth() / 2 - 30, g.guiHeight() / 2 + 20, WHITE); + } + return; + } + WeaponDefinition def = definition(stack); + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + String texture = def != null && def.type == WeaponType.AT_MISSILE ? "javelin" : "stinger"; + g.blit(new ResourceLocation(MCHeli.MOD_ID, "textures/gui/" + texture + ".png"), (w - size) / 2, (h - size) / 2, size, size, 0.0F, 0.0F, 256, 256, 256, 256); + + int x = w / 2 + 20; + int y = h / 2 + 20; + if (!LauncherItem.isLoaded(stack)) { + g.drawString(font, Component.translatable("hud.mcheli.launcher.empty"), x, y, RED); + } else if (guidance != null && guidance.isLocked()) { + Entity target = guidance.lastLocked(); + g.drawString(font, Component.translatable("hud.mcheli.locked"), x, y, canFire(player, target) ? GREEN : RED); + if (target != null) { + g.drawString(font, String.format("[%.0f m]", target.distanceTo(player)), x, y + 10, GREEN); + } + } else if (guidance != null && guidance.lockCount() > 0) { + g.drawString(font, Component.translatable("hud.mcheli.locking", guidance.lockCount() * 100 / Math.max(guidance.lockCountMax(), 1)), x, y, WHITE); + } + if (def != null && def.modeNum > 1) { + g.drawString(font, Component.translatable(mode == 0 ? "hud.mcheli.launcher.direct" : "hud.mcheli.launcher.top_attack"), x, y + 20, WHITE); + } + if (def != null && def.zoom.length > 0) { + g.drawString(font, Component.translatable("hud.mcheli.zoom", String.format("%.1f", def.zoom[Math.min(zoomIndex, def.zoom.length - 1)])), + x, y + 30, WHITE); + } + } +} From 93a0e4318e49fe07c4c8bfe1eb6d69d9d0604626 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:09 +0800 Subject: [PATCH 1173/1461] Add client/RangefinderController.java to the 1.20.6 shared sources --- .../mcheli/client/RangefinderController.java | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java new file mode 100644 index 00000000..08df6b45 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/RangefinderController.java @@ -0,0 +1,153 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.SpotPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.Locale; + +/** + * Looking through the rangefinder, {@code MCH_ClientToolTickHandler} and {@code MCH_GuiRangeFinder}. + * Once it has been held up for a moment the scope covers the view and reads the range; the zoom + * key and the weapon mode key zoom in and out while held, the flare key picks what to spot, and + * the attack key spots. + */ +final class RangefinderController { + private static final ResourceLocation SCOPE = new ResourceLocation(MCHeli.MOD_ID, "textures/gui/rangefinder.png"); + /** Ticks of holding it up before the scope shows, as in the original. */ + private static final int RAISE_TICKS = 8; + private static final int COOLDOWN = 60; + private static final float MIN_ZOOM = 1.2F; + private static final float MAX_ZOOM = 10.0F; + private static final double RANGE = 300.0; + private static final int GREEN = 0xFF10FF20; + private static final int RED = 0xFFFF1010; + private static final int WHITE = 0xFFFFFFFF; + private static final int GREY = 0xFF404040; + + private static float zoom = 2.0F; + private static SpotPacket.Mode mode = SpotPacket.Mode.PLAYERS_AND_VEHICLES; + private static int cooldown; + + private RangefinderController() { + } + + /** The rangefinder the player is looking through, or empty. */ + private static ItemStack scope(LocalPlayer player) { + if (player == null || !player.isUsingItem() || player.getTicksUsingItem() <= RAISE_TICKS || !RangefinderItem.canUse(player)) { + return ItemStack.EMPTY; + } + ItemStack stack = player.getUseItem(); + return stack.getItem() instanceof RangefinderItem ? stack : ItemStack.EMPTY; + } + + static boolean isScoped() { + return !scope(Minecraft.getInstance().player).isEmpty(); + } + + static void onClientTick() { + if (cooldown > 0) { + cooldown--; + } + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty()) { + zoom = 2.0F; + return; + } + if (KeyBindings.FOLD.isDown() && zoom < MAX_ZOOM) { + zoom = Math.min(zoom + zoom / 10.0F, MAX_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 1.0F); + } + if (KeyBindings.WEAPON_MODE.isDown() && zoom > MIN_ZOOM) { + zoom = Math.max(zoom - zoom / 10.0F, MIN_ZOOM); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.05F, 0.9F); + } + while (KeyBindings.FLARE.consumeClick()) { + mode = SpotPacket.Mode.values()[(mode.ordinal() + 1) % SpotPacket.Mode.values().length]; + // Alone in a world there is no one else to spot, as the original reasoned. + boolean alone = mc.hasSingleplayerServer() && !mc.getSingleplayerServer().isPublished(); + if (alone && mode == SpotPacket.Mode.PLAYERS_AND_VEHICLES) { + mode = SpotPacket.Mode.MOBS; + } + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 0.9F); + } + if (mc.options.keyAttack.isDown() && cooldown == 0) { + if (mode != SpotPacket.Mode.MARK_POINT && RangefinderItem.charges(stack) <= 0) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 1.0F, 1.0F); + } else { + cooldown = COOLDOWN; + ModNetwork.sendToServer(new SpotPacket(mode)); + } + } + } + + static float fov(float fov) { + return isScoped() ? fov / zoom : fov; + } + + static void drawHud(GuiGraphics g, float delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + ItemStack stack = scope(player); + if (stack.isEmpty() || !mc.options.getCameraType().isFirstPerson()) { + return; + } + int w = g.guiWidth(); + int h = g.guiHeight(); + int size = Math.max(w, h); + g.blit(SCOPE, (w - size) / 2, (h - size) / 2, size, size, 0.0F, 0.0F, 256, 256, 256, 256); + + // The readouts sit on the scope texture's own labels: lay them out in its 512 wide space, + // centred, as the original did, and scale that with the scope. + float k = size / 512.0F; + g.pose().pushPose(); + g.pose().translate(w / 2.0F, h / 2.0F, 0.0F); + g.pose().scale(k, k, 1.0F); + drawReadouts(g, mc.font, player, stack); + g.pose().popPose(); + } + + /** In scope space: the centre of the view is at 0, 0. */ + private static void drawReadouts(GuiGraphics g, Font font, LocalPlayer player, ItemStack stack) { + int charge = RangefinderItem.charges(stack) * 100 / RangefinderItem.CHARGES; + // Over BATT. + g.drawString(font, String.format("%3d%%", charge), -100, 55, charge > 0 ? GREEN : RED); + if (charge <= 0) { + g.drawString(font, Component.translatable("hud.mcheli.rangefinder.empty"), -40, 55, RED); + } else { + // Over RANGE METERS. + Vec3 eye = player.getEyePosition(); + HitResult hit = player.level().clip(new ClipContext(eye, eye.add(player.getLookAngle().scale(RANGE)), + ClipContext.Block.OUTLINE, ClipContext.Fluid.ANY, player)); + String range = hit.getType() == HitResult.Type.MISS ? "----" : String.format("%4d", Math.round(hit.getLocation().distanceTo(eye))); + g.drawString(font, range, 40, 55, hit.getType() == HitResult.Type.MISS ? RED : GREEN); + } + // The spot cooldown bar, and the zoom above it. + g.fill(-100, 51, -70, 53, GREEN); + g.fill(-100, 51, -100 + cooldown / 2, 53, RED); + g.drawString(font, String.format("x%.1f", zoom), -100, 31, WHITE); + int y = 21; + for (SpotPacket.Mode m : SpotPacket.Mode.values()) { + if (m == mode) { + g.drawString(font, ">", 30, y, WHITE); + } + g.drawString(font, Component.translatable("hud.mcheli.rangefinder." + m.name().toLowerCase(Locale.ROOT)), 40, y, m == mode ? WHITE : GREY); + y += 10; + } + } +} From 6fe98d002ec24277768d1aa7ff12232edec9c3cc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:10 +0800 Subject: [PATCH 1174/1461] Add client/RuntimeAssets.java to the 1.20.6 shared sources --- .../mcheli/client/RuntimeAssets.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java new file mode 100644 index 00000000..dfcd0db1 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java @@ -0,0 +1,165 @@ +package com.jnhprojects.mcheli.client; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Vehicle textures, models and sounds that can change while the game runs, without a resource + * reload (F3 + T), which would stall the game for every pack loaded and is not available on a + * server at all. + * + *

Lookups try the resource manager first, so resource packs keep the last word, then the + * runtime roots: folders laid out like {@code assets/mcheli} that content can add at any time. + * Content packs and, later, the workshop register their folders here. + *

    + *
  • Textures found only in a runtime root are uploaded as a {@link DynamicTexture} the first + * time a vehicle draws them. {@link #refreshTextures} uploads every vehicle texture again + * from disk, which is how a content reload shows edited textures.
  • + *
  • Models are read through {@link #open} by the mesh cache, which a reload clears.
  • + *
  • Sounds found only in a runtime root are streamed from the file + * ({@link #soundFile}); see {@code RuntimeSoundInstance}.
  • + *
+ */ +public final class RuntimeAssets { + private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeAssets.class); + + /** Searched in order, so the pack loaded last comes first. Replaced as a whole. */ + private static volatile List roots = List.of(); + /** The pack open in the workshop, searched first whether the game has loaded it or not. */ + private static volatile @Nullable Path workshopRoot; + /** Every vehicle texture asked for, and whether we uploaded it ourselves. */ + private static final Map TEXTURES = new ConcurrentHashMap<>(); + + private RuntimeAssets() { + } + + /** + * Sets the folders laid out like {@code assets/mcheli} that are searched after the resource + * packs, the first one first. Content packs set theirs whenever the packs in use change. + */ + public static void setRoots(List folders) { + roots = List.copyOf(folders); + } + + /** Sets the {@code assets/mcheli} folder of the pack open in the workshop, or {@code null} for none. */ + public static void setWorkshopRoot(@Nullable Path folder) { + workshopRoot = folder; + } + + /** Opens an {@code mcheli} asset from the resource packs, or else from a runtime root. */ + public static @Nullable InputStream open(ResourceLocation id) throws IOException { + Optional resource = Minecraft.getInstance().getResourceManager().getResource(id); + if (resource.isPresent()) { + return resource.get().open(); + } + Path file = find(id.getPath()); + return file != null ? Files.newInputStream(file) : null; + } + + /** + * A texture ready to bind. The first request for a texture no resource pack has, but a runtime + * root does, uploads it. Cheap after the first call, so renderers can call it every frame. + */ + public static ResourceLocation texture(ResourceLocation id) { + TEXTURES.computeIfAbsent(id, key -> { + if (Minecraft.getInstance().getResourceManager().getResource(key).isPresent()) { + return false; + } + return upload(key); + }); + return id; + } + + /** + * Uploads every texture vehicles have drawn so far again, read fresh from disk. Resource pack + * textures are replaced too, until the next resource reload loads them the normal way. One that + * no longer exists anywhere, because its pack is gone, is released. + */ + public static void refreshTextures() { + for (Map.Entry entry : TEXTURES.entrySet()) { + boolean ours = upload(entry.getKey()); + if (!ours && entry.getValue()) { + Minecraft.getInstance().getTextureManager().release(entry.getKey()); + } + entry.setValue(ours); + } + } + + /** Uploads one texture again, read fresh from disk: the workshop just replaced its file. */ + public static void refreshTexture(ResourceLocation id) { + TEXTURES.put(id, upload(id)); + } + + private static boolean upload(ResourceLocation id) { + try (InputStream in = open(id)) { + if (in == null) { + return false; + } + NativeImage image = NativeImage.read(in); + Minecraft.getInstance().getTextureManager().register(id, new DynamicTexture(image)); + return true; + } catch (IOException e) { + LOGGER.warn("Could not load texture {}", id, e); + return false; + } + } + + /** + * Drops the textures we uploaded, so the texture manager loads them again the normal way. + * Called on resource reload. + */ + public static void clearTextures() { + TEXTURES.forEach((id, ours) -> { + if (ours) { + Minecraft.getInstance().getTextureManager().release(id); + } + }); + TEXTURES.clear(); + } + + /** + * The OGG file for a sound event of ours that no resource pack defines, such as {@code heli} + * from {@code sounds/heli.ogg} in a runtime root. Numbered variants ({@code heli1.ogg}) count. + */ + public static @Nullable Path soundFile(String event) { + Path file = find("sounds/" + event + ".ogg"); + return file != null ? file : find("sounds/" + event + "1.ogg"); + } + + private static @Nullable Path find(String path) { + Path workshop = workshopRoot; + if (workshop != null) { + Path file = inRoot(workshop, path); + if (file != null) { + return file; + } + } + for (Path root : roots) { + Path file = inRoot(root, path); + if (file != null) { + return file; + } + } + return null; + } + + private static @Nullable Path inRoot(Path root, String path) { + Path file = root.resolve(path).normalize(); + return file.startsWith(root) && Files.isRegularFile(file) ? file : null; + } +} From 2476474571c99389f76c19f3acc93490a7427329 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:10 +0800 Subject: [PATCH 1175/1461] Add client/TargetMarkers.java to the 1.20.6 shared sources --- .../mcheli/client/TargetMarkers.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java new file mode 100644 index 00000000..9f251d36 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/TargetMarkers.java @@ -0,0 +1,93 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.joml.Vector4f; +import org.jspecify.annotations.Nullable; + +import java.util.Map; + +/** + * {@code MCH_GuiTargetMarker}: the team's spotted targets and marked point on screen. A target + * gets a triangle over its head with its distance; one off screen gets a diamond at the edge on + * its side. The marked point gets a diamond with its distance and coordinates. + */ +final class TargetMarkers { + private static final int TARGET = 0xFFFF4040; + private static final int POINT = 0xFFFFD000; + private static final int EDGE = 12; + + private TargetMarkers() { + } + + static void tick() { + Markers.tick(); + } + + static void draw(GuiGraphics g, float delta) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || mc.player == null) { + return; + } + float partialTicks = delta; + for (Map.Entry spotted : Markers.spotted().entrySet()) { + Entity entity = mc.level.getEntity(spotted.getKey()); + if (entity == null || entity == mc.player || !entity.isAlive()) { + continue; + } + Vec3 top = entity.getPosition(partialTicks).add(0.0, entity.getBbHeight() + 0.5, 0.0); + mark(g, mc, top, "â–¼", TARGET, "%.0fm".formatted(mc.player.distanceTo(entity)), null); + } + BlockPos point = Markers.point(); + if (point != null) { + Vec3 at = Vec3.atCenterOf(point).add(0.0, 1.0, 0.0); + String coordinates = "%d %d %d".formatted(point.getX(), point.getY(), point.getZ()); + mark(g, mc, at, "â—†", POINT, "%.0fm".formatted(Math.sqrt(mc.player.distanceToSqr(at))), coordinates); + } + } + + private static void mark(GuiGraphics g, Minecraft mc, Vec3 at, String symbol, int color, String distance, @Nullable String extra) { + Camera camera = mc.gameRenderer.getMainCamera(); + Vector3fc forward = camera.getLookVector(); + Vec3 offset = at.subtract(camera.getPosition()); + boolean ahead = offset.x * forward.x() + offset.y * forward.y() + offset.z * forward.z() > 0.0; + // 26.x has projectPointToScreen for this; the view bobbing and the sprint widening are left out. + Vector3f view = new Vector3f((float) offset.x, (float) offset.y, (float) offset.z).rotate(camera.rotation().conjugate(new Quaternionf())); + Vector4f clip = mc.gameRenderer.getProjectionMatrix(ClientHooks.fov(mc.options.fov().get())) + .transform(new Vector4f(view, 1.0F)); + Vec3 ndc = new Vec3(clip.x() / clip.w(), clip.y() / clip.w(), clip.z() / clip.w()); + int w = g.guiWidth(); + int h = g.guiHeight(); + double x = (ndc.x + 1.0) / 2.0 * w; + double y = (1.0 - ndc.y) / 2.0 * h; + Font font = mc.font; + if (ahead && x >= 0 && x <= w && y >= 0 && y <= h) { + g.drawCenteredString(font, symbol, (int) x, (int) y - 4, color); + g.drawCenteredString(font, distance, (int) x, (int) y + 6, color); + if (extra != null) { + g.drawCenteredString(font, extra, (int) x, (int) y + 16, color); + } + return; + } + // Behind or off to the side: point the way along the screen edge. + double dx = ahead ? x - w / 2.0 : w / 2.0 - x; + double dy = ahead ? y - h / 2.0 : h / 2.0 - y; + double scale = Math.max(Math.abs(dx) / (w / 2.0 - EDGE), Math.abs(dy) / (h / 2.0 - EDGE)); + if (scale <= 0.0) { + return; + } + int ex = Mth.clamp((int) (w / 2.0 + dx / scale), EDGE, w - EDGE); + int ey = Mth.clamp((int) (h / 2.0 + dy / scale), EDGE, h - EDGE); + g.drawCenteredString(font, "â—†", ex, ey - 4, color); + } +} From df95cde0b7f05ff46ed8b3252a825d9c7d199538 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:11 +0800 Subject: [PATCH 1176/1461] Add client/VehicleCamera.java to the 1.20.6 shared sources --- .../mcheli/client/VehicleCamera.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java new file mode 100644 index 00000000..5c6437bb --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/VehicleCamera.java @@ -0,0 +1,126 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.PostChain; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; + +import java.util.function.Consumer; + +/** + * The local crew member's sight, {@code MCH_Camera}: zoom for vehicles with a {@code CameraZoom} + * above one, and night vision and thermal view for those with {@code EnableNightVision}. Both + * reset whenever the player changes seat or leaves the vehicle. + */ +public final class VehicleCamera { + /** The original's camera modes; the HUD reads the ordinal as {@code cam_mode}. */ + public enum Mode { + NORMAL, + NIGHT_VISION, + /** Night vision seen through a white hot filter, the original's "pencil" shader mode. */ + THERMAL + } + + /** The post effect of the thermal view, {@code assets/mcheli/shaders/post/thermal.json}. */ + private static final ResourceLocation THERMAL_EFFECT = new ResourceLocation(MCHeli.MOD_ID, "shaders/post/thermal.json"); + + private static float zoom = 1.0F; + private static Mode mode = Mode.NORMAL; + + private VehicleCamera() { + } + + public static float zoom() { + return zoom; + } + + public static Mode mode() { + return mode; + } + + /** Back to the plain view, on a seat change or after leaving the vehicle. */ + static void reset() { + zoom = 1.0F; + mode = Mode.NORMAL; + } + + /** Keeps the thermal filter loaded while the thermal view is on, and only then. */ + static void tick() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + return; + } + PostChain current = mc.gameRenderer.currentEffect(); + boolean active = current != null && current.getName().equals(THERMAL_EFFECT.toString()); + if (mode == Mode.THERMAL && !active) { + ClientHooks.postEffects().load(THERMAL_EFFECT); + } else if (mode != Mode.THERMAL && active) { + mc.gameRenderer.shutdownEffect(); + } + } + + /** + * {@code zoomCamera}: each press doubles the zoom up to the vehicle's {@code CameraZoom}, and + * the press after that goes back to one. The caller checks that the seat looks through a sight. + */ + static void onZoomKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + zoomStep(player, def != null && !vehicle.isDestroyed() ? def.cameraZoom : 1); + } + + /** One press of the zoom key on a sight that zooms up to {@code max}. */ + static void zoomStep(LocalPlayer player, int max) { + if (max <= 1) { + refuse(player); + return; + } + zoom = zoom >= max - 0.01F ? 1.0F : Math.min(zoom * 2.0F, max); + player.level().playLocalSound(player, ModSounds.get("zoom"), SoundSource.PLAYERS, 0.5F, 1.0F); + } + + /** + * {@code switchCameraMode}: steps from the plain view to night vision, to thermal view and + * back. The server applies night vision as an effect in both of the last two; the thermal + * filter is drawn here. + */ + static void onCameraModeKey(LocalPlayer player, AircraftEntity vehicle) { + AircraftDefinition def = vehicle.definition(); + if (def == null || !def.nightVision || vehicle.isDestroyed()) { + refuse(player); + return; + } + nextMode(player, on -> ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), + on ? CrewActionPacket.Action.NIGHT_VISION_ON : CrewActionPacket.Action.NIGHT_VISION_OFF))); + } + + /** + * Steps to the next camera mode on a sight that has night vision. {@code nightVision} asks + * the server to switch the effect on or off, only when that changes. + */ + static void nextMode(LocalPlayer player, Consumer nightVision) { + Mode next = Mode.values()[(mode.ordinal() + 1) % Mode.values().length]; + if (mode == Mode.NORMAL || next == Mode.NORMAL) { + nightVision.accept(next != Mode.NORMAL); + } + mode = next; + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + + /** The view's field of view, narrowed by the zoom. */ + public static float fov(float fov) { + return zoom > 1.0F ? fov / zoom : fov; + } + + /** {@code playSoundNG}: the key does nothing in this seat. */ + private static void refuse(LocalPlayer player) { + player.level().playLocalSound(player, ModSounds.get("ng"), SoundSource.PLAYERS, 0.5F, 1.0F); + } +} From 6e11800fb2069dbcc2921afd7d36b9c6518ce06c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:11 +0800 Subject: [PATCH 1177/1461] Add client/VehiclePicker.java to the 1.20.6 shared sources --- .../mcheli/client/VehiclePicker.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java new file mode 100644 index 00000000..9507412f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.BoardVehiclePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Right-clicking anywhere on a vehicle's hull boards it. + * + *

Vanilla only picks an entity by its main hit box, which for most MCHeli vehicles is the + * default two by 0.7 block box at their feet; the hull's real shape lives in its extra + * {@code BoundingBox} lines. In the original those boxes were clickable too. Inflating the main box + * instead would change how the vehicle collides with terrain, so the use key looks for them here. + */ +final class VehiclePicker { + private VehiclePicker() { + } + + static boolean onUseItem(InteractionHand hand) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || hand != InteractionHand.MAIN_HAND || player.getVehicle() instanceof AircraftEntity) { + return false; + } + // Vanilla already handles a click on the main box. + if (mc.hitResult instanceof EntityHitResult hit && hit.getEntity() instanceof AircraftEntity) { + return false; + } + Hit hit = pick(player, mc.hitResult); + if (hit == null) { + return false; + } + ModNetwork.sendToServer(new BoardVehiclePacket(hit.vehicle.getId(), hit.location)); + return true; + } + + private record Hit(AircraftEntity vehicle, Vec3 location) { + } + + /** The nearest vehicle hull along the view, closer than the block or entity vanilla picked. */ + private static @Nullable Hit pick(Player player, @Nullable HitResult vanilla) { + double range = player.entityInteractionRange(); + Vec3 eye = player.getEyePosition(); + Vec3 to = eye.add(player.getViewVector(1.0F).scale(range)); + double limit = vanilla != null && vanilla.getType() != HitResult.Type.MISS ? eye.distanceToSqr(vanilla.getLocation()) : range * range; + Hit best = null; + for (AircraftEntity vehicle : player.level().getEntitiesOfClass(AircraftEntity.class, new AABB(eye, to).inflate(16.0))) { + Optional at = vehicle.clipHitBoxes(eye, to, 0.2); + vehicle.consumeHitBoxFactor(); + if (at.isPresent() && eye.distanceToSqr(at.get()) < limit) { + limit = eye.distanceToSqr(at.get()); + best = new Hit(vehicle, at.get()); + } + } + return best; + } +} From 84b223925391e6ec3289920a7929d724ad0fb6d7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:11 +0800 Subject: [PATCH 1178/1461] Add client/WeaponController.java to the 1.20.6 shared sources --- .../mcheli/client/WeaponController.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java new file mode 100644 index 00000000..d46804e6 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java @@ -0,0 +1,187 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.FireWeaponPacket; +import com.jnhprojects.mcheli.network.LockWarningPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The crew member's trigger: fires, switches weapons and modes, drops flares and resupplies. + * + *

Like the original, the client fires first. Its copy of the weapon decides whether the shot + * can happen (ready, loaded, locked on, a target in sight) and works out the option values; only + * then is the server asked, which fires the real projectile. + */ +final class WeaponController { + private WeaponController() { + } + + private record Seat(AircraftEntity vehicle, VehicleWeapons weapons, int seat) { + } + + private static @Nullable Seat seat(@Nullable LocalPlayer player) { + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle) || vehicle.definition() == null) { + return null; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (weapons == null || seat < 0 || seat >= vehicle.definition().seatCount()) { + return null; + } + return new Seat(vehicle, weapons, seat); + } + + /** The use key is the trigger while the seat has a weapon, so it must not also use items. */ + static boolean onUseItem() { + Seat s = seat(Minecraft.getInstance().player); + return s != null && s.weapons.selected(s.seat) >= 0; + } + + static boolean onPickBlock() { + Seat s = seat(Minecraft.getInstance().player); + if (s == null || s.weapons.sets().isEmpty()) { + return false; + } + send(s.vehicle, Minecraft.getInstance().player != null && Minecraft.getInstance().player.isShiftKeyDown() + ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + return true; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + Seat s = seat(player); + if (s == null) { + return; + } + while (KeyBindings.SWITCH_WEAPON.consumeClick()) { + send(s.vehicle, player.isShiftKeyDown() ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + WeaponSet set = s.weapons.current(s.seat); + if (set != null && set.switchMode()) { + send(s.vehicle, CrewActionPacket.Action.WEAPON_MODE); + click(); + } + } + while (KeyBindings.FLARE.consumeClick()) { + if (s.seat <= 1 && s.weapons.flares.use()) { + send(s.vehicle, CrewActionPacket.Action.FLARE); + click(); + } + } + + if (mc.screen == null && mc.options.keyUse.isDown() && !s.vehicle.isDestroyed()) { + fire(s, player); + } + } + + private static void fire(Seat s, LocalPlayer player) { + int id = s.weapons.selected(s.seat); + if (id < 0 || s.weapons.operatorSeat(id) != s.seat) { + return; + } + Shot shot = new Shot(s.vehicle, player); + boolean fired = s.weapons.use(player, s.seat, shot, (set, mount, sh) -> clientShot(s, id, set, sh, player)); + if (fired) { + ModNetwork.sendToServer(new FireWeaponPacket(s.vehicle.getId(), id, shot.option1, shot.option2)); + } + } + + /** The client half of each weapon's {@code shot}: can it fire, and with which options. */ + private static boolean clientShot(Seat s, int id, WeaponSet set, Shot shot, LocalPlayer player) { + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_2, ROCKET, MARKER_ROCKET -> { + shot.option1 = set.mode(); + yield true; + } + case AA_MISSILE, AT_MISSILE -> { + Guidance g = s.weapons.guidance(id); + if (g == null) { + yield false; + } + boolean locked = g.lock(player, viewPoint(player)); + if (g.shouldNotifyTarget() && g.target() != null) { + ModNetwork.sendToServer(new LockWarningPacket(g.target().getId())); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (!locked || g.lastLocked() == null) { + yield false; + } + shot.option1 = g.lastLocked().getId(); + shot.option2 = set.mode(); + yield true; + } + case TV_MISSILE -> { + shot.option1 = set.mode(); + shot.option2 = 0; + yield true; + } + case AS_MISSILE -> landAhead(s.vehicle, player, 1.62, 200.0, false) != null; + case CAS -> landAhead(s.vehicle, player, 2.0, 80.0, false) != null; + case TORPEDO -> !w.guidedTorpedo || landAhead(player, player, 0.0, 100.0, true) != null; + case DUMMY, SMOKE -> false; + case null -> false; + default -> true; + }; + } + + /** + * Where the player sees from: in first person the camera, which CrewController moves to gunner + * sights and launcher cameras; otherwise the player's eyes. + */ + private static Vec3 viewPoint(LocalPlayer player) { + Minecraft mc = Minecraft.getInstance(); + return mc.options.getCameraType().isFirstPerson() ? mc.gameRenderer.getMainCamera().getPosition() : player.getEyePosition(); + } + + /** A block along the player's view: dry ground for strike weapons, water for guided torpedoes. */ + private static @Nullable BlockHitResult landAhead(Entity origin, LocalPlayer player, double eyeHeight, double range, boolean water) { + Vec3 from = origin.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(Vec3.directionFromRotation(player.getXRot(), player.getYRot()).scale(range)); + BlockHitResult hit = player.level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, player)); + if (hit.getType() != HitResult.Type.BLOCK) { + return null; + } + boolean wet = false; + BlockPos pos = hit.getBlockPos(); + for (Direction d : Direction.values()) { + wet |= player.level().getFluidState(pos.relative(d)).is(FluidTags.WATER); + } + return wet == water ? hit : null; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + private static void click() { + LocalPlayer player = Minecraft.getInstance().player; + if (player != null) { + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + } +} From 85ce473ccce72b3b4fae597ef30cde6e8434a662 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:12 +0800 Subject: [PATCH 1179/1461] Add client/WeaponHud.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/client/WeaponHud.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java new file mode 100644 index 00000000..d08d5bee --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java @@ -0,0 +1,122 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; + +/** + * The vehicle HUD layer. In first person it runs the seat's HUD script (the {@code HUD} line of + * the definition, or the kind's default), or {@code tv_missile} while flying a TV missile, as the + * original did; a seat whose HUD is {@code none} gets nothing. Where no script draws (third + * person, a destroyed vehicle, a script that does not exist) it shows a plain text readout of hit + * points, ammunition, reload, heat, lock and flares. + */ +final class WeaponHud { + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int RED = 0xFFFF5050; + private static final int GREEN = 0xFF60FF60; + private static final String NO_HUD = "none"; + + private WeaponHud() { + } + + /** A pilot in gunner mode gets the gunner seat's HUD, as in the original. */ + private static int hudSeat(AircraftEntity vehicle, int seat) { + return seat == 0 && vehicle.isGunnerMode() ? 1 : seat; + } + + static void draw(GuiGraphics g, float delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + AircraftDefinition def = vehicle.definition(); + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (def == null || weapons == null || seat < 0 || seat >= def.seatCount()) { + return; + } + if (mc.options.getCameraType().isFirstPerson() && !vehicle.isDestroyed()) { + String hud = CrewController.tvCamera(vehicle, seat) != null ? "tv_missile" + : hudSeat(vehicle, seat) < def.huds.size() ? def.huds.get(hudSeat(vehicle, seat)) : null; + if (NO_HUD.equals(hud)) { + // The original's way of saying a seat has no HUD at all. + return; + } + if (hud != null && HudRenderer.draw(g, vehicle, def, player, seat, hud, delta)) { + return; + } + } + Font font = mc.font; + int x = g.guiWidth() - 160; + int y = g.guiHeight() - 92; + + float hp = (float) vehicle.hp() / def.maxHp; + int hpColor = vehicle.isDestroyed() ? RED : hp < 0.5F ? AMBER : GREEN; + g.drawString(font, Component.translatable("hud.mcheli.hp", vehicle.hp(), def.maxHp), x, y, hpColor); + y += 11; + + WeaponSet set = weapons.current(seat); + if (set != null) { + g.drawString(font, set.displayName(), x, y, WHITE); + y += 11; + Component ammo = vehicle.hasInfiniteAmmo(player) && set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo_infinite", set.ammo()) + : set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo", set.ammo(), set.restAmmo()) + : Component.translatable("hud.mcheli.ammo_none"); + g.drawString(font, ammo, x, y, set.ammo() == 0 && set.magazineSize() > 0 ? RED : WHITE); + y += 11; + Component status = null; + int color = AMBER; + if (set.isReloading()) { + status = Component.translatable("hud.mcheli.reloading"); + } else if (set.isOverheated()) { + status = Component.translatable("hud.mcheli.overheat"); + color = RED; + } else if (set.isInPreparation()) { + status = Component.translatable("hud.mcheli.preparing"); + } else if (set.magazineSize() > 0 && set.ammo() == 0 && set.restAmmo() == 0) { + status = Component.translatable("hud.mcheli.resupply", KeyBindings.VEHICLE_SCREEN.getTranslatedKeyMessage()); + color = RED; + } + Guidance lock = weapons.guidance(weapons.selected(seat)); + if (status == null && lock != null && lock.lockCount() > 0) { + status = lock.isLocked() + ? Component.translatable("hud.mcheli.locked") + : Component.translatable("hud.mcheli.locking", lock.lockCount() * 100 / Math.max(lock.lockCountMax(), 1)); + color = lock.isLocked() ? RED : AMBER; + } + if (status != null) { + g.drawString(font, status, x, y, color); + y += 11; + } + if (set.definition().maxHeatCount > 0) { + int width = 100; + int filled = Math.min(width, set.currentHeat * width / set.definition().maxHeatCount); + g.fill(x, y + 2, x + width, y + 5, 0x80000000); + g.fill(x, y + 2, x + filled, y + 5, set.isOverheated() ? RED : AMBER); + y += 8; + } + } else if (!weapons.sets().isEmpty()) { + g.drawString(font, Component.translatable("hud.mcheli.no_weapon"), x, y, WHITE); + y += 11; + } + if (seat <= 1 && weapons.flares.isFitted()) { + Component flare = vehicle.isFlareActive() ? Component.translatable("hud.mcheli.flare_active") + : weapons.flares.isPreparing() ? Component.translatable("hud.mcheli.flare_wait") + : Component.translatable("hud.mcheli.flare_ready", KeyBindings.FLARE.getTranslatedKeyMessage()); + g.drawString(font, flare, x, y, vehicle.isFlareActive() ? GREEN : WHITE); + } + } +} From c89234a502183598b40459da14f5f4f1746f20d7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:12 +0800 Subject: [PATCH 1180/1461] Add client/hud/HudCanvas.java to the 1.20.6 shared sources --- .../mcheli/client/hud/HudCanvas.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java new file mode 100644 index 00000000..ba8976c6 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java @@ -0,0 +1,164 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.ResourceLocation; + +/** + * The drawing primitives of {@code MCH_HudItem}, on the 1.20.6 GUI. + * + *

The original drew GL lines, points and quads at fractional GUI coordinates. The GUI now only + * takes whole numbers, but every element keeps a copy of the pose, so each primitive is a unit + * square stretched and turned into place. Lines are one GUI unit thick, which is what + * {@code glLineWidth(scaleFactor)} gave; blips are two units square. + */ +final class HudCanvas { + private static final float LINE_WIDTH = 1.0F; + private static final float POINT_SIZE = 2.0F; + + private final GuiGraphics g; + private final Font font; + + HudCanvas(GuiGraphics g, Font font) { + this.g = g; + this.font = font; + } + + PoseStack pose() { + return g.pose(); + } + + /** Turns everything drawn until the matching {@code pop} by {@code degrees} about a point. */ + void pushRotation(double x, double y, float degrees) { + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) x, (float) y, 0.0F); + pose.mulPose(Axis.ZP.rotation((float) Math.toRadians(degrees))); + pose.translate((float) -x, (float) -y, 0.0F); + } + + void pop() { + g.pose().popPose(); + } + + void line(double x0, double y0, double x1, double y1, int color) { + double dx = x1 - x0; + double dy = y1 - y0; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + return; + } + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) x0, (float) y0, 0.0F); + pose.mulPose(Axis.ZP.rotation((float) Math.atan2(dy, dx))); + pose.translate(0.0F, -LINE_WIDTH / 2.0F, 0.0F); + pose.scale((float) length, LINE_WIDTH, 1.0F); + g.fill(0, 0, 1, 1, color); + pose.popPose(); + } + + /** {@code GL_LINES}: x, y pairs taken two points at a time. */ + void lines(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 4) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_STRIP}. */ + void strip(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 2) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_LOOP}. */ + void loop(double[] p, int color) { + strip(p, color); + if (p.length >= 4) { + line(p[p.length - 2], p[p.length - 1], p[0], p[1], color); + } + } + + /** + * {@code GL_LINES} with {@code glLineStipple(factor, pattern)}: each segment starts the 16 bit + * pattern afresh, lowest bit first, every bit covering {@code factor} GUI units. + */ + void stippled(double[] p, int color, int factor, int pattern) { + double step = Math.max(factor, 1); + for (int i = 0; i + 3 < p.length; i += 4) { + double dx = p[i + 2] - p[i]; + double dy = p[i + 3] - p[i + 1]; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + continue; + } + double ux = dx / length; + double uy = dy / length; + double runStart = -1.0; + int bit = 0; + for (double t = 0.0; t < length; t += step, bit = (bit + 1) & 15) { + boolean on = (pattern >> bit & 1) != 0; + if (on && runStart < 0.0) { + runStart = t; + } else if (!on && runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i] + ux * t, p[i + 1] + uy * t, color); + runStart = -1.0; + } + } + if (runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i + 2], p[i + 3], color); + } + } + } + + void rect(double x0, double y0, double x1, double y1, int color) { + double left = Math.min(x0, x1); + double top = Math.min(y0, y1); + double width = Math.abs(x1 - x0); + double height = Math.abs(y1 - y0); + if (width <= 0.0 || height <= 0.0) { + return; + } + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) left, (float) top, 0.0F); + pose.scale((float) width, (float) height, 1.0F); + g.fill(0, 0, 1, 1, color); + pose.popPose(); + } + + void point(double x, double y, int color) { + rect(x - POINT_SIZE / 2.0, y - POINT_SIZE / 2.0, x + POINT_SIZE / 2.0, y + POINT_SIZE / 2.0, color); + } + + /** + * {@code drawTexture}: texels {@code u, v, uWidth, vHeight} of a {@code textureWidth} by + * {@code textureHeight} image, drawn into a box and turned about its centre. Drawn untinted, + * as the original reset the colour to white first. + */ + void texture(ResourceLocation texture, double left, double top, double width, double height, double u, double v, + double uWidth, double vHeight, float rotation, int textureWidth, int textureHeight) { + if (width == 0.0 || height == 0.0) { + return; + } + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) (left + width / 2.0), (float) (top + height / 2.0), 0.0F); + pose.mulPose(Axis.ZP.rotation((float) Math.toRadians(rotation))); + pose.translate((float) (-width / 2.0), (float) (-height / 2.0), 0.0F); + pose.scale((float) width, (float) height, 1.0F); + g.blit(texture, 0, 0, 1, 1, (float) u, (float) v, (int) Math.round(uWidth), (int) Math.round(vHeight), textureWidth, textureHeight); + pose.popPose(); + } + + void text(String text, int x, int y, int color, boolean centered) { + if (centered) { + g.drawCenteredString(font, text, x, y, color); + } else { + g.drawString(font, text, x, y, color); + } + } +} From dd91013bbb6be5ca9cf11043c2ef137eea9d0028 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:13 +0800 Subject: [PATCH 1181/1461] Add client/hud/HudRenderer.java to the 1.20.6 shared sources --- .../mcheli/client/hud/HudRenderer.java | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java new file mode 100644 index 00000000..5ed457aa --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java @@ -0,0 +1,354 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.hud.Expression; +import com.jnhprojects.mcheli.hud.HudCommand; +import com.jnhprojects.mcheli.hud.HudScript; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.IllegalFormatException; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Runs HUD scripts, the drawing half of {@code MCH_Hud} and the {@code MCH_HudItem} subclasses. + * + *

An {@code If} hides every line up to its {@code EndIf}; {@code Exit} ends the current script + * only, so a {@code Call}er carries on; the colour carries into called scripts and over to the next + * frame. A script cannot call itself, directly or through another, the same guard the original had. + * A line that fails to format is skipped and reported once, where the original crashed. + */ +public final class HudRenderer { + private static final Logger LOGGER = LoggerFactory.getLogger(HudRenderer.class); + private static final int AZIMUTH_RED = 0xFFFF0000; + private static final String[] AZIMUTH_8 = {"S", "SW", "W", "NW", "N", "NE", "E", "SE"}; + /** {@code glLineStipple} pattern of the original's dashed lines. */ + private static final int DASHES = 0xCCCC; + + private static final HudState STATE = new HudState(); + private static final Set REPORTED = new HashSet<>(); + + private final HudCanvas canvas; + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final LocalPlayer player; + private final double centerX; + private final double centerY; + private final Set running = new HashSet<>(); + + private HudRenderer(HudCanvas canvas, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, double width, double height) { + this.canvas = canvas; + this.vehicle = vehicle; + this.def = def; + this.player = player; + this.centerX = width / 2.0; + this.centerY = height / 2.0; + } + + public static void tick() { + LocalPlayer player = Minecraft.getInstance().player; + STATE.tick(player != null && player.getVehicle() instanceof AircraftEntity vehicle ? vehicle : null); + } + + /** Fed by the pilot's mouse steering; moves the control stick indicator. */ + public static void onPilotLook(float dYaw, float dPitch) { + STATE.onPilotLook(dYaw, dPitch); + } + + /** + * Draws a HUD script for the local player's seat. + * + * @return false if no script by that name is loaded, so the caller can draw something else + */ + public static boolean draw(GuiGraphics g, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, + int seat, String hud, float partialTicks) { + HudScript script = HudScripts.INSTANCE.get(hud); + if (script == null) { + return false; + } + double width = g.guiWidth(); + double height = g.guiHeight(); + STATE.update(vehicle, def, player, seat, width, height, partialTicks); + HudRenderer renderer = new HudRenderer(new HudCanvas(g, Minecraft.getInstance().font), vehicle, def, player, width, height); + renderer.run(script); + return true; + } + + private double eval(Expression e) { + return e.eval(STATE.vars); + } + + private double x(Expression e) { + return centerX + eval(e); + } + + private double y(Expression e) { + return centerY + eval(e); + } + + private double[] points(List coordinates) { + double[] out = new double[coordinates.size()]; + for (int i = 0; i + 1 < out.length; i += 2) { + out[i] = x(coordinates.get(i)); + out[i + 1] = y(coordinates.get(i + 1)); + } + return out; + } + + private void run(HudScript script) { + if (!running.add(script.name())) { + return; + } + try { + boolean hidden = false; + for (HudCommand command : script.commands()) { + if (command instanceof HudCommand.If c) { + hidden = eval(c.condition()) == 0.0; + continue; + } + if (command instanceof HudCommand.EndIf) { + hidden = false; + continue; + } + if (hidden) { + continue; + } + if (command instanceof HudCommand.Exit) { + return; + } + execute(script, command); + } + } finally { + running.remove(script.name()); + } + } + + private void execute(HudScript script, HudCommand command) { + int color = STATE.color; + switch (command) { + case HudCommand.Color c -> { + STATE.color = (int) (long) eval(c.argb()); + STATE.vars.set(HudVariable.COLOR, STATE.color & 0xFFFFFFFFL); + } + case HudCommand.Call c -> { + HudScript called = HudScripts.INSTANCE.get(c.hud()); + if (called != null) { + run(called); + } + } + case HudCommand.Text c -> text(script, c, color); + case HudCommand.Texture c -> { + ResourceLocation texture = new ResourceLocation(MCHeli.MOD_ID, "textures/gui/" + c.name() + ".png"); + int[] size = HudScripts.INSTANCE.textureSize(Minecraft.getInstance().getResourceManager(), texture); + canvas.texture(texture, x(c.left()), y(c.top()), eval(c.width()), eval(c.height()), + eval(c.u()), eval(c.v()), eval(c.uWidth()), eval(c.vHeight()), (float) eval(c.rotation()), size[0], size[1]); + } + case HudCommand.Rect c -> { + // The original truncated the size, not the position. + double left = x(c.left()); + double top = y(c.top()); + canvas.rect(left, top, left + (int) eval(c.width()), top + (int) eval(c.height()), color); + } + case HudCommand.Line c -> canvas.strip(points(c.points()), color); + case HudCommand.LineStipple c -> canvas.stippled(points(c.points()), color, (int) eval(c.factor()), (int) eval(c.pattern())); + case HudCommand.Radar c -> radar(c, color); + case HudCommand.Graduation c -> graduation(c, color); + case HudCommand.CameraRot c -> cameraRot(x(c.x()), y(c.y()), color); + case HudCommand.If c -> { + } + case HudCommand.EndIf c -> { + } + case HudCommand.Exit c -> { + } + } + } + + /** {@code MCH_HudItemString}: skipped while a weapon value is shown and no weapon (or magazine) applies. */ + private void text(HudScript script, HudCommand.Text c, int color) { + Object[] args = new Object[c.args().size()]; + for (int i = 0; i < args.length; i++) { + TextArg arg = c.args().get(i); + if ((arg.needsWeapon() && !STATE.hasWeapon()) || (arg.needsMagazine() && !STATE.hasMagazine())) { + return; + } + args[i] = STATE.text(arg, vehicle, def, player); + } + String text; + try { + text = String.format(Locale.ROOT, c.format(), args); + } catch (IllegalFormatException e) { + String key = script.name() + ':' + c.line(); + if (REPORTED.add(key)) { + LOGGER.warn("HUD {}.txt line {}: cannot format \"{}\": {}", script.name(), c.line(), c.format(), e.getMessage()); + } + return; + } + canvas.text(text, (int) x(c.x()), (int) y(c.y()), color, c.centered()); + } + + /** {@code MCH_HudItemRadar}: blips at half scale for a 64 unit box, turned with the view. */ + private void radar(HudCommand.Radar c, int color) { + List blips = c.enemies() ? STATE.enemyBlips : STATE.entityBlips; + if (blips.isEmpty()) { + return; + } + double rotation = Math.toRadians(eval(c.rotation())); + double left = x(c.left()); + double top = y(c.top()); + double w = eval(c.width()); + double h = eval(c.height()); + double cos = Math.cos(rotation); + double sin = Math.sin(rotation); + for (double[] blip : blips) { + double px = blip[0] / 2.0 * w / 64.0; + double py = blip[1] / 2.0 * h / 64.0; + double rx = px * cos - py * sin; + double ry = px * sin + py * cos; + if (rx > -w / 2.0 && rx < w / 2.0 && ry > -h / 2.0 && ry < h / 2.0) { + canvas.point(rx + left + w / 2.0, ry + top + h / 2.0, color); + } + } + } + + private void graduation(HudCommand.Graduation c, int color) { + int x = (int) x(c.x()); + int y = (int) y(c.y()); + double angle = eval(c.angle()); + canvas.pushRotation(x, y, (float) eval(c.roll())); + switch (c.kind()) { + case YAW -> headingTape(angle, color, x, y); + case PITCH_1 -> pitchScale(angle, color, x, y); + case PITCH_2 -> pitchLadder(angle, color, x, y, true); + case PITCH_3 -> pitchLadder(angle, color, x, y, false); + } + canvas.pop(); + } + + /** {@code drawCommonGraduationYaw}: a tick every degree, labels every three, compass points every 45. */ + private void headingTape(double playerYaw, int color, int posX, int posY) { + double yaw = HudState.rotate360(playerYaw); + posX -= 90; + double[] line = new double[76]; + int offset = (int) (yaw * 10.0) % 10; + int degree = (int) yaw - 9; + for (int i = 0; i < line.length / 4; i++, degree++) { + int azimuthX = posX + i * 10 - offset; + line[i * 4] = azimuthX; + line[i * 4 + 1] = posY; + line[i * 4 + 2] = azimuthX; + line[i * 4 + 3] = posY + (degree % 45 == 0 ? 15 : degree % 3 == 0 ? 10 : 5); + if (degree % 45 == 0) { + canvas.text(AZIMUTH_8[Math.floorMod(degree, 360) / 45], azimuthX, posY - 10, AZIMUTH_RED, true); + } else if (degree % 3 == 0) { + int heading = degree + 180; + if (heading < 0) { + heading += 360; + } + if (heading > 360) { + heading -= 360; + } + canvas.text(Integer.toString(heading), azimuthX, posY - 10, color, true); + } + } + canvas.lines(line, color); + } + + /** {@code drawCommonGraduationPitch1}: a scale of ticks on both sides that slides with the pitch. */ + private void pitchScale(double playerPitch, int color, int posX, int posY) { + int pitch = (int) playerPitch % 360; + int y = (int) (playerPitch * 10.0 % 10.0); + if (y < 0) { + y += 10; + } + int left = posX - 100; + int right = posX + 100; + posY -= 80; + double[] line = new double[144]; + int p = playerPitch < 0.0 && y != 0 ? pitch - 9 : pitch - 8; + for (int i = 0; i < line.length / 8; i++, p++) { + int outer = p % 3 == 0 ? 15 : 5; + double rowY = posY + i * 10 - y; + line[i * 8] = left - outer; + line[i * 8 + 1] = rowY; + line[i * 8 + 2] = left; + line[i * 8 + 3] = rowY; + line[i * 8 + 4] = right + outer; + line[i * 8 + 5] = rowY; + line[i * 8 + 6] = right; + line[i * 8 + 7] = rowY; + } + canvas.lines(line, color); + canvas.strip(new double[]{left - 25, posY - 90, left, posY - 90, left, posY + 90, left - 25, posY + 90}, color); + canvas.strip(new double[]{right + 25, posY - 90, right, posY - 90, right, posY + 90, right + 25, posY + 90}, color); + } + + /** + * {@code drawCommonGraduationPitch2}: a ladder with a rung every five degrees, dashed below + * the horizon. {@code wide} is {@code Pitch2} with five rungs; {@code Pitch3} has three, twice as far apart. + */ + private void pitchLadder(double playerPitch, int color, int posX, int posY, boolean wide) { + playerPitch = -playerPitch; + int pitchStep = (int) playerPitch / 5 * 5; + int start = wide ? 0 : 1; + int end = wide ? 5 : 4; + int spacing = wide ? 1 : 2; + double rest = playerPitch % 5.0; + double[] line = new double[8]; + for (int i = start; i < end; i++) { + int pitch = -(-pitchStep - 10 + i * 5); + int halfWidth = pitch != 0 ? 50 : 100; + int y = posY + (int) (-60 * spacing + rest * 6.0 * spacing + i * 30 * spacing); + line[0] = posX - halfWidth; + line[1] = y + (pitch == 0 ? 0 : pitch > 0 ? 2 : -2); + line[2] = posX - 50; + line[3] = y; + line[4] = posX + halfWidth; + line[5] = line[1]; + line[6] = posX + 50; + line[7] = y; + canvas.lines(line, color); + line[0] = posX - 50; + line[1] = y; + line[2] = posX - 30; + line[3] = y; + line[4] = posX + 50; + line[5] = y; + line[6] = posX + 30; + line[7] = y; + if (pitch >= 0) { + canvas.lines(line, color); + } else { + canvas.stippled(line, color, 1, DASHES); + } + if (pitch != 0) { + canvas.text(Integer.toString(pitch), posX - 60, y - 4, color, true); + canvas.text(Integer.toString(pitch), posX + 60, y - 4, color, true); + } + } + } + + /** {@code MCH_HudItemCameraRot}: a frame with a box showing where the crew's view points against the hull. */ + private void cameraRot(double posX, double posY, int color) { + canvas.loop(new double[]{posX - 21, posY - 11, posX + 21, posY - 11, posX + 21, posY + 11, posX - 21, posY + 11}, color); + canvas.stippled(new double[]{posX - 21, posY, posX, posY, posX + 21, posY, posX, posY, + posX, posY - 11, posX, posY, posX, posY + 11, posX, posY}, color, 1, DASHES); + float pitch = Mth.clamp(player.getXRot(), -30.0F, 70.0F); + pitch = (float) ((pitch - 20.0F) * 0.16); + float yaw = Mth.clamp(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot()) * 2.0F, -50.0F, 50.0F); + yaw = (float) (yaw * 0.34); + canvas.loop(new double[]{posX + yaw - 3, posY + pitch - 2, posX + yaw + 3, posY + pitch - 2, + posX + yaw + 3, posY + pitch + 2, posX + yaw - 3, posY + pitch + 2}, color); + } +} From f2f7eaf4b47db98765b4fe2e92164dd2a0d90b0d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:13 +0800 Subject: [PATCH 1182/1461] Add client/hud/HudScripts.java to the 1.20.6 shared sources --- .../mcheli/client/hud/HudScripts.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java new file mode 100644 index 00000000..5a670dbd --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java @@ -0,0 +1,86 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.hud.HudFormatException; +import com.jnhprojects.mcheli.hud.HudScript; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +/** + * The HUD scripts in {@code assets/mcheli/hud}, read through the resource manager so resource + * packs can replace or add them, and reloaded with F3 + T. A script that fails to parse is logged + * and left out; the vehicles using it fall back to the plain readout. + */ +public final class HudScripts implements ResourceManagerReloadListener { + public static final HudScripts INSTANCE = new HudScripts(); + private static final Logger LOGGER = LoggerFactory.getLogger(HudScripts.class); + private static final String FOLDER = "hud"; + private static final int DEFAULT_TEXTURE_SIZE = 256; + + private Map scripts = Map.of(); + private final Map textureSizes = new HashMap<>(); + + private HudScripts() { + } + + public @Nullable HudScript get(String name) { + return scripts.get(name.toLowerCase(Locale.ROOT)); + } + + @Override + public void onResourceManagerReload(ResourceManager resources) { + Map loaded = new HashMap<>(); + resources.listResources(FOLDER, id -> id.getNamespace().equals(MCHeli.MOD_ID) && id.getPath().endsWith(".txt")) + .forEach((id, resource) -> { + String path = id.getPath(); + String name = path.substring(FOLDER.length() + 1, path.length() - 4).toLowerCase(Locale.ROOT); + try (Reader in = resource.openAsReader()) { + loaded.put(name, HudScript.parse(name, in)); + } catch (IOException | HudFormatException e) { + LOGGER.error("Skipping HUD {}: {}", id, e.getMessage()); + } + }); + scripts = Map.copyOf(loaded); + textureSizes.clear(); + LOGGER.info("Loaded {} HUD scripts", scripts.size()); + } + + /** + * Width and height of a GUI texture in pixels, read from the PNG header once. The original + * fell back to 256 when it could not tell, and so does this. + */ + int[] textureSize(ResourceManager resources, ResourceLocation texture) { + return textureSizes.computeIfAbsent(texture, id -> readSize(resources.getResource(id))); + } + + private static int[] readSize(Optional resource) { + if (resource.isPresent()) { + try (InputStream in = resource.get().open(); DataInputStream data = new DataInputStream(in)) { + // 8 byte signature, IHDR length and type, then width and height as big endian ints. + data.skipNBytes(16); + int width = data.readInt(); + int height = data.readInt(); + if (width > 0 && height > 0) { + return new int[]{width, height}; + } + } catch (IOException e) { + LOGGER.warn("Could not read the size of a HUD texture", e); + } + } + return new int[]{DEFAULT_TEXTURE_SIZE, DEFAULT_TEXTURE_SIZE}; + } +} From 022f75ba99db23256932c902067d0f5cb068fca4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:14 +0800 Subject: [PATCH 1183/1461] Add client/hud/HudState.java to the 1.20.6 shared sources --- .../mcheli/client/hud/HudState.java | 373 ++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java new file mode 100644 index 00000000..3f9db748 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java @@ -0,0 +1,373 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.client.VehicleCamera; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * What the HUD scripts read, the static state of {@code MCH_HudItem}. Ticked with the client, so + * the radar sweep, altitude and stick behave as in the original; {@link #update} then fills in the + * per frame values before a script runs. + * + *

Systems that arrive later read as their idle state for now: the vehicle inventory is empty + * and the mortar distance is unknown. + */ +final class HudState { + private static final int RADAR_RANGE = 64; + private static final int ALTITUDE_INTERVAL = 10; + private static final double STICK_MAX = 40.0; + /** Roughly the mouse units of the original per degree of pilot look. */ + private static final double STICK_PER_DEGREE = 6.0; + private static final int STICK_SAMPLES = 4; + + final HudVariable.Values vars = new HudVariable.Values(); + final List entityBlips = new ArrayList<>(); + final List enemyBlips = new ArrayList<>(); + + private @Nullable AircraftEntity vehicle; + /** Where the selected mortar would land, or -1. */ + private float mortarDistance = -1.0F; + private int radarRotate; + private int radarRotateO; + private int altitude; + private int altitudeCountdown; + private double stickX; + private double stickY; + private final double[] stickXSamples = new double[STICK_SAMPLES]; + private final double[] stickYSamples = new double[STICK_SAMPLES]; + private int stickSample; + private int fuelWarnCount; + + /** Colour set by the last {@code Color} line. Like the original it carries over between frames. */ + int color = 0xFF000000; + + // Per frame, for the text arguments. + private @Nullable WeaponSet weapon; + private String weaponName = ""; + private String weaponAmmo = ""; + private String weaponRestAmmo = ""; + private float reloadPercent; + private float reloadSeconds; + + /** Mouse look of the pilot this frame, in degrees; drives the control stick indicator. */ + void onPilotLook(float dYaw, float dPitch) { + stickX += dYaw * STICK_PER_DEGREE; + stickY += dPitch * STICK_PER_DEGREE; + double length = Math.sqrt(stickX * stickX + stickY * stickY); + if (length > STICK_MAX) { + stickX *= STICK_MAX / length; + stickY *= STICK_MAX / length; + } + } + + void tick(@Nullable AircraftEntity riding) { + if (riding != vehicle) { + vehicle = riding; + radarRotate = 0; + radarRotateO = 0; + altitudeCountdown = 0; + entityBlips.clear(); + enemyBlips.clear(); + } + stickX *= 0.85; + stickY *= 0.85; + if (riding == null || riding.definition() == null) { + return; + } + if (riding.definition().entityRadar) { + radarRotateO = radarRotate; + radarRotate += radarSpeed(riding); + if (radarRotate >= 360) { + radarRotate = 0; + radarRotateO = 0; + sweepRadar(riding); + } + } + if (--altitudeCountdown <= 0) { + altitude = measureAltitude(riding); + altitudeCountdown = ALTITUDE_INTERVAL; + } + } + + /** Degrees per tick, from each kind's client tick handler. */ + private static int radarSpeed(AircraftEntity vehicle) { + return vehicle instanceof PlaneEntity || vehicle instanceof TankEntity ? 10 : 5; + } + + /** {@code MCH_Radar.updateXZ}: mobs within 64 blocks that are not buried under five or more blocks. */ + private void sweepRadar(AircraftEntity center) { + entityBlips.clear(); + enemyBlips.clear(); + Level level = center.level(); + for (Mob mob : level.getEntitiesOfClass(Mob.class, center.getBoundingBox().inflate(RADAR_RANGE))) { + double x = mob.getX() - center.getX(); + double z = mob.getZ() - center.getZ(); + if (x * x + z * z >= RADAR_RANGE * RADAR_RANGE || isBuried(level, mob)) { + continue; + } + (mob instanceof Enemy ? enemyBlips : entityBlips).add(new double[]{x, z}); + } + } + + private static boolean isBuried(Level level, Mob mob) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(mob.getBlockX(), Math.max(mob.getBlockY() + 1, level.getMinBuildHeight()), mob.getBlockZ()); + int blocks = 0; + for (; pos.getY() < level.getMaxBuildHeight() - 1; pos.move(0, 1, 0)) { + if (!level.getBlockState(pos).isAir() && ++blocks >= 5) { + return true; + } + } + return false; + } + + /** {@code updateAltitude}: blocks of air below the vehicle, looking at most 256 down. */ + private static int measureAltitude(AircraftEntity vehicle) { + Level level = vehicle.level(); + BlockPos.MutableBlockPos pos = vehicle.blockPosition().mutable(); + int top = pos.getY(); + for (int i = 0; i < 256 && top - i > level.getMinBuildHeight(); i++) { + pos.setY(top - i); + if (!level.getBlockState(pos).isAir()) { + return i; + } + } + return 0; + } + + float radarRotation(float partialTicks) { + float rot = radarRotate; + float prev = radarRotateO; + if (rot < prev) { + rot += 360.0F; + } + return prev + (rot - prev) * partialTicks; + } + + void update(AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, int seat, double width, double height, + float partialTicks) { + HudVariable.Values v = vars; + v.set(HudVariable.COLOR, color & 0xFFFFFFFFL); + v.set(HudVariable.CENTER_X, width / 2.0); + v.set(HudVariable.CENTER_Y, height / 2.0); + v.set(HudVariable.WIDTH, width); + v.set(HudVariable.HEIGHT, height); + v.set(HudVariable.TIME, Math.floorMod(vehicle.level().getDayTime(), 24000L)); + v.set(HudVariable.TEST_MODE, 0.0); + v.set(HudVariable.PLYR_YAW, Mth.wrapDegrees(player.getViewYRot(partialTicks))); + v.set(HudVariable.PLYR_PITCH, player.getViewXRot(partialTicks)); + v.set(HudVariable.YAW, Mth.wrapDegrees(vehicle.getViewYRot(partialTicks))); + v.set(HudVariable.PITCH, vehicle.getViewXRot(partialTicks)); + v.set(HudVariable.ROLL, Mth.wrapDegrees(vehicle.roll(partialTicks))); + v.set(HudVariable.ALTITUDE, altitude); + v.set(HudVariable.SEA_ALT, Math.max(vehicle.getY() - vehicle.level().getSeaLevel(), 0.0)); + v.set(HudVariable.HAVE_RADAR, def.entityRadar); + v.set(HudVariable.RADAR_ROT, radarRotation(partialTicks)); + v.set(HudVariable.HP, vehicle.hp()); + v.set(HudVariable.MAX_HP, def.maxHp); + v.set(HudVariable.HP_RTO, def.maxHp > 0 ? (double) vehicle.hp() / def.maxHp : 0.0); + v.set(HudVariable.THROTTLE, vehicle.visibleThrottle()); + v.set(HudVariable.POS_X, vehicle.getX()); + v.set(HudVariable.POS_Y, vehicle.getY()); + v.set(HudVariable.POS_Z, vehicle.getZ()); + // Only the pilot's client simulates the hull, so read motion from the position instead. + double mx = vehicle.getX() - vehicle.xo; + double my = vehicle.getY() - vehicle.yo; + double mz = vehicle.getZ() - vehicle.zo; + v.set(HudVariable.MOTION_X, mx); + v.set(HudVariable.MOTION_Y, my); + v.set(HudVariable.MOTION_Z, mz); + v.set(HudVariable.SPEED, Math.sqrt(mx * mx + my * my + mz * mz)); + v.set(HudVariable.FUEL, vehicle.fuelRatio()); + v.set(HudVariable.LOW_FUEL, lowFuel(vehicle)); + updateStick(seat == 0 && vehicle.steersWithMouse()); + updateWeapon(vehicle, seat); + v.set(HudVariable.VTOL_STAT, vehicle.vtolMode()); + v.set(HudVariable.FREE_LOOK, seat == 0 && vehicle.input().freeLook); + v.set(HudVariable.GUNNER_MODE, vehicle.isGunnerMode(player)); + v.set(HudVariable.CAM_MODE, VehicleCamera.mode().ordinal()); + v.set(HudVariable.CAM_ZOOM, VehicleCamera.zoom()); + v.set(HudVariable.AUTO_PILOT, vehicle instanceof PlaneEntity && vehicle.isGunnerMode(player)); + VehicleWeapons weapons = vehicle.weapons(); + boolean flares = weapons != null && weapons.flares.isFitted(); + v.set(HudVariable.HAVE_FLARE, flares); + v.set(HudVariable.CAN_FLARE, flares && !weapons.flares.isPreparing() && !weapons.flares.isActive()); + v.set(HudVariable.INVENTORY, 0.0); + v.set(HudVariable.HOVERING, vehicle instanceof HelicopterEntity && vehicle.isHoveringMode()); + v.set(HudVariable.IS_UAV, def.uav); + double uavDistance = vehicle.stationDistance(); + // {@code getUAV_Fs}: signal strength, full at the station and gone 120 blocks out. + v.set(HudVariable.UAV_FS, uavDistance < 0.0 ? 0.0 : 1.0 - Math.min(uavDistance, 120.0) / 120.0); + } + + /** + * {@code isLowFuel}: below 10% the warning blinks, on for eight seconds out of every fourteen. + * Ticks with each update, as the original counted per frame. + */ + private boolean lowFuel(AircraftEntity vehicle) { + if (fuelWarnCount <= 0) { + fuelWarnCount = 280; + } + fuelWarnCount--; + return fuelWarnCount < 160 && vehicle.maxFuel() > 0 && vehicle.fuelRatio() < 0.1F; + } + + /** The original averaged the last four stick positions, scaled to -1..1, with up positive. */ + private void updateStick(boolean pilot) { + stickXSamples[stickSample] = pilot ? stickX / STICK_MAX : 0.0; + stickYSamples[stickSample] = pilot ? -stickY / STICK_MAX : 0.0; + stickSample = (stickSample + 1) % STICK_SAMPLES; + double x = 0.0; + double y = 0.0; + for (int i = 0; i < STICK_SAMPLES; i++) { + x += stickXSamples[i]; + y += stickYSamples[i]; + } + vars.set(HudVariable.STICK_X, x / STICK_SAMPLES); + vars.set(HudVariable.STICK_Y, y / STICK_SAMPLES); + } + + /** {@code updateWeapon} and {@code updateVarMap_Weapon}. */ + private void updateWeapon(AircraftEntity vehicle, int seat) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + weapon = set; + double reloadTime = 0.0; + double heat = 0.0; + double lock = 0.0; + int sightType = 0; + if (set != null) { + WeaponDefinition def = set.definition(); + weaponName = set.displayName(); + weaponAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.ammo()) : ""; + weaponRestAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.restAmmo()) : ""; + float wait; + if (def.delay > def.reloadTime) { + reloadSeconds = Math.abs(set.countWait); + wait = Math.min(Math.abs((float) set.countWait / Math.max(def.delay, 1)), 1.0F); + } else { + reloadSeconds = set.countReloadWait; + wait = (float) set.countReloadWait / Math.max(def.reloadTime, 1); + } + reloadTime = wait; + reloadSeconds /= 20.0F; + reloadPercent = (1.0F - wait) * 100.0F; + if (def.maxHeatCount > 0) { + heat = Math.min((double) set.currentHeat / def.maxHeatCount, 1.0); + } + Guidance guidance = weapons.guidance(weapons.selected(seat)); + if (def.sight == WeaponDefinition.Sight.LOCK && guidance != null && guidance.lockCountMax() > 0) { + lock = (double) guidance.lockCount() / guidance.lockCountMax(); + sightType = 2; + } else if (def.sight == WeaponDefinition.Sight.ROCKET) { + sightType = 1; + } + vars.set(HudVariable.IS_HEAT_WPN, def.maxHeatCount > 0); + vars.set(HudVariable.RELOADING, set.isInPreparation()); + vars.set(HudVariable.DSP_MT_DIST, def.displayMortarDistance); + // The original worked the arc out every fifth tick; it is cheap enough to follow every update. + mortarDistance = def.displayMortarDistance ? (float) set.landingDistance(vehicle) : -1.0F; + } else { + vars.set(HudVariable.IS_HEAT_WPN, 0.0); + vars.set(HudVariable.RELOADING, 0.0); + vars.set(HudVariable.DSP_MT_DIST, 0.0); + mortarDistance = -1.0F; + } + vars.set(HudVariable.RELOAD_TIME, reloadTime); + vars.set(HudVariable.WPN_HEAT, heat); + vars.set(HudVariable.SIGHT_TYPE, sightType); + vars.set(HudVariable.LOCK, lock); + vars.set(HudVariable.MT_DIST, mortarDistance); + } + + boolean hasWeapon() { + return weapon != null; + } + + boolean hasMagazine() { + return weapon != null && weapon.magazineSize() > 0; + } + + /** A {@code DrawString} argument, typed the way the original passed it to {@code String.format}. */ + @Nullable Object text(TextArg arg, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player) { + int worldTime = (int) Math.floorMod(vehicle.level().getDayTime() + 6000L, 24000L); + return switch (arg) { + case NONE -> null; + // The vehicle inventory screen and its key come later in phase 4. + case KEY_GUI -> ""; + case NAME -> def.displayName; + case ALTITUDE -> altitude; + case DATE -> new Date(); + case MC_THOR -> worldTime / 1000; + case MC_TMIN -> worldTime % 1000 * 36 / 10 / 60; + case MC_TSEC -> worldTime % 1000 * 36 / 10 % 60; + case MAX_HP -> def.maxHp; + case HP -> vehicle.hp(); + case HP_PER -> def.maxHp > 0 ? 100.0 * vehicle.hp() / def.maxHp : 0.0; + case POS_X -> vehicle.getX(); + case POS_Y -> vehicle.getY(); + case POS_Z -> vehicle.getZ(); + case MOTION_X -> vars.get(HudVariable.MOTION_X); + case MOTION_Y -> vars.get(HudVariable.MOTION_Y); + case MOTION_Z -> vars.get(HudVariable.MOTION_Z); + case INVENTORY -> 0; + case WPN_NAME -> weaponName; + case WPN_AMMO -> weaponAmmo; + case WPN_RM_AMMO -> weaponRestAmmo; + case RELOAD_PER -> reloadPercent; + case RELOAD_SEC -> reloadSeconds; + case MORTAR_DIST -> mortarDistance; + case MC_VER -> "26.3"; + case MOD_VER -> Platform.get().modVersion(); + case MOD_NAME -> "MCHeli Rework"; + case YAW -> rotate360(vehicle.getYRot() + 180.0); + case PITCH -> -vehicle.getXRot(); + case ROLL -> Mth.wrapDegrees(vehicle.roll()); + case PLYR_YAW -> rotate360(player.getYRot() + 180.0); + case PLYR_PITCH -> -player.getXRot(); + case TVM_POS_X, TVM_POS_Y, TVM_POS_Z, TVM_DIFF -> tvMissile(arg, vehicle); + case CAM_ZOOM -> VehicleCamera.zoom(); + case UAV_DIST -> Math.max(vehicle.stationDistance(), 0.0); + case THROTTLE -> vehicle.visibleThrottle() * 100.0; + }; + } + + private static double tvMissile(TextArg arg, AircraftEntity vehicle) { + TvMissileEntity missile = vehicle.tvMissile(); + if (missile == null) { + return 0.0; + } + return switch (arg) { + case TVM_POS_X -> missile.getX(); + case TVM_POS_Y -> missile.getY(); + case TVM_POS_Z -> missile.getZ(); + default -> missile.position().distanceTo(vehicle.position()); + }; + } + + static double rotate360(double degrees) { + double r = degrees % 360.0; + return r >= 0.0 ? r : r + 360.0; + } +} From 18b839dd5f4de841b0804e44f480199604e04ba2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:14 +0800 Subject: [PATCH 1184/1461] Add client/particle/GlowParticle.java to the 1.20.6 shared sources --- .../mcheli/client/particle/GlowParticle.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java new file mode 100644 index 00000000..4d854b29 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.ParticleRenderType; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.particle.TextureSheetParticle; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; +import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; +import org.jspecify.annotations.Nullable; + +/** + * {@code mcheli:glow}: a ball of light added on top of the scene. A flash swells quickly and fades + * out; a fire holds its brightness, wavering, and only fades towards the end. How bright it gets + * depends on the light where it starts, so the same explosion is a flicker at noon and lights up + * the sky at night. + */ +public class GlowParticle extends TextureSheetParticle { + private final float size; + private final float peak; + private final boolean flicker; + private float waver = 1.0F; + + GlowParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, GlowParticleOptions options, + TextureAtlasSprite sprite, float strength) { + super(level, x, y, z); + setSprite(sprite); + setParticleSpeed(xd, yd, zd); + this.size = options.size(); + this.peak = strength; + this.flicker = options.flicker(); + this.quadSize = size; + this.lifetime = options.lifetime(); + this.friction = 0.9F; + this.gravity = 0.0F; + this.hasPhysics = false; + int color = options.color(); + setColor(FastColor.ARGB32.red(color) / 255.0F, FastColor.ARGB32.green(color) / 255.0F, FastColor.ARGB32.blue(color) / 255.0F); + this.alpha = 0.0F; + } + + @Override + public void tick() { + super.tick(); + if (flicker) { + waver = 0.7F + random.nextFloat() * 0.3F; + } + } + + @Override + public void render(VertexConsumer buffer, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + if (flicker) { + // Burning: hold, then die down over the last third. + alpha = peak * waver * Math.min(1.0F, (1.0F - t) * 3.0F); + } else { + // A flash: brightest at once, gone quickly. + float left = 1.0F - t; + alpha = peak * left * left; + } + super.render(buffer, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return flicker ? size * (0.85F + 0.15F * waver) : size * (0.6F + 0.4F * Math.min(1.0F, t * 4.0F)); + } + + @Override + protected int getLightColor(float partialTicks) { + return LightTexture.FULL_BRIGHT; + } + + @Override + public ParticleRenderType getRenderType() { + return Glow.PARTICLE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public @Nullable Particle createParticle(GlowParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd) { + float strength = Glow.strength(level, BlockPos.containing(x, y, z)); + if (strength <= 0.0F) { + return null; + } + return new GlowParticle(level, x, y, z, xd, yd, zd, options, sprites.get(level.random), strength); + } + } +} From f86a31f412dec2636b891245a86ef8f5ec05bff7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:15 +0800 Subject: [PATCH 1185/1461] Add client/particle/SmokeParticle.java to the 1.20.6 shared sources --- .../mcheli/client/particle/SmokeParticle.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java new file mode 100644 index 00000000..3efefa3f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/particle/SmokeParticle.java @@ -0,0 +1,85 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.ParticleRenderType; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.particle.TextureSheetParticle; +import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; + +/** + * {@code mcheli:smoke}: a puff of coloured smoke, {@code MCH_EntityParticleSmoke}. It swells to its + * full size over the first part of its life, its colour drifts towards white, it rises slowly and + * fades out at the end. The sprites are vanilla's campfire smoke. + */ +public class SmokeParticle extends TextureSheetParticle { + private final float size; + + SmokeParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, SmokeParticleOptions options, + SpriteSet sprites, RandomSource random) { + super(level, x, y, z); + setSprite(sprites.get(random)); + setParticleSpeed(xd, yd, zd); + // The original drew a puff 0.1 x its size out from the middle, so size 10 is two blocks across. + this.size = options.size() * 0.1F * (0.8F + random.nextFloat() * 0.4F); + this.quadSize = size * 0.4F; + this.lifetime = options.lifetime() > 0 ? options.lifetime() : 60 + random.nextInt(40); + this.friction = 0.96F; + this.gravity = -0.002F; + this.hasPhysics = true; + setColor((FastColor.ARGB32.red(options.color()) / 255.0F), (FastColor.ARGB32.green(options.color()) / 255.0F), (FastColor.ARGB32.blue(options.color()) / 255.0F)); + this.alpha = 0.9F; + } + + @Override + public void tick() { + super.tick(); + // toWhite: pale towards the brightest channel while the colour is still strong. + float max = Math.max(rCol, Math.max(gCol, bCol)); + float min = Math.min(rCol, Math.min(gCol, bCol)); + if (max - min > 0.2F) { + rCol += (max - rCol) * 0.016F; + gCol += (max - gCol) * 0.016F; + bCol += (max - bCol) * 0.016F; + } + } + + @Override + public void render(VertexConsumer buffer, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + alpha = 0.9F * Math.min(1.0F, (1.0F - t) * 4.0F); + super.render(buffer, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return size * (0.4F + 0.6F * Math.min(1.0F, t * 3.0F)); + } + + @Override + public ParticleRenderType getRenderType() { + return Glow.SMOKE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public Particle createParticle(SmokeParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd) { + return new SmokeParticle(level, x, y, z, xd, yd, zd, options, sprites, level.random); + } + } +} From fb6019e28207b9684156e392d3dae92d872a77b8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:15 +0800 Subject: [PATCH 1186/1461] Add client/render/A10Renderer.java to the 1.20.6 shared sources --- .../mcheli/client/render/A10Renderer.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java new file mode 100644 index 00000000..df32bfa2 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +/** The close air support A-10, {@code MCH_RenderA10}. */ +public class A10Renderer extends EntityRenderer { + private static final ResourceLocation TEXTURE = new ResourceLocation(MCHeli.MOD_ID, "textures/bullets/a10.png"); + + public A10Renderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 10.5F; + } + + @Override + public void render(A10Entity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + Mesh mesh = entity.isVisible() ? MeshCache.INSTANCE.get("", "a-10") : null; + if (mesh != null) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + pose.mulPose(Axis.XP.rotationDegrees(-entity.getViewXRot(partialTicks))); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(TEXTURE)), light, OverlayTexture.NO_OVERLAY); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(A10Entity entity) { + return TEXTURE; + } +} From ca13018e36d083dd121582921fdd70e3f3f5ffa5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:16 +0800 Subject: [PATCH 1187/1461] Add client/render/AircraftRenderState.java to the 1.20.6 shared sources --- .../client/render/AircraftRenderState.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java new file mode 100644 index 00000000..2fcf06d6 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import org.jspecify.annotations.Nullable; + +/** + * Snapshot of a vehicle for one frame, filled in by {@link AircraftRenderer} before it draws, the + * way 26.x's render states work. Every angle is already interpolated for the frame's partial tick. + */ +public class AircraftRenderState { + /** Packed block and sky light at the vehicle. */ + public int lightCoords; + /** Definitions are immutable after loading, so the snapshot can share them. */ + public @Nullable AircraftDefinition definition; + public String texture = ""; + public float yaw; + public float pitch; + public float roll; + /** Helicopter main rotor or plane propeller angle. */ + public float rotorAngle; + /** Each helicopter blade's own angle, per rotor, so folding blades can leave the rotor's spin. */ + public float[][] bladeAngles = new float[0][]; + public float throttle; + + // Part states, 0 to 90 degrees + public float canopy; + public float landingGear; + public float hatch; + /** Rope length below the hooks while troops go down, 0 with the ropes in. */ + public float ropeLength; + /** The beams of the search lights, or false with them off. */ + public boolean searchLight; + /** Where the crew aim a turning search light, in world degrees. */ + public float searchLightYaw; + public float searchLightPitch; + public float nozzle; + public float wing; + + public float wheelRoll; + public float steer; + public final float[] roller = new float[2]; + /** Track run per side from 0 to 1, in link spacings. */ + public final float[] track = new float[2]; + public float[] rotatingParts = new float[0]; + + /** Per seat: occupant view relative to the hull, and whether anyone sits there. */ + public float[] seatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public boolean[] seatOccupied = new boolean[0]; + /** Seat of the player at this client, or -1, and whether they look through their own eyes. */ + public int localSeat = -1; + public boolean localFirstPerson; + /** Shade for the unlit glass, from the light at the vehicle. */ + public int glassColor = 0xFFFFFFFF; + /** Drawn inside a screen, as the workshop does, rather than in the world. */ + public boolean inGui; + + /** Per {@code AddPartWeapon} line, the state of the weapon it belongs to; see {@link WeaponPartState}. */ + public WeaponPartState[] weaponParts = new WeaponPartState[0]; + /** Per weapon bay, 0 to 90 degrees. */ + public float[] bays = new float[0]; + + /** How one weapon part is drawn this frame. */ + public static final class WeaponPartState { + /** Whether the part's weapon exists; without one the part follows its operator's view. */ + public boolean hasWeapon; + /** Aim relative to the weapon's default direction. */ + public float yaw; + public float pitch; + public float defaultYaw; + /** How far a turret weapon lags behind the pilot's view. */ + public float turretLag; + public float recoil; + public float barrel; + /** A missile part whose round is on its way and not reloaded yet. */ + public boolean hidden; + } +} From 119dd249a17ef0f1d0227de85f2b84e376f1fda1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:16 +0800 Subject: [PATCH 1188/1461] Add client/render/AircraftRenderer.java to the 1.20.6 shared sources --- .../client/render/AircraftRenderer.java | 350 ++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java new file mode 100644 index 00000000..7849eb86 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java @@ -0,0 +1,350 @@ +package com.jnhprojects.mcheli.client.render; + +import net.minecraft.client.renderer.RenderType; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RepellingHook; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.SearchLight; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.AnimationState; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PartState; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * Draws any MCHeli vehicle from its MQO/OBJ model, following {@code MCH_RenderAircraft}: the hull, + * the kind's own moving parts (rotors, wings, gun mounts) and then the parts every kind can have. + */ +public class AircraftRenderer extends EntityRenderer { + private static final float[] HULL_COLOR = {0.0F, 0.88F, 1.0F}; + private static final float[] GENERATED_COLOR = {1.0F, 0.75F, 0.0F}; + /** Ropes take the colour of brown wool. */ + private static final ResourceLocation ROPE = new ResourceLocation("textures/block/brown_wool.png"); + private static final float ROPE_WIDTH = 0.03F; + + /** Reused every frame; rendering is single threaded. */ + private final AircraftRenderState state = new AircraftRenderState(); + + public AircraftRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 2.0F; + } + + /** What 26.x splits off as the render state: everything the frame needs, read from the entity. */ + private void extractRenderState(T entity, AircraftRenderState state, float partialTicks, int light) { + state.lightCoords = light; + AircraftDefinition def = entity.definition(); + state.definition = def; + state.texture = entity.texture(); + state.yaw = entity.getViewYRot(partialTicks); + state.pitch = entity.getViewXRot(partialTicks); + state.roll = entity.roll(partialTicks); + if (def == null) { + return; + } + state.rotorAngle = switch (entity) { + case HelicopterEntity heli -> heli.rotorAngle(partialTicks); + case PlaneEntity plane -> plane.rotorAngle(partialTicks); + default -> 0.0F; + }; + state.canopy = angle(entity.canopy(), partialTicks); + state.landingGear = angle(entity.landingGear(), partialTicks); + state.hatch = angle(entity.hatch(), partialTicks); + if (entity instanceof HelicopterEntity heli && def instanceof HelicopterDefinition heliDef) { + bladeAngles(heli, heliDef, state, partialTicks); + } + state.ropeLength = entity.isRepelling() ? entity.ropeLength() : 0.0F; + state.searchLight = entity.isSearchLightOn(); + if (state.searchLight) { + Entity aimer = entity.searchLightAimer(); + state.searchLightYaw = aimer != null ? aimer.getViewYRot(partialTicks) : state.yaw; + state.searchLightPitch = aimer != null ? aimer.getViewXRot(partialTicks) : state.pitch; + } + if (entity instanceof PlaneEntity plane) { + state.nozzle = angle(plane.nozzle(), partialTicks); + state.wing = angle(plane.wing(), partialTicks); + } else { + state.nozzle = 0.0F; + state.wing = 0.0F; + } + + AnimationState anim = entity.animation; + state.throttle = Mth.lerp(partialTicks, anim.prevThrottle, anim.throttle); + state.wheelRoll = Mth.lerp(partialTicks, anim.prevWheelRoll, anim.wheelRoll); + state.steer = Mth.lerp(partialTicks, anim.prevSteer, anim.steer); + for (int i = 0; i < 2; i++) { + state.roller[i] = Mth.lerp(partialTicks, anim.prevRoller[i], anim.roller[i]); + state.track[i] = lerpWrapped(anim.prevTrack[i], anim.track[i], partialTicks); + } + if (state.rotatingParts.length != anim.rotatingParts.length) { + state.rotatingParts = new float[anim.rotatingParts.length]; + } + for (int i = 0; i < anim.rotatingParts.length; i++) { + state.rotatingParts[i] = Mth.lerp(partialTicks, anim.prevRotatingParts[i], anim.rotatingParts[i]); + } + + int seats = def.seatAndRackCount(); + if (state.seatYaw.length != seats) { + state.seatYaw = new float[seats]; + state.seatPitch = new float[seats]; + state.seatOccupied = new boolean[seats]; + } + for (int i = 0; i < seats; i++) { + state.seatYaw[i] = anim.seatYaw(i, partialTicks); + state.seatPitch[i] = anim.seatPitch(i, partialTicks); + state.seatOccupied[i] = entity.occupant(i) != null; + } + Minecraft mc = Minecraft.getInstance(); + state.localSeat = mc.player != null ? entity.seatOf(mc.player) : -1; + state.localFirstPerson = mc.options.getCameraType().isFirstPerson(); + state.glassColor = glassColor(entity, state.lightCoords); + extractWeapons(entity, def, state, partialTicks); + } + + /** + * Glass is drawn without the light map, so shade it by hand: as bright as the brighter of block + * light and the sky light left after night or rain darkens it. + */ + private static int glassColor(AircraftEntity entity, int lightCoords) { + int sky = Math.max(LightTexture.sky(lightCoords) - entity.level().getSkyDarken(), 0); + int light = Math.max(LightTexture.block(lightCoords), sky); + int shade = (int) ((0.3F + 0.7F * light / 15.0F) * 255.0F); + return 0xFF000000 | shade << 16 | shade << 8 | shade; + } + + /** {@code renderWeapon}'s inputs: each part's weapon aim, recoil, barrel spin and reload state. */ + private static void extractWeapons(AircraftEntity entity, AircraftDefinition def, AircraftRenderState state, float partialTicks) { + VehicleWeapons weapons = entity.weapons(); + int parts = def.weaponParts.size(); + if (state.weaponParts.length != parts) { + state.weaponParts = new AircraftRenderState.WeaponPartState[parts]; + for (int i = 0; i < parts; i++) { + state.weaponParts[i] = new AircraftRenderState.WeaponPartState(); + } + } + WeaponSet previous = null; + int mount = 0; + for (int i = 0; i < parts; i++) { + WeaponPart part = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState out = state.weaponParts[i]; + WeaponSet set = weapons != null && !part.weapons().isEmpty() ? weapons.byName(part.weapons().getFirst()) : null; + if (set != previous) { + mount = 0; + previous = set; + } + out.hasWeapon = set != null; + if (set == null) { + continue; + } + float yaw = set.rotationYaw; + float prevYaw = set.prevRotationYaw; + if (yaw - prevYaw > 180.0F) { + prevYaw += 360.0F; + } else if (yaw - prevYaw < -180.0F) { + prevYaw -= 360.0F; + } + out.yaw = Mth.lerp(partialTicks, prevYaw, yaw) - set.defaultRotationYaw; + out.pitch = Mth.lerp(partialTicks, set.prevRotationPitch, set.rotationPitch); + out.defaultYaw = set.defaultRotationYaw; + out.turretLag = set.rotationTurretYaw; + out.barrel = Mth.lerp(partialTicks, set.prevBarrel, set.barrel); + // A part shared by several weapons kicks back with whichever fired hardest. + float recoil = set.recoil(0, partialTicks); + for (String name : part.weapons()) { + WeaponSet other = weapons.byName(name); + if (other != null) { + recoil = Math.max(recoil, other.recoil(0, partialTicks)); + } + } + out.recoil = recoil; + out.hidden = part.missile() && weapons.isMountUsed(weapons.indexOf(set.name()), mount); + mount++; + } + if (state.bays.length != def.weaponBays.size()) { + state.bays = new float[def.weaponBays.size()]; + } + for (int i = 0; i < state.bays.length; i++) { + state.bays[i] = weapons != null ? weapons.bayAngle(i, partialTicks) : 0.0F; + } + } + + private static float angle(@Nullable PartState part, float partialTicks) { + return part != null ? part.angle(partialTicks) : 0.0F; + } + + /** Interpolates a value that wraps from 1 back to 0. */ + private static float lerpWrapped(float prev, float current, float partialTicks) { + float d = current - prev; + if (d > 0.5F) { + d -= 1.0F; + } else if (d < -0.5F) { + d += 1.0F; + } + float v = prev + d * partialTicks; + return v - Mth.floor(v); + } + + @Override + public boolean shouldRender(T entity, Frustum frustum, double camX, double camY, double camZ) { + return entity.shouldRender(camX, camY, camZ) && frustum.isVisible(cullingBox(entity)); + } + + /** The hit box is far smaller than most models, so cull on a generous box instead. */ + private static AABB cullingBox(AircraftEntity entity) { + AircraftDefinition def = entity.definition(); + double reach = def != null ? Math.max(8.0, def.markerWidth * 4.0) : 8.0; + return entity.getBoundingBox().inflate(reach, reach / 2.0, reach); + } + + @Override + public void render(T entity, float entityYaw, float partialTicks, PoseStack poseStack, MultiBufferSource buffers, int light) { + extractRenderState(entity, state, partialTicks, light); + if (state.definition != null && entityRenderDispatcher.shouldRenderHitBoxes()) { + showHull(entity, state.definition, poseStack, buffers); + } + draw(state, poseStack, buffers); + super.render(entity, entityYaw, partialTicks, poseStack, buffers, light); + } + + /** The vehicle and what hangs off it, from a state alone; the workshop draws a model with no entity this way. */ + public static void draw(AircraftRenderState state, PoseStack poseStack, MultiBufferSource buffers) { + AircraftDefinition def = state.definition; + Mesh mesh = def != null ? MeshCache.INSTANCE.get(def.folder(), def.name()) : null; + if (mesh != null) { + // Canopies and windshields are semi-transparent texels; PartRenderer draws those + // triangles blended, as the original did, and everything else as cutout. + poseStack.pushPose(); + poseStack.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + poseStack.mulPose(Axis.XP.rotationDegrees(state.pitch)); + poseStack.mulPose(Axis.ZP.rotationDegrees(state.roll)); + new PartRenderer(def, mesh, state, poseStack, buffers, texture(def.folder(), state.texture), state.lightCoords, + OverlayTexture.NO_OVERLAY, state.glassColor).submitAll(); + if (state.ropeLength < 0.0F) { + ropes(def, state, poseStack, buffers); + } + if (state.searchLight) { + searchLights(def, state, poseStack, buffers); + } + poseStack.popPose(); + } + } + + /** + * With F3+B on, outlines the hull boxes next to vanilla's main box: what projectiles hit and + * what other vehicles collide with. The definition's own boxes are cyan, the ones generated + * from the model amber. + */ + private static void showHull(AircraftEntity entity, AircraftDefinition def, PoseStack pose, MultiBufferSource buffers) { + List hull = entity.hullBoxes(); + if (hull.size() < 2) { + return; + } + VertexConsumer lines = buffers.getBuffer(RenderType.lines()); + int listed = def.boundingBoxes.size(); + // The first is the main box, which vanilla draws itself; then the listed boxes, then the generated ones. + for (int i = 1; i < hull.size(); i++) { + float[] c = i <= listed ? HULL_COLOR : GENERATED_COLOR; + LevelRenderer.renderLineBox(pose, lines, hull.get(i).move(-entity.getX(), -entity.getY(), -entity.getZ()), c[0], c[1], c[2], 1.0F); + } + } + + /** + * {@code renderLight}: each lamp's cone. A turning light follows the crew's aim; a fixed one + * points along the vehicle, and a steering one also turns with the front wheels. + */ + private static void searchLights(AircraftDefinition def, AircraftRenderState state, PoseStack pose, MultiBufferSource buffers) { + for (SearchLight light : def.searchLights) { + pose.pushPose(); + pose.translate(light.position().x(), light.position().y(), light.position().z()); + if (!light.fixed()) { + pose.mulPose(Axis.YN.rotationDegrees(state.searchLightYaw - state.yaw + light.yaw())); + pose.mulPose(Axis.XP.rotationDegrees(state.searchLightPitch + 90.0F - state.pitch + light.pitch())); + } else { + float steer = light.steering() ? -state.steer * light.steeringRotation() : 0.0F; + pose.mulPose(Axis.YN.rotationDegrees(light.yaw() + steer)); + pose.mulPose(Axis.XP.rotationDegrees(90.0F + light.pitch())); + } + Glow.cone(pose, buffers, light.height(), light.width() / 2.0F, light.colorStart(), light.colorEnd()); + pose.popPose(); + } + } + + private static void bladeAngles(HelicopterEntity heli, HelicopterDefinition def, AircraftRenderState state, float partialTicks) { + if (state.bladeAngles.length != def.rotors.size()) { + state.bladeAngles = new float[def.rotors.size()][]; + } + for (int r = 0; r < def.rotors.size(); r++) { + RotorDefinition rotor = def.rotors.get(r); + int blades = Math.max(rotor.blades(), 1); + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + if (state.bladeAngles[r] == null || state.bladeAngles[r].length != blades) { + state.bladeAngles[r] = new float[blades]; + } + for (int b = 0; b < blades; b++) { + state.bladeAngles[r][b] = heli.bladeAngle(r, b, step, partialTicks); + } + } + } + + /** {@code renderRope}: a dark line straight down from each hook, in the vehicle's own frame. */ + private static void ropes(AircraftDefinition def, AircraftRenderState state, PoseStack pose, MultiBufferSource buffers) { + int light = state.lightCoords; + float length = state.ropeLength; + VertexConsumer buffer = buffers.getBuffer(RenderType.entityCutout(ROPE)); + PoseStack.Pose p = pose.last(); + for (RepellingHook hook : def.repellingHooks) { + float x = (float) hook.position().x(); + float y = (float) hook.position().y(); + float z = (float) hook.position().z(); + // Two thin crossed strips, so the rope shows from every side. + ropeStrip(p, buffer, x - ROPE_WIDTH, z, x + ROPE_WIDTH, z, y, length, light); + ropeStrip(p, buffer, x, z - ROPE_WIDTH, x, z + ROPE_WIDTH, y, length, light); + } + } + + private static void ropeStrip(PoseStack.Pose p, VertexConsumer buffer, float x0, float z0, float x1, float z1, float y, float length, int light) { + ropeVertex(p, buffer, x0, y, z0, 0.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y, z1, 1.0F, 0.0F, light); + ropeVertex(p, buffer, x1, y + length, z1, 1.0F, 1.0F, light); + ropeVertex(p, buffer, x0, y + length, z0, 0.0F, 1.0F, light); + } + + private static void ropeVertex(PoseStack.Pose p, VertexConsumer buffer, float x, float y, float z, float u, float v, int light) { + buffer.vertex(p, x, y, z).color(0xFFFFFFFF).uv(u, v).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(light).normal(p, 0.0F, 1.0F, 0.0F).endVertex(); + } + + /** Vehicles bind their own textures per part; this is only for vanilla's bookkeeping. */ + @Override + public ResourceLocation getTextureLocation(T entity) { + return texture(entity.definition() != null ? entity.definition().folder() : "", entity.texture()); + } + + private static ResourceLocation texture(String folder, String name) { + return RuntimeAssets.texture(new ResourceLocation(MCHeli.MOD_ID, "textures/" + folder + "/" + name + ".png")); + } +} From 01c33723d5631d83c4a5a822b41280b952a15722 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:17 +0800 Subject: [PATCH 1189/1461] Add client/render/ChainRenderer.java to the 1.20.6 shared sources --- .../mcheli/client/render/ChainRenderer.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java new file mode 100644 index 00000000..ae17943f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ChainRenderer.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +/** + * {@code MCH_RenderChain}: a line of links from the towed end to the end that pulls, one link + * every 0.95 blocks, each turned along the chain. + */ +public class ChainRenderer extends EntityRenderer { + private static final ResourceLocation TEXTURE = new ResourceLocation(MCHeli.MOD_ID, "textures/chain.png"); + private static final double LINK = 0.95; + private static final int GREY = 0xFF808080; + + public ChainRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + /** The chain hangs over the end that pulls but reaches to the other end; keep both in view. */ + @Override + public boolean shouldRender(ChainEntity chain, Frustum frustum, double x, double y, double z) { + Entity towed = chain.towed(); + return super.shouldRender(chain, frustum, x, y, z) || towed != null && frustum.isVisible(towed.getBoundingBox()); + } + + @Override + public void render(ChainEntity chain, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + Entity tow = chain.tow(); + Entity towed = chain.towed(); + Mesh mesh = MeshCache.INSTANCE.get("", "chain"); + if (mesh != null && tow != null && towed != null) { + Vec3 start = towed.getPosition(partialTicks); + Vec3 from = start.subtract(chain.getPosition(partialTicks)); + Vec3 span = tow.getPosition(partialTicks).subtract(start); + double length = span.length(); + if (length > LINK) { + Vec3 step = span.scale(LINK / length); + float yaw = (float) Math.toDegrees(Math.atan2(step.z, step.x)); + float pitch = (float) Math.toDegrees(Math.atan2(step.y, Math.sqrt(step.x * step.x + step.z * step.z))); + VertexConsumer buffer = buffers.getBuffer(RenderType.entityCutout(TEXTURE)); + pose.pushPose(); + pose.translate(from.x, from.y, from.z); + for (double left = length; left > LINK; left -= LINK) { + pose.translate(step.x, step.y, step.z); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-yaw)); + pose.mulPose(Axis.ZP.rotationDegrees(pitch)); + MeshRenderer.emit(mesh.groups(), pose.last(), buffer, light, OverlayTexture.NO_OVERLAY, GREY); + pose.popPose(); + } + pose.popPose(); + } + } + super.render(chain, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(ChainEntity chain) { + return TEXTURE; + } +} From 3266d83d4d24885e1d7b4b8fc9adb09494e1c526 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:17 +0800 Subject: [PATCH 1190/1461] Add client/render/DraftingTableRenderer.java to the 1.20.6 shared sources --- .../client/render/DraftingTableRenderer.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java new file mode 100644 index 00000000..f42386fb --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/DraftingTableRenderer.java @@ -0,0 +1,38 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.Vec3; + +/** {@code MCH_DraftingTableRenderer}: the original model, turned to the block's eighth of a circle. */ +public class DraftingTableRenderer implements BlockEntityRenderer { + private static final ResourceLocation TEXTURE = new ResourceLocation(MCHeli.MOD_ID, "textures/block/drafting_table.png"); + /** The original greyed the table a little. */ + private static final int TINT = 0xFFBFBFBF; + + public DraftingTableRenderer(BlockEntityRendererProvider.Context context) { + } + + @Override + public void render(DraftingTableBlockEntity table, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light, int overlay) { + Mesh mesh = MeshCache.INSTANCE.get("blocks", "drafting_table"); + if (mesh == null) { + return; + } + pose.pushPose(); + pose.translate(0.5F, 0.0F, 0.5F); + pose.mulPose(Axis.YP.rotationDegrees(-table.getBlockState().getValue(DraftingTableBlock.ROTATION) * 45.0F + 180.0F)); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(TEXTURE)), light, OverlayTexture.NO_OVERLAY, TINT); + pose.popPose(); + } +} From 80667575b886d2b33ab4aebb88c54a29ced38e92 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:18 +0800 Subject: [PATCH 1191/1461] Add client/render/EquipmentRenderer.java to the 1.20.6 shared sources --- .../client/render/EquipmentRenderer.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java new file mode 100644 index 00000000..25766956 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/EquipmentRenderer.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; + +/** + * Equipment drawn as one model from the models root with the texture of the same name, turned to + * the entity's heading: the container, {@code MCH_RenderContainer}. + */ +public class EquipmentRenderer extends EntityRenderer { + private final String model; + private final ResourceLocation texture; + private final float yOffset; + private final int tint; + + /** + * @param model the file name under {@code models/}, without extension + * @param yOffset how far the model sits below the entity's position + * @param tint ARGB the model is multiplied by; the original greyed the container a little + */ + public EquipmentRenderer(EntityRendererProvider.Context context, String model, float yOffset, int tint, float shadow) { + super(context); + this.model = model; + this.texture = new ResourceLocation(MCHeli.MOD_ID, "textures/" + model + ".png"); + this.yOffset = yOffset; + this.tint = tint; + this.shadowRadius = shadow; + } + + @Override + public void render(T entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + Mesh mesh = MeshCache.INSTANCE.get("", model); + if (mesh != null) { + pose.pushPose(); + pose.translate(0.0F, -yOffset, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + pose.mulPose(Axis.XP.rotationDegrees(entity.getViewXRot(partialTicks))); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(texture)), light, OverlayTexture.NO_OVERLAY, tint); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(T entity) { + return texture; + } +} From 279a7fd823faffe08d8f90613e2091b061aa98f6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:18 +0800 Subject: [PATCH 1192/1461] Add client/render/FlareRenderer.java to the 1.20.6 shared sources --- .../mcheli/client/render/FlareRenderer.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java new file mode 100644 index 00000000..4d577f09 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Flares and the burning debris of a crash were only their particle trails. Not in the original: + * a burning flare now glows, which is what makes a flare salvo light up the night sky. Smoke + * grenades stay invisible. + */ +public class FlareRenderer extends EntityRenderer { + private static final float RADIUS = 1.4F; + + public FlareRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + @Override + public void render(FlareEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + float glow = entity.isSmoke() || entity.isInWater() ? 0.0F : Glow.strength(entity.level(), entity.blockPosition()); + // A cheap waver keyed to the entity, so neighbouring flares do not pulse in step. + float flicker = 0.8F + 0.2F * (float) Math.sin((entity.tickCount + partialTicks) * 1.7F + entity.getId()); + Quaternionf camera = entityRenderDispatcher.cameraOrientation(); + Glow.halo(pose, buffers, camera, Vec3.ZERO, RADIUS * flicker, GlowParticleOptions.FIRE, glow); + Glow.halo(pose, buffers, camera, Vec3.ZERO, RADIUS * 0.35F, GlowParticleOptions.FLASH, glow); + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(FlareEntity entity) { + return Glow.TEXTURE; + } +} From 5be9f0c43922a2fe229fe7d4b0ef6b53cebcaec7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:18 +0800 Subject: [PATCH 1193/1461] Add client/render/GlassSplit.java to the 1.20.6 shared sources --- .../mcheli/client/render/GlassSplit.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java new file mode 100644 index 00000000..5be980cd --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Sorts a model's triangles into opaque ones and see-through glass, by the alpha of the texture + * under each triangle. + * + *

Drawing a whole vehicle translucent made its glass write depth before the game drew water, + * so water behind a canopy vanished. Opaque triangles now draw as cutout, like any entity, and + * only the glass draws translucent, through a type that leaves the depth buffer alone. + */ +final class GlassSplit { + static final GlassSplit INSTANCE = new GlassSplit(); + private static final Logger LOGGER = LoggerFactory.getLogger(GlassSplit.class); + /** Texels between these alphas are glass; fully clear ones are cut out, solid ones opaque. */ + private static final int MIN_GLASS_ALPHA = 10; + private static final int MAX_GLASS_ALPHA = 245; + + record Split(List opaque, List glass) { + static final Split EMPTY = new Split(List.of(), List.of()); + } + + private record Alpha(int width, int height, byte[] values) { + int at(float u, float v) { + int x = Math.floorMod((int) Math.floor(u * width), width); + int y = Math.floorMod((int) Math.floor(v * height), height); + return values[y * width + x] & 0xFF; + } + } + + private final Map> textures = new HashMap<>(); + private final Map, Map> splits = new IdentityHashMap<>(); + private final Map, Map, List>> buckets = new IdentityHashMap<>(); + + private GlassSplit() { + } + + /** Called from the render thread only. */ + Split split(List groups, ResourceLocation texture) { + if (groups.isEmpty()) { + return Split.EMPTY; + } + return splits.computeIfAbsent(groups, g -> new HashMap<>()).computeIfAbsent(texture, t -> compute(groups, t)); + } + + /** + * A part's objects by the texture each draws with, for a vehicle with {@code PartTexture} lines. + * The lists stay the same objects from frame to frame, so {@link #split} can remember them too. + * Called from the render thread only. + */ + List buckets(List groups, Map overrides) { + return buckets.computeIfAbsent(groups, g -> new HashMap<>()) + .computeIfAbsent(Map.copyOf(overrides), o -> PartTextures.assign(groups, o)); + } + + void clear() { + textures.clear(); + splits.clear(); + buckets.clear(); + } + + private Split compute(List groups, ResourceLocation texture) { + Alpha alpha = textures.computeIfAbsent(texture, GlassSplit::load).orElse(null); + if (alpha == null) { + return new Split(groups, List.of()); + } + List opaque = new ArrayList<>(); + List glass = new ArrayList<>(); + for (MeshGroup group : groups) { + split(group, alpha, opaque, glass); + } + return new Split(List.copyOf(opaque), List.copyOf(glass)); + } + + private static void split(MeshGroup group, Alpha alpha, List opaque, List glass) { + float[] uv = group.uvs(); + int triangles = group.vertexCount() / 3; + boolean[] isGlass = new boolean[triangles]; + int glassCount = 0; + for (int t = 0; t < triangles; t++) { + int i = t * 6; + float u = (uv[i] + uv[i + 2] + uv[i + 4]) / 3.0F; + float v = (uv[i + 1] + uv[i + 3] + uv[i + 5]) / 3.0F; + int a = alpha.at(u, v); + if (a > MIN_GLASS_ALPHA && a < MAX_GLASS_ALPHA) { + isGlass[t] = true; + glassCount++; + } + } + if (glassCount == 0) { + opaque.add(group); + } else if (glassCount == triangles) { + glass.add(group); + } else { + opaque.add(pick(group, isGlass, false, triangles - glassCount)); + glass.add(pick(group, isGlass, true, glassCount)); + } + } + + private static MeshGroup pick(MeshGroup group, boolean[] isGlass, boolean wanted, int count) { + float[] p = new float[count * 9]; + float[] n = new float[count * 9]; + float[] uv = new float[count * 6]; + int out = 0; + for (int t = 0; t < isGlass.length; t++) { + if (isGlass[t] != wanted) { + continue; + } + System.arraycopy(group.positions(), t * 9, p, out * 9, 9); + System.arraycopy(group.normals(), t * 9, n, out * 9, 9); + System.arraycopy(group.uvs(), t * 6, uv, out * 6, 6); + out++; + } + return new MeshGroup(group.name(), p, n, uv); + } + + private static Optional load(ResourceLocation texture) { + try (InputStream in = RuntimeAssets.open(texture)) { + if (in == null) { + return Optional.empty(); + } + return Optional.of(read(in)); + } catch (IOException | RuntimeException e) { + LOGGER.warn("Could not read {} to find its glass; drawing it opaque", texture, e); + return Optional.empty(); + } + } + + private static Alpha read(InputStream in) throws IOException { + try (NativeImage image = NativeImage.read(in)) { + int w = image.getWidth(); + int h = image.getHeight(); + byte[] values = new byte[w * h]; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + values[y * w + x] = (byte) (image.getPixelRGBA(x, y) >>> 24); + } + } + return new Alpha(w, h, values); + } + } +} From aa687c86d138883770974ee133915f1aed3cf3e3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:19 +0800 Subject: [PATCH 1194/1461] Port the glow particle types and vertices to 1.20.6 --- .../mcheli/client/render/Glow.java | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java new file mode 100644 index 00000000..66ef04b9 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java @@ -0,0 +1,239 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.effect.NightGlow; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.client.particle.ParticleRenderType; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Not in the original: light that adds to the scene instead of covering it, for tracers, rocket + * motors, flares and fireballs. Everything here blends additively and leaves the depth buffer + * alone, so overlapping glows brighten each other and never cut holes in the smoke behind them. + */ +public final class Glow { + /** A white disc fading to nothing at the rim; also the {@code mcheli:glow} particle sprite. */ + public static final ResourceLocation TEXTURE = new ResourceLocation(MCHeli.MOD_ID, "textures/particle/glow.png"); + + /** The particle render type for {@code mcheli:glow}: additive, depth tested, no depth writes. */ + public static final ParticleRenderType PARTICLE_LAYER = new ParticleRenderType() { + @Override + public void begin(BufferBuilder builder, TextureManager textures) { + RenderSystem.depthMask(false); + RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_PARTICLES); + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE); + builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE); + } + + @Override + public void end(Tesselator tesselator) { + tesselator.end(); + } + + @Override + public String toString() { + return "MCHELI_GLOW"; + } + }; + + private static final RenderType HALO = Types.halo(); + + /** + * The particle render type for {@code mcheli:smoke}: vanilla's translucent particles with depth + * writes off. A puff's clear edge would otherwise hide the puffs drawn after it, and a smoke + * cloud is mostly overlapping puffs. + */ + public static final ParticleRenderType SMOKE_LAYER = new ParticleRenderType() { + @Override + public void begin(BufferBuilder builder, TextureManager textures) { + RenderSystem.depthMask(false); + RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_PARTICLES); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE); + } + + @Override + public void end(Tesselator tesselator) { + tesselator.end(); + } + + @Override + public String toString() { + return "MCHELI_SMOKE"; + } + }; + + private static final int FULL_BRIGHT = LightTexture.FULL_BRIGHT; + + private Glow() { + } + + /** + * How strongly something at {@code pos} glows: faint by day, full at night, in caves and + * under water. 0 when the player turned the glow off. + */ + public static float strength(Level level, BlockPos pos) { + if (!ClientConfig.nightGlow()) { + return 0.0F; + } + return NightGlow.strength(level.getBrightness(LightLayer.BLOCK, pos), level.getBrightness(LightLayer.SKY, pos), level.getSkyDarken()); + } + + /** + * A disc of light facing the camera, centred {@code offset} away from the current pose. + * + * @param camera the camera's orientation, from the entity render dispatcher + * @param color RGB; {@code strength} becomes the alpha, which scales the light added + */ + public static void halo(PoseStack pose, MultiBufferSource buffers, Quaternionf camera, Vec3 offset, float radius, int color, + float strength) { + if (strength <= 0.0F || radius <= 0.0F) { + return; + } + int argb = FastColor.ARGB32.color(alpha(strength), color); + pose.pushPose(); + pose.translate(offset.x, offset.y, offset.z); + pose.mulPose(camera); + PoseStack.Pose p = pose.last(); + VertexConsumer buffer = buffers.getBuffer(HALO); + // Both windings, so culling never hides it whichever way the quad ends up facing. + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + pose.popPose(); + } + + /** + * A tracer: a ribbon of light from the current pose back along {@code tail}, turned to face + * the camera, bright at the head and fading out towards the end. + * + * @param toCamera from the camera to the head, so the ribbon can be turned side on to the view + */ + public static void streak(PoseStack pose, MultiBufferSource buffers, Vec3 toCamera, Vec3 tail, float width, int color, float strength) { + if (strength <= 0.0F || tail.lengthSqr() < 1.0E-4) { + return; + } + Vec3 side = tail.cross(toCamera); + if (side.lengthSqr() < 1.0E-8) { + // Seen exactly end on; the halo at the head covers this. + return; + } + side = side.normalize().scale(width); + int head = FastColor.ARGB32.color(alpha(strength), color); + int end = FastColor.ARGB32.color(0, color); + float sx = (float) side.x; + float sy = (float) side.y; + float sz = (float) side.z; + float tx = (float) tail.x; + float ty = (float) tail.y; + float tz = (float) tail.z; + PoseStack.Pose p = pose.last(); + VertexConsumer buffer = buffers.getBuffer(HALO); + // The middle row of the disc texture gives the ribbon soft edges. + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + } + + /** + * A search light's beam, {@code renderLight}: a cone {@code height} long along +Y to a disc + * {@code radius} across, fading from {@code start} at the lamp to {@code end} at the rim (ARGB). + */ + public static void cone(PoseStack pose, MultiBufferSource buffers, float height, float radius, int start, int end) { + VertexConsumer buffer = buffers.getBuffer(HALO); + PoseStack.Pose p = pose.last(); + int segments = 24; + for (int i = 0; i < segments; i++) { + float a0 = i * Mth.TWO_PI / segments; + float a1 = (i + 1) * Mth.TWO_PI / segments; + // A quad with the apex twice is the fan's triangle; the texture's bright middle colours it. + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, 0.0F, 0.0F, 0.0F, 0.5F, 0.5F, start); + vertex(buffer, p, Mth.sin(a0) * radius, height, Mth.cos(a0) * radius, 0.5F, 0.5F, end); + vertex(buffer, p, Mth.sin(a1) * radius, height, Mth.cos(a1) * radius, 0.5F, 0.5F, end); + } + } + + private static int alpha(float strength) { + return Math.clamp(Math.round(strength * 255.0F), 0, 255); + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float x, float y, float z, float u, float v, int argb) { + buffer.vertex(pose, x, y, z) + .color(argb) + .uv(u, v) + .overlayCoords(OverlayTexture.NO_OVERLAY) + .uv2(FULL_BRIGHT) + .normal(pose, 0.0F, 1.0F, 0.0F).endVertex(); + } + + /** + * Builds the halo render type. A subclass only to reach the protected render state shards; + * {@code RenderType.create} is not public on every loader, the constructor is. + */ + private abstract static class Types extends RenderType { + private Types() { + super("", DefaultVertexFormat.NEW_ENTITY, VertexFormat.Mode.QUADS, 0, false, false, () -> { + }, () -> { + }); + } + + /** The eyes shader (full bright, no light map), additive, depth tested without depth writes. */ + static RenderType halo() { + RenderStateShard[] shards = { + RENDERTYPE_EYES_SHADER, + new TextureStateShard(TEXTURE, false, false), + LIGHTNING_TRANSPARENCY, + COLOR_WRITE, + LEQUAL_DEPTH_TEST + }; + return new RenderType(MCHeli.MOD_ID + "_glow", DefaultVertexFormat.NEW_ENTITY, VertexFormat.Mode.QUADS, + TRANSIENT_BUFFER_SIZE, false, true, + () -> { + for (RenderStateShard shard : shards) { + shard.setupRenderState(); + } + }, + () -> { + for (RenderStateShard shard : shards) { + shard.clearRenderState(); + } + }) { + }; + } + } +} From d07041b0ba717112d17a9c744b6381a032d85bd8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:19 +0800 Subject: [PATCH 1195/1461] Add client/render/GltdRenderer.java to the 1.20.6 shared sources --- .../mcheli/client/render/GltdRenderer.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java new file mode 100644 index 00000000..fee03024 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/GltdRenderer.java @@ -0,0 +1,89 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * {@code MCH_RenderGLTD}: the tripod, its head turned to where the viewer aims, and a faint red + * laser out of the head while someone uses it. The viewer's own head part is left out in first + * person, where it would fill the view. + */ +public class GltdRenderer extends EntityRenderer { + private static final ResourceLocation TEXTURE = new ResourceLocation(MCHeli.MOD_ID, "textures/gltd.png"); + private static final float HEAD_PIVOT = 0.45F; + private static final double LASER_LENGTH = 100.0; + private static final int LASER = 0xFF2020; + + public GltdRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + @Override + public void render(GltdEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + float bodyYaw = entity.getViewYRot(partialTicks); + Entity viewer = entity.getFirstPassenger(); + Minecraft mc = Minecraft.getInstance(); + boolean drawHead = viewer == null || viewer != mc.player || !mc.options.getCameraType().isFirstPerson(); + float headYaw = viewer != null ? viewer.getViewYRot(partialTicks) : bodyYaw; + float headPitch = viewer != null ? viewer.getViewXRot(partialTicks) : 0.0F; + Mesh mesh = MeshCache.INSTANCE.get("", "gltd"); + if (mesh != null) { + VertexConsumer buffer = buffers.getBuffer(RenderType.entityCutout(TEXTURE)); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-bodyYaw)); + draw(mesh.part("$body"), pose, buffer, light); + pose.popPose(); + if (drawHead) { + pose.pushPose(); + pose.translate(0.0F, HEAD_PIVOT, 0.0F); + pose.mulPose(Axis.YP.rotationDegrees(-headYaw)); + pose.mulPose(Axis.XP.rotationDegrees(headPitch)); + pose.translate(0.0F, -HEAD_PIVOT, 0.0F); + draw(mesh.part("$head"), pose, buffer, light); + pose.popPose(); + } + } + if (viewer != null) { + float yaw = headYaw * Mth.DEG_TO_RAD; + float pitch = headPitch * Mth.DEG_TO_RAD; + Vec3 look = new Vec3(-Mth.sin(yaw) * Mth.cos(pitch), -Mth.sin(pitch), Mth.cos(yaw) * Mth.cos(pitch)); + Vec3 head = new Vec3(0.0, HEAD_PIVOT + 0.2, 0.0); + Vec3 toCamera = entity.getPosition(partialTicks).add(head).subtract(entityRenderDispatcher.camera.getPosition()); + pose.pushPose(); + pose.translate(head.x, head.y, head.z); + // Brightest at the tripod, fading out along the beam. + Glow.streak(pose, buffers, toCamera, look.scale(LASER_LENGTH), 0.02F, LASER, 0.25F); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(GltdEntity entity) { + return TEXTURE; + } + + private static void draw(List groups, PoseStack pose, VertexConsumer buffer, int light) { + if (!groups.isEmpty()) { + MeshRenderer.emit(groups, pose.last(), buffer, light, OverlayTexture.NO_OVERLAY); + } + } +} From 294bf2245fb52ea7bab0b33290aa4c30593f12c6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:20 +0800 Subject: [PATCH 1196/1461] Add client/render/MeshCache.java to the 1.20.6 shared sources --- .../mcheli/client/render/MeshCache.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java new file mode 100644 index 00000000..61c12d04 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshLoader; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Lazily loads vehicle models, from the resource packs (so they can replace them) or else from the + * runtime roots of {@link RuntimeAssets}. Models are parsed the first time a vehicle of that kind + * is drawn, and dropped on resource reload and on content reload ({@link #clear}). + */ +public final class MeshCache implements ResourceManagerReloadListener { + public static final MeshCache INSTANCE = new MeshCache(); + private static final Logger LOGGER = LoggerFactory.getLogger(MeshCache.class); + private static final String[] EXTENSIONS = {".mqo", ".obj"}; + + private final Map> meshes = new ConcurrentHashMap<>(); + + private MeshCache() { + } + + /** + * @param folder content folder, for example {@code helicopters}, or empty for the models root + * @param name definition name + * @return the mesh, or {@code null} if no model exists or it failed to load + */ + public @Nullable Mesh get(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, true)).orElse(null); + } + + /** Like {@link #get}, for optional models such as separate part files; a missing one is not logged. */ + public @Nullable Mesh find(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, false)).orElse(null); + } + + private static Optional load(String folder, String name, boolean required) { + for (String extension : EXTENSIONS) { + ResourceLocation id = new ResourceLocation(MCHeli.MOD_ID, + (folder.isEmpty() ? "models/" : "models/" + folder + "/") + name + extension); + try (InputStream in = RuntimeAssets.open(id)) { + if (in == null) { + continue; + } + Mesh mesh = MeshLoader.load(id.toString(), in); + LOGGER.debug("Loaded {}", mesh); + return Optional.of(mesh); + } catch (IOException | RuntimeException e) { + LOGGER.error("Failed to load model {}", id, e); + return Optional.empty(); + } + } + if (required) { + LOGGER.warn("No model found for {}/{}", folder, name); + } + return Optional.empty(); + } + + /** Forgets every model and glass split, so the next frame reads them again from disk. */ + public void clear() { + meshes.clear(); + GlassSplit.INSTANCE.clear(); + } + + @Override + public void onResourceManagerReload(ResourceManager resourceManager) { + clear(); + RuntimeAssets.clearTextures(); + } +} From fd340731dbe9c2749776749228084a3357799418 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:20 +0800 Subject: [PATCH 1197/1461] Write mesh vertices with the older builder of 1.20.6 --- .../mcheli/client/render/MeshRenderer.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java new file mode 100644 index 00000000..92b04403 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java @@ -0,0 +1,88 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.List; + +/** Streams {@link MeshGroup} triangles into an entity render type. */ +final class MeshRenderer { + private static final int WHITE = 0xFFFFFFFF; + + private MeshRenderer() { + } + + /** + * Entity render types draw quads, so each triangle is sent as a quad whose last corner repeats + * the third one. The GPU discards the degenerate half for free. + */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay) { + emit(groups, pose, buffer, light, overlay, WHITE); + } + + /** As above, tinted with an ARGB colour, for bullets drawn in their {@code BulletColor}. */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + /** + * As above, but only the triangles that face the camera. No vanilla type is both culled and + * free of depth writes, so glass is culled here. Back to back glass faces would otherwise sort + * in an arbitrary order and swap places as the view moves. + * + *

Entities are submitted in world-aligned space with the camera at the origin, so a front + * face (counter-clockwise, as in the original's GL setup) is one whose normal points back + * towards the origin. + */ + static void emitFrontFaces(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + Matrix4f m = pose.pose(); + Vector3f a = new Vector3f(); + Vector3f b = new Vector3f(); + Vector3f c = new Vector3f(); + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + m.transformPosition(p[v * 3], p[v * 3 + 1], p[v * 3 + 2], a); + m.transformPosition(p[v * 3 + 3], p[v * 3 + 4], p[v * 3 + 5], b); + m.transformPosition(p[v * 3 + 6], p[v * 3 + 7], p[v * 3 + 8], c); + b.sub(a); + c.sub(a); + if (b.cross(c).dot(a) >= 0.0F) { + continue; + } + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float[] p, float[] n, float[] uv, int i, int light, int overlay, int color) { + int a = i * 3; + int t = i * 2; + buffer.vertex(pose, p[a], p[a + 1], p[a + 2]) + .color(color) + .uv(uv[t], uv[t + 1]) + .overlayCoords(overlay) + .uv2(light) + .normal(pose, n[a], n[a + 1], n[a + 2]).endVertex(); + } +} From a2705fd16d203aa7662f350acec956cd0c62774e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:21 +0800 Subject: [PATCH 1198/1461] Add client/render/ParachuteRenderer.java to the 1.20.6 shared sources --- .../client/render/ParachuteRenderer.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java new file mode 100644 index 00000000..32e51b79 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ParachuteRenderer.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +/** + * {@code MCH_RenderParachute}: the item's round canopy, or the ejection seat with its canopy, + * from the models the import brought in. + */ +public class ParachuteRenderer extends EntityRenderer { + private static final ResourceLocation ROUND = new ResourceLocation(MCHeli.MOD_ID, "textures/parachute1.png"); + private static final ResourceLocation SEAT = new ResourceLocation(MCHeli.MOD_ID, "textures/parachute2.png"); + + public ParachuteRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.5F; + } + + @Override + public void render(ParachuteEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + List groups = groups(entity); + if (!groups.isEmpty()) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + MeshRenderer.emit(groups, pose.last(), buffers.getBuffer(RenderType.entityCutout(getTextureLocation(entity))), light, OverlayTexture.NO_OVERLAY); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(ParachuteEntity entity) { + return entity.kind() == ParachuteEntity.Kind.ITEM ? ROUND : SEAT; + } + + private static List groups(ParachuteEntity entity) { + if (entity.kind() == ParachuteEntity.Kind.ITEM) { + Mesh mesh = MeshCache.INSTANCE.get("", "parachute1"); + return mesh != null ? mesh.groups() : List.of(); + } + Mesh mesh = MeshCache.INSTANCE.get("", "parachute2"); + boolean canopy = entity.kind() == ParachuteEntity.Kind.DROP || entity.isOpen(); + return mesh != null ? mesh.part(canopy ? "$parachute" : "$seat") : List.of(); + } +} From c05d7dee3b8db2bf5f28f7df3393a20746a253ae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:21 +0800 Subject: [PATCH 1199/1461] Add client/render/PartRenderer.java to the 1.20.6 shared sources --- .../mcheli/client/render/PartRenderer.java | 557 ++++++++++++++++++ 1 file changed, 557 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java new file mode 100644 index 00000000..d8e8cd0f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java @@ -0,0 +1,557 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.CameraPart; +import com.jnhprojects.mcheli.content.aircraft.part.CrawlerTrack; +import com.jnhprojects.mcheli.content.aircraft.part.LandingGearPart; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.content.aircraft.part.PropellerPart; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import com.jnhprojects.mcheli.content.aircraft.part.ThrottlePart; +import com.jnhprojects.mcheli.content.aircraft.part.TrackRoller; +import com.jnhprojects.mcheli.content.aircraft.part.VehiclePart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.content.aircraft.part.WheelPart; +import com.jnhprojects.mcheli.content.aircraft.part.WingPart; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.jnhprojects.mcheli.model.PartTextures; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import org.joml.Vector3f; + +import java.util.List; + +/** + * Submits a vehicle's hull and moving parts for one frame. A port of the part routines in + * {@code MCH_RenderAircraft} and the per kind renderers; every transform is applied in hull space + * on top of the pose the caller already set up. + * + *

A part is looked up as {@code $name} inside the vehicle model, or as a separate + * {@code _} model file, the two places the original accepted. + */ +final class PartRenderer { + private static final Vector3 X_AXIS = new Vector3(1, 0, 0); + private static final Vector3 DOWN_AXIS = new Vector3(0, -1, 0); + + private final AircraftDefinition def; + private final Mesh mesh; + private final AircraftRenderState state; + private final PoseStack pose; + private final MultiBufferSource buffers; + private final ResourceLocation texture; + private final RenderType opaqueType; + private final RenderType glassType; + private final int light; + private final int overlay; + /** Glass draws unlit, so its colour carries the brightness of where the vehicle stands. */ + private final int glassColor; + + PartRenderer(AircraftDefinition def, Mesh mesh, AircraftRenderState state, PoseStack pose, MultiBufferSource buffers, + ResourceLocation texture, int light, int overlay, int glassColor) { + this.def = def; + this.mesh = mesh; + this.state = state; + this.pose = pose; + this.buffers = buffers; + this.texture = texture; + // Culled, as the original drew it. The models are full of back to back faces for thin + // panels, and drawing both sides of those makes them fight for the same depth. + this.opaqueType = RenderType.entityCutout(texture); + // The one entity translucent type that does not write depth, so water behind glass still + // draws. It does not cull, so MeshRenderer drops the back faces itself. + this.glassType = RenderType.entityTranslucentEmissive(texture); + this.light = light; + this.overlay = overlay; + this.glassColor = glassColor; + } + + void submitAll() { + switch (def) { + case PlaneDefinition plane -> { + nozzles(plane); + wings(plane); + propellers(plane); + hull(); + } + case HelicopterDefinition heli -> { + hull(); + rotors(heli); + } + case VehicleDefinition vehicle -> { + hull(); + for (VehiclePart part : vehicle.parts) { + vehiclePart(vehicle, part); + } + } + default -> hull(); + } + weapons(); + rotatingParts(); + movingParts(def.hatches, state.hatch); + trackRollers(); + crawlerTracks(); + steeringWheels(); + wheels(); + throttles(); + cameras(); + landingGear(); + weaponBays(); + movingParts(def.canopies, state.canopy); + } + + /** + * The body and, as a {@code $} object of its own so it can have its own {@code PartTexture}, + * the cockpit. A model without {@code $body} draws as one piece, cockpit included, and no original + * model has a {@code $cockpit}. + */ + private void hull() { + submit(mesh.body()); + if (mesh.contains("$body")) { + submit(mesh.part("$cockpit")); + } + } + + // Kind specific parts + + private void rotors(HelicopterDefinition heli) { + for (int r = 0; r < heli.rotors.size(); r++) { + RotorDefinition rotor = heli.rotors.get(r); + List blade = part(rotor.part()); + if (blade.isEmpty() || isZero(rotor.axis())) { + continue; + } + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + int blades = Math.max(rotor.blades(), 1); + for (int i = 0; i < blades; i++) { + pose.pushPose(); + Vector3 p = rotor.position(); + translate(p, 1.0F); + float[] angles = r < state.bladeAngles.length ? state.bladeAngles[r] : null; + rotate(angles != null && i < angles.length ? angles[i] : state.rotorAngle + i * step, rotor.axis()); + if (!rotor.legacyPivot()) { + translate(p, -1.0F); + } + submit(blade); + pose.popPose(); + } + } + } + + private void nozzles(PlaneDefinition plane) { + for (MovingPart nozzle : plane.nozzles) { + pose.pushPose(); + rotateAround(nozzle.position(), state.nozzle, nozzle.axis()); + submit(part(nozzle.part())); + pose.popPose(); + } + } + + private void wings(PlaneDefinition plane) { + for (WingPart wing : plane.wings) { + pose.pushPose(); + rotateAround(wing.position(), state.wing * wing.factor(), wing.axis()); + submit(part(wing.part())); + for (MovingPart pylon : wing.pylons()) { + pose.pushPose(); + rotateAround(pylon.position(), state.wing * pylon.factor(), pylon.axis()); + submit(part(pylon.part())); + pose.popPose(); + } + pose.popPose(); + } + } + + /** Tilt rotors follow the nozzle state, the blades spin with the engine. */ + private void propellers(PlaneDefinition plane) { + if (!plane.hasNozzleState()) { + return; + } + for (PropellerPart rotor : plane.propellers) { + pose.pushPose(); + rotateAround(rotor.position(), state.nozzle * rotor.factor(), rotor.axis()); + submit(part(rotor.part())); + for (PropellerPart.Blade blade : rotor.blades()) { + List groups = part(blade.part()); + pose.pushPose(); + rotateAround(blade.position(), state.rotorAngle, blade.axis()); + for (int i = 0; i < blade.count(); i++) { + rotateAround(blade.position(), blade.step(), blade.axis()); + submit(groups); + } + pose.popPose(); + } + pose.popPose(); + } + } + + /** {@code MCH_RenderVehicle.drawPart}: mount parts turn with the gunner's view. */ + private void vehiclePart(VehicleDefinition vehicle, VehiclePart part) { + pose.pushPose(); + if (part.yaw() || part.pitch() || part.type() == VehiclePart.TYPE_ROTATING_BARREL) { + translate(part.position(), 1.0F); + if (part.yaw()) { + rotate(seatYaw(0), DOWN_AXIS); + } + if (part.pitch()) { + rotate(Mth.clamp(seatPitch(0), vehicle.minRotationPitch, vehicle.maxRotationPitch), X_AXIS); + } + translate(part.position(), -1.0F); + } + for (VehiclePart child : part.children()) { + vehiclePart(vehicle, child); + } + boolean gunnerLooking = state.localSeat == 0 && state.localFirstPerson; + if (part.drawFirstPerson() || !gunnerLooking) { + submit(part(part.part())); + } + pose.popPose(); + } + + // Parts every kind can have + + /** + * Gun mounts, turrets and missiles, {@code renderWeapon}. Each follows its weapon's aim, kicks + * back as it fires, spins its barrels and, for missiles, disappears until reloaded. A part + * whose weapon is missing follows the view of the seat that would fire it. + */ + private void weapons() { + float turretYaw = seatYaw(0); + for (int i = 0; i < def.weaponParts.size(); i++) { + WeaponPart w = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState ws = i < state.weaponParts.length ? state.weaponParts[i] : null; + boolean live = ws != null && ws.hasWeapon; + WeaponMount mount = mount(w); + int seat = mount != null ? mount.seatIndex() : 0; + if (w.hideInFirstPerson() && state.localFirstPerson && state.localSeat == seat) { + continue; + } + float defaultYaw = live ? ws.defaultYaw : mount != null ? mount.defaultYaw() : 0.0F; + float yaw; + float pitch; + if (live) { + yaw = ws.yaw; + pitch = ws.pitch; + } else { + yaw = Mth.wrapDegrees(seatYaw(seat) - defaultYaw); + pitch = seatPitch(seat); + } + + pose.pushPose(); + if (w.turret()) { + rotateAround(def.turretPosition, turretYaw, DOWN_AXIS); + } + translate(w.position(), 1.0F); + if (w.yaw()) { + rotate(w.turret() && !live ? yaw - turretYaw : yaw, DOWN_AXIS); + } + if (w.turret() && live) { + rotate(-(turretYaw - ws.turretLag), DOWN_AXIS); + } + // Side facing guns pitch around their own axis, which the original did by turning + // into the gun's frame and flipping the sign for guns pointing left or right. + boolean sideways = false; + if ((int) defaultYaw != 0) { + float t = Mth.wrapDegrees(defaultYaw); + sideways = (t >= 45.0F && t <= 135.0F) || (t <= -45.0F && t >= -135.0F); + rotate(-defaultYaw, DOWN_AXIS); + } + if (w.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && w.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, w.recoil() * ws.recoil); + } + if (mount != null || live) { + rotate(defaultYaw, DOWN_AXIS); + } + if (live && w.rotatingBarrel()) { + rotate(ws.barrel, w.axis()); + } + translate(w.position(), -1.0F); + if (!w.missile() || !live || !ws.hidden) { + submit(part(w.part())); + for (WeaponPart.Child child : w.children()) { + pose.pushPose(); + translate(child.position(), 1.0F); + if (child.yaw()) { + rotate(yaw, DOWN_AXIS); + } + if (live && (int) defaultYaw != 0) { + rotate(-defaultYaw, DOWN_AXIS); + } + if (child.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && child.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, -child.recoil() * ws.recoil); + } + if (live && (int) defaultYaw != 0) { + rotate(defaultYaw, DOWN_AXIS); + } + translate(child.position(), -1.0F); + submit(part(child.part())); + pose.popPose(); + } + } + pose.popPose(); + } + } + + private WeaponMount mount(WeaponPart part) { + if (part.weapons().isEmpty()) { + return null; + } + int group = def.weaponGroup(part.weapons().getFirst()); + return group >= 0 ? def.weaponGroups.get(group).mounts().getFirst() : null; + } + + private void rotatingParts() { + for (int i = 0; i < def.rotatingParts.size() && i < state.rotatingParts.length; i++) { + RotatingPart part = def.rotatingParts.get(i); + pose.pushPose(); + rotateAround(part.position(), state.rotatingParts[i], part.axis()); + submit(part(part.part())); + pose.popPose(); + } + } + + /** Hatches and canopies: swing by {@code angle * factor}, or slide along their offset. */ + private void movingParts(List parts, float angle) { + for (MovingPart part : parts) { + pose.pushPose(); + moveOrSwing(part, angle); + submit(part(part.part())); + pose.popPose(); + } + } + + private void moveOrSwing(MovingPart part, float angle) { + if (part.slide()) { + translate(part.position(), angle / 90.0F); + } else { + rotateAround(part.position(), angle * part.factor(), part.axis()); + } + } + + private void trackRollers() { + for (TrackRoller roller : def.trackRollers) { + pose.pushPose(); + rotateAround(roller.position(), state.roller[roller.side()], X_AXIS); + submit(part(roller.part())); + pose.popPose(); + } + } + + /** + * Each link slides towards the next one by the track's run, so the loop appears to move. + * The link part is modelled at the origin and placed at each point of the outline. + */ + private void crawlerTracks() { + for (CrawlerTrack track : def.crawlerTracks) { + List link = part(track.part()); + List links = track.links(); + int count = links.size() - 1; + if (link.isEmpty() || count < 1) { + continue; + } + float run = state.track[track.side()]; + for (int i = 0; i < count; i++) { + CrawlerTrack.Link cur = links.get(i); + CrawlerTrack.Link next = links.get((i + 1) % count); + float angleNext = next.angle(); + if (angleNext - cur.angle() < -180.0F) { + angleNext += 360.0F; + } else if (angleNext - cur.angle() > 180.0F) { + angleNext -= 360.0F; + } + pose.pushPose(); + pose.translate(0.0F, cur.y() + (next.y() - cur.y()) * run, cur.z() + (next.z() - cur.z()) * run); + pose.mulPose(Axis.XN.rotationDegrees(cur.angle() + (angleNext - cur.angle()) * run)); + submit(link); + pose.popPose(); + } + } + } + + private void steeringWheels() { + for (WheelPart wheel : def.steeringWheels) { + pose.pushPose(); + rotateAround(wheel.position(), state.steer * wheel.steerAngle(), wheel.axis()); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void wheels() { + for (WheelPart wheel : def.wheels) { + pose.pushPose(); + rotateAround(wheel.pivot(), state.steer * wheel.steerAngle(), wheel.axis()); + rotateAround(wheel.position(), state.wheelRoll, X_AXIS); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void throttles() { + for (ThrottlePart lever : def.throttleParts) { + pose.pushPose(); + rotateAround(lever.position(), state.throttle * lever.angle(), lever.axis()); + translate(lever.slide(), state.throttle); + submit(part(lever.part())); + pose.popPose(); + } + } + + /** Sensor turrets look where the gunner looks, or the pilot when the gunner seat is empty. */ + private void cameras() { + int seat = state.seatOccupied.length > 1 && state.seatOccupied[1] ? 1 : 0; + for (CameraPart camera : def.cameraParts) { + pose.pushPose(); + translate(camera.position(), 1.0F); + if (camera.yawSync()) { + rotate(seatYaw(seat), DOWN_AXIS); + } + if (camera.pitchSync()) { + rotate(seatPitch(seat), X_AXIS); + } + translate(camera.position(), -1.0F); + submit(part(camera.part())); + pose.popPose(); + } + } + + /** + * Gear legs swing up with the gear state; reversed parts close as the gear rises, and gear bay + * doors open fully halfway through and shut again. + */ + private void landingGear() { + float angle = state.landingGear; + float reverse = 90.0F - angle; + float door = Math.min(90.0F, 90.0F * Mth.sin(angle * 2.0F * Mth.DEG_TO_RAD) * 3.0F); + for (LandingGearPart gear : def.landingGear) { + pose.pushPose(); + translate(gear.position(), 1.0F); + if (gear.reverse()) { + rotate(reverse * gear.factor(), gear.axis()); + } else { + rotate((gear.hatch() ? door : angle) * gear.factor(), gear.axis()); + } + if (gear.secondAxis() != null) { + rotate((gear.reverse() ? reverse : angle) * gear.secondFactor(), gear.secondAxis()); + } + translate(gear.position(), -1.0F); + if (gear.slide() != null) { + float f = angle / 90.0F; + translate(gear.slide(), gear.reverse() ? 1.0F - f : f); + } + submit(part(gear.part())); + pose.popPose(); + } + } + + /** Bays swing open while a crew member has one of their weapons selected. */ + private void weaponBays() { + for (int i = 0; i < def.weaponBays.size(); i++) { + WeaponBayPart bay = def.weaponBays.get(i); + pose.pushPose(); + moveOrSwing(bay.door(), i < state.bays.length ? state.bays[i] : 0.0F); + submit(part(bay.door().part())); + pose.popPose(); + } + } + + // Helpers + + private float seatYaw(int seat) { + return seat >= 0 && seat < state.seatYaw.length ? state.seatYaw[seat] : 0.0F; + } + + private float seatPitch(int seat) { + return seat >= 0 && seat < state.seatPitch.length ? state.seatPitch[seat] : 0.0F; + } + + private List part(String name) { + List groups = mesh.part("$" + name); + if (!groups.isEmpty()) { + return groups; + } + Mesh separate = MeshCache.INSTANCE.find(def.folder(), def.name() + "_" + name); + return separate != null ? separate.groups() : List.of(); + } + + private void submit(List groups) { + if (groups.isEmpty()) { + return; + } + if (def.partTextures.isEmpty()) { + submit(groups, texture, opaqueType, glassType); + return; + } + for (PartTextures.Bucket bucket : GlassSplit.INSTANCE.buckets(groups, def.partTextures)) { + if (bucket.texture() == null) { + submit(bucket.groups(), texture, opaqueType, glassType); + } else { + ResourceLocation own = RuntimeAssets.texture(new ResourceLocation(MCHeli.MOD_ID, + "textures/" + def.folder() + "/" + bucket.texture() + ".png")); + submit(bucket.groups(), own, RenderType.entityCutout(own), RenderType.entityTranslucentEmissive(own)); + } + } + } + + private void submit(List groups, ResourceLocation texture, RenderType opaqueType, RenderType glassType) { + GlassSplit.Split split = GlassSplit.INSTANCE.split(groups, texture); + if (!split.opaque().isEmpty()) { + MeshRenderer.emit(split.opaque(), pose.last(), buffers.getBuffer(opaqueType), light, overlay); + } + if (!split.glass().isEmpty()) { + VertexConsumer buffer = buffers.getBuffer(glassType); + // Culling glass relies on the camera sitting at the origin, which is not so in a GUI. + if (state.inGui) { + MeshRenderer.emit(split.glass(), pose.last(), buffer, light, overlay, glassColor); + } else { + MeshRenderer.emitFrontFaces(split.glass(), pose.last(), buffer, light, overlay, glassColor); + } + } + } + + private void translate(Vector3 v, float scale) { + pose.translate((float) v.x() * scale, (float) v.y() * scale, (float) v.z() * scale); + } + + private void rotateAround(Vector3 pivot, float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + translate(pivot, 1.0F); + rotate(degrees, axis); + translate(pivot, -1.0F); + } + + /** Like {@code glRotatef}: any axis length, a zero axis does nothing. */ + private void rotate(float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + Vector3f a = new Vector3f((float) axis.x(), (float) axis.y(), (float) axis.z()).normalize(); + pose.mulPose(Axis.of(a).rotationDegrees(degrees)); + } + + private static boolean isZero(Vector3 v) { + return v.x() * v.x() + v.y() * v.y() + v.z() * v.z() < 1.0E-12; + } +} From 8e9a8dea0fcb5463df7db3811dab6d77dfc1b0bd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:22 +0800 Subject: [PATCH 1200/1461] Add client/render/ProjectileRenderer.java to the 1.20.6 shared sources --- .../client/render/ProjectileRenderer.java | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java new file mode 100644 index 00000000..a4564d2e --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java @@ -0,0 +1,178 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.weapon.WeaponColor; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Draws rounds, rockets, missiles and bombs from {@code models/bullets}, tinted with the weapon's + * {@code BulletColor} (its in-water colour below the surface) and always fully lit, as the + * original's {@code MCH_RenderBulletBase} did. + * + *

Not in the original: rounds trail a tracer and rocket motors burn with a glow, faint by day + * and bright at night, so a firefight in the dark can be followed by eye. Nothing glows under + * water. + */ +public class ProjectileRenderer extends EntityRenderer { + private static final int FULL_BRIGHT = 0xF000F0; + /** Warm tracer colour for rounds whose weapon keeps the default white tint. */ + private static final int TRACER = 0xFFC46A; + /** The longest tracer ribbon, in blocks; fast rounds cover far more than this per tick. */ + private static final double MAX_TRACER = 6.0; + /** How far behind the projectile's centre the motor glow sits, in blocks. */ + private static final double MOTOR_OFFSET = 0.6; + + public ProjectileRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + enum GlowKind { + NONE, + TRACER, + MOTOR + } + + /** What 26.x splits off as the render state. */ + static class State { + double x; + double y; + double z; + String model = ""; + float yaw; + float pitch; + int color = 0xFFFFFFFF; + boolean visible = true; + GlowKind glowKind = GlowKind.NONE; + float glow; + int glowColor = TRACER; + /** Distance covered over the last tick, which points the tracer and the motor glow. */ + Vec3 travel = Vec3.ZERO; + } + + private final State state = new State(); + + private static void extractRenderState(ProjectileEntity entity, State state, float partialTicks) { + state.x = Mth.lerp(partialTicks, entity.xo, entity.getX()); + state.y = Mth.lerp(partialTicks, entity.yo, entity.getY()); + state.z = Mth.lerp(partialTicks, entity.zo, entity.getZ()); + state.model = entity.model(); + state.yaw = entity.getViewYRot(partialTicks); + state.pitch = entity.getViewXRot(partialTicks); + state.visible = !entity.isSpent() && (!(entity instanceof BombEntity bomb) || bomb.isVisible()); + WeaponDefinition w = entity.weapon(); + boolean inWater = inWater(entity.level(), entity); + WeaponColor c = w != null ? (inWater ? w.colorInWater : w.color) : WeaponColor.WHITE; + state.color = c.argb(); + + state.glowKind = inWater || entity.isSpent() ? GlowKind.NONE : glowKind(entity); + if (state.glowKind != GlowKind.NONE) { + state.glow = Glow.strength(entity.level(), entity.blockPosition()); + state.travel = new Vec3(entity.getX() - entity.xo, entity.getY() - entity.yo, entity.getZ() - entity.zo); + if (state.glowKind == GlowKind.MOTOR) { + state.glowColor = GlowParticleOptions.FIRE; + } else { + // A weapon with its own tint keeps it for the tracer; the default white burns warm. + int tint = state.color & 0xFFFFFF; + state.glowColor = tint == 0xFFFFFF ? TRACER : tint; + } + } + } + + private static GlowKind glowKind(ProjectileEntity entity) { + if (entity instanceof BulletEntity) { + return GlowKind.TRACER; + } + if (entity instanceof RocketEntity || entity instanceof MarkerRocketEntity || entity instanceof AAMissileEntity + || entity instanceof ASMissileEntity || entity instanceof ATMissileEntity || entity instanceof TvMissileEntity) { + return GlowKind.MOTOR; + } + return GlowKind.NONE; + } + + /** The original looked for water at the round and up to two blocks above it. */ + private static boolean inWater(Level level, ProjectileEntity e) { + for (int y = 0; y < 3; y++) { + if (level.getFluidState(BlockPos.containing(e.getX() + 0.5, e.getY() + 1.5 - y, e.getZ() + 0.5)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + @Override + public void render(ProjectileEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + extractRenderState(entity, state, partialTicks); + Mesh mesh = state.visible ? MeshCache.INSTANCE.find("bullets", state.model) : null; + if (mesh != null) { + ResourceLocation texture = new ResourceLocation(MCHeli.MOD_ID, "textures/bullets/" + state.model + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityTranslucentCull(texture)), FULL_BRIGHT, + OverlayTexture.NO_OVERLAY, state.color); + pose.popPose(); + } + if (state.visible) { + submitGlow(state, pose, buffers); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(ProjectileEntity entity) { + return new ResourceLocation(MCHeli.MOD_ID, "textures/bullets/" + entity.model() + ".png"); + } + + private Quaternionf camera() { + return entityRenderDispatcher.cameraOrientation(); + } + + private void submitGlow(State state, PoseStack pose, MultiBufferSource buffers) { + if (state.glowKind == GlowKind.NONE || state.glow <= 0.0F) { + return; + } + double speed = state.travel.length(); + Vec3 back = speed > 1.0E-3 ? state.travel.scale(-1.0 / speed) : Vec3.ZERO; + switch (state.glowKind) { + case TRACER -> { + Vec3 cameraPos = entityRenderDispatcher.camera.getPosition(); + Vec3 toCamera = new Vec3(state.x - cameraPos.x, state.y - cameraPos.y, state.z - cameraPos.z); + Glow.streak(pose, buffers, toCamera, back.scale(Math.min(speed, MAX_TRACER)), 0.08F, state.glowColor, state.glow); + Glow.halo(pose, buffers, camera(), Vec3.ZERO, 0.35F, state.glowColor, state.glow); + } + case MOTOR -> { + Vec3 nozzle = back.scale(MOTOR_OFFSET); + Glow.halo(pose, buffers, camera(), nozzle, 1.1F, state.glowColor, state.glow); + Glow.halo(pose, buffers, camera(), nozzle, 0.4F, GlowParticleOptions.FLASH, state.glow); + } + default -> { + } + } + } +} From 0fbe85e3f02362a613432f1fe46dbd01fa952f05 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:22 +0800 Subject: [PATCH 1201/1461] Add client/render/RiderPose.java to the 1.20.6 shared sources --- .../mcheli/client/render/RiderPose.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java new file mode 100644 index 00000000..98ff23fd --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java @@ -0,0 +1,40 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Tilts players riding a vehicle with its hull, as the original did from its render hook. + * + *

Vanilla keeps riders upright, so a banked plane left the pilot standing straight up through + * the canopy. The tilt is the hull's pitch and roll expressed in the world frame, applied about + * the rider's vehicle attachment point (the hip), which is the point that sits on the seat. The + * rider's own body yaw is applied by the renderer afterwards, so turning in the seat still works. + * + *

The entity render dispatcher pushes a pose around every entity, so nothing needs popping here. + */ +public final class RiderPose { + private RiderPose() { + } + + /** Called before a living entity is drawn, with the pose the renderer is about to use. */ + public static void beforeRender(LivingEntity rider, PoseStack pose, float partialTicks) { + if (!(rider instanceof Player) || !(rider.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + float yaw = vehicle.getViewYRot(partialTicks); + float pitch = vehicle.getViewXRot(partialTicks); + float roll = vehicle.roll(partialTicks); + if (pitch == 0.0F && roll == 0.0F) { + return; + } + Quaternionf tilt = AircraftRotation.orientation(yaw, pitch, roll).rotateY((float) Math.toRadians(yaw)); + Vec3 hip = rider.getVehicleAttachmentPoint(vehicle); + pose.rotateAround(tilt, (float) hip.x, (float) hip.y, (float) hip.z); + } +} From 7f61bf23f8cd84637acad581ad0ede1a501689d5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:23 +0800 Subject: [PATCH 1202/1461] Add client/render/ThrowableRenderer.java to the 1.20.6 shared sources --- .../client/render/ThrowableRenderer.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java new file mode 100644 index 00000000..1e003cb4 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/ThrowableRenderer.java @@ -0,0 +1,39 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +/** {@code MCH_RenderThrowable}: the grenade's own model from {@code models/throwable}. */ +public class ThrowableRenderer extends EntityRenderer { + public ThrowableRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + @Override + public void render(ThrowableEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + String name = entity.definitionName(); + Mesh mesh = name.isEmpty() ? null : MeshCache.INSTANCE.get("throwable", name); + if (mesh != null) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(getTextureLocation(entity))), light, OverlayTexture.NO_OVERLAY); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(ThrowableEntity entity) { + return new ResourceLocation(MCHeli.MOD_ID, "textures/throwable/" + entity.definitionName() + ".png"); + } +} From fa12c146289452cf8b76776cac3956d2e39c637f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:23 +0800 Subject: [PATCH 1203/1461] Add client/render/UavStationRenderer.java to the 1.20.6 shared sources --- .../client/render/UavStationRenderer.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java new file mode 100644 index 00000000..077cb1ea --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/render/UavStationRenderer.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +/** + * The UAV station and the portable controller, {@code MCH_RenderUavStation}. The station's screen + * lights up while a drone is flown from it; the controller's lid, keyboard half and display fold + * open as it is opened, and light up once fully open. + */ +public class UavStationRenderer extends EntityRenderer { + private static final String[] MODEL = {"uav_station", "uav_portable_controller"}; + /** The original greyed both a little. */ + private static final int TINT = 0xFFBFBFBF; + + public UavStationRenderer(EntityRendererProvider.Context context) { + super(context); + shadowRadius = 1.0F; + } + + @Override + public void render(UavStationEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + int index = entity.kind() == UavStationEntity.PORTABLE ? 1 : 0; + float cover = entity.cover(partialTicks); + boolean lit = index == 1 ? cover > 0.95F : entity.isLinked(); + Mesh mesh = MeshCache.INSTANCE.get("", MODEL[index]); + if (mesh != null) { + ResourceLocation texture = new ResourceLocation(MCHeli.MOD_ID, "textures/" + MODEL[index] + (lit ? "_on" : "") + ".png"); + VertexConsumer buffer = buffers.getBuffer(RenderType.entityCutout(texture)); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getViewYRot(partialTicks))); + if (index == 0) { + draw(pose, buffer, mesh.groups(), light); + } else { + draw(pose, buffer, mesh.part("$body"), light); + folding(pose, buffer, mesh.part("$cover"), cover * 60.0F, -0.1812F, -0.3186F, light); + folding(pose, buffer, mesh.part("$laptop_cover"), cover * 95.0F, -0.1808F, -0.0422F, light); + folding(pose, buffer, mesh.part("$display"), cover * -85.0F, -0.1807F, 0.2294F, light); + } + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(UavStationEntity entity) { + return new ResourceLocation(MCHeli.MOD_ID, "textures/" + MODEL[entity.kind() == UavStationEntity.PORTABLE ? 1 : 0] + ".png"); + } + + /** {@code renderRotPart}: turns a part about the x axis through its hinge. */ + private static void folding(PoseStack pose, VertexConsumer buffer, List part, float angle, float y, float z, int light) { + pose.pushPose(); + pose.translate(0.0F, y, z); + pose.mulPose(Axis.XN.rotationDegrees(angle)); + pose.translate(0.0F, -y, -z); + draw(pose, buffer, part, light); + pose.popPose(); + } + + private static void draw(PoseStack pose, VertexConsumer buffer, List groups, int light) { + if (!groups.isEmpty()) { + MeshRenderer.emit(groups, pose.last(), buffer, light, OverlayTexture.NO_OVERLAY, TINT); + } + } +} From 3b104c4f18390262e99eaa106a5812aa080eb34f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:23 +0800 Subject: [PATCH 1204/1461] Add client/screen/DraftingTableScreen.java to the 1.20.6 shared sources --- .../client/screen/DraftingTableScreen.java | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java new file mode 100644 index 00000000..52af9514 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/DraftingTableScreen.java @@ -0,0 +1,326 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.workshop.WorkshopHomeScreen; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.network.DraftingTablePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.item.crafting.ShapedRecipe; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * The drafting table's recipe browser, {@code MCH_DraftingTableGui}. Pick a list (helicopters, + * planes, tanks, ground vehicles or equipment), step through its recipes, and Create makes the one + * shown into the output slot. The right page shows the original's illustrated description for + * equipment and a vehicle's numbers for vehicles; the original turned a 3D model there instead. + * + *

The recipes come from the client's recipe manager, which the server fills on join. + */ +public class DraftingTableScreen extends AbstractContainerScreen { + private static final ResourceLocation BACKGROUND = new ResourceLocation(MCHeli.MOD_ID, "textures/gui/drafting_table.png"); + private static final int TEXTURE_SIZE = 512; + /** The description pages take this part of their 512 square texture. */ + private static final int PAGE_WIDTH = 340; + private static final int PAGE_HEIGHT = 380; + private static final int PAGE_X = 210; + private static final int PAGE_Y = 20; + private static final int GRID_X = 121; + private static final int GRID_Y = 34; + private static final int TEXT = 0xFF6A3A00; + private static final int WHITE = 0xFFFFFFFF; + private static final String[] LISTS = {"helicopter", "plane", "vehicle", "tank", "item"}; + + private final List>> lists = new ArrayList<>(); + private int list; + private int index; + private int page; + private int pageCount; + private @Nullable Button create; + private @Nullable Button previous; + private @Nullable Button next; + private @Nullable Button previousPage; + private @Nullable Button nextPage; + + public DraftingTableScreen(DraftingTableMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 400; + imageHeight = 240; + inventoryLabelX = 30; + inventoryLabelY = 130; + } + + @Override + protected void init() { + super.init(); + collectRecipes(); + for (int i = 0; i < LISTS.length; i++) { + int which = i; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.list." + LISTS[i]), b -> selectList(which)) + .bounds(leftPos + 20, topPos + 20 + i * 20, 90, 20).build()); + } + create = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.create"), b -> create()) + .bounds(leftPos + 120, topPos + 89, 50, 20).build()); + previous = addRenderableWidget(Button.builder(Component.literal("<<"), b -> step(-1)).bounds(leftPos + 120, topPos + 111, 36, 20).build()); + next = addRenderableWidget(Button.builder(Component.literal(">>"), b -> step(1)).bounds(leftPos + 155, topPos + 111, 35, 20).build()); + previousPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.previous_page"), b -> turnPage(-1)) + .bounds(leftPos + 210, topPos + 210, 60, 20).build()); + nextPage = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.next_page"), b -> turnPage(1)) + .bounds(leftPos + 270, topPos + 210, 60, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.drafting_table.workshop"), b -> openWorkshop()) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.drafting_table.workshop_hint"))) + .bounds(leftPos + 334, topPos + 210, 56, 20).build()); + selectList(list); + } + + /** The workshop is a screen of its own and needs no block, so the table's menu closes first. */ + private void openWorkshop() { + minecraft.player.closeContainer(); + minecraft.setScreen(new WorkshopHomeScreen()); + } + + /** Every MCHeli crafting recipe, sorted into the five lists by what it makes. */ + private void collectRecipes() { + lists.clear(); + for (int i = 0; i < LISTS.length; i++) { + lists.add(new ArrayList<>()); + } + for (RecipeHolder holder : minecraft.level.getRecipeManager().getAllRecipesFor(RecipeType.CRAFTING)) { + ItemStack result = resultOf(holder); + if (holder.id().getNamespace().equals(MCHeli.MOD_ID) && !result.isEmpty() + && BuiltInRegistries.ITEM.getKey(result.getItem()).getNamespace().equals(MCHeli.MOD_ID)) { + lists.get(listOf(result.getItem())).add(holder); + } + } + for (List> entries : lists) { + entries.sort(Comparator.comparing(e -> resultOf(e).getHoverName().getString())); + } + } + + private ItemStack resultOf(RecipeHolder holder) { + return holder.value().getResultItem(minecraft.level.registryAccess()); + } + + private static int listOf(Item item) { + if (item == ModItems.HELICOPTER.get()) { + return 0; + } + if (item == ModItems.PLANE.get()) { + return 1; + } + if (item == ModItems.VEHICLE.get()) { + return 2; + } + if (item == ModItems.TANK.get()) { + return 3; + } + return 4; + } + + private void selectList(int which) { + list = which; + index = 0; + showRecipe(); + } + + private void step(int direction) { + int size = lists.get(list).size(); + if (size > 0) { + index = Math.floorMod(index + direction, size); + showRecipe(); + } + } + + private void turnPage(int direction) { + page = Math.clamp(page + direction, 0, Math.max(pageCount - 1, 0)); + updateButtons(); + } + + private void showRecipe() { + page = 0; + ItemStack result = result(); + pageCount = descriptionPages(result); + updateButtons(); + } + + private @Nullable RecipeHolder current() { + List> entries = lists.get(list); + return entries.isEmpty() ? null : entries.get(Math.min(index, entries.size() - 1)); + } + + private ItemStack result() { + RecipeHolder entry = current(); + return entry != null ? resultOf(entry) : ItemStack.EMPTY; + } + + /** The original's description pages for equipment: {@code drafting_table_desc/_.png}. */ + private int descriptionPages(ItemStack result) { + if (result.isEmpty() || AircraftItem.vehicleOf(result) != null) { + return 0; + } + String name = descriptionName(result); + int pages = 0; + while (pages < 10 && minecraft.getResourceManager().getResource(pageTexture(name, pages)).isPresent()) { + pages++; + } + return pages; + } + + private static String descriptionName(ItemStack result) { + if (result.getItem() == ModItems.THROWABLE.get()) { + String throwable = result.get(ModDataComponents.THROWABLE.get()); + return throwable != null ? throwable : "throwable"; + } + return BuiltInRegistries.ITEM.getKey(result.getItem()).getPath(); + } + + private static ResourceLocation pageTexture(String name, int page) { + return new ResourceLocation(MCHeli.MOD_ID, "textures/drafting_table_desc/" + name + "_" + page + ".png"); + } + + private boolean canCreate() { + RecipeHolder entry = current(); + if (entry == null || menu.getSlot(menu.slots.size() - 1).hasItem() && !minecraft.player.getAbilities().instabuild) { + return false; + } + if (minecraft.player.getAbilities().instabuild) { + return true; + } + StackedContents contents = new StackedContents(); + minecraft.player.getInventory().fillStackedContents(contents); + return contents.canCraft(entry.value(), null); + } + + private void create() { + RecipeHolder entry = current(); + if (entry != null) { + ModNetwork.sendToServer(new DraftingTablePacket(entry.id())); + } + } + + private void updateButtons() { + if (create == null || previous == null || next == null || previousPage == null || nextPage == null) { + return; + } + boolean any = current() != null; + create.active = any && canCreate(); + previous.active = lists.get(list).size() > 1; + next.active = lists.get(list).size() > 1; + previousPage.visible = pageCount > 1; + nextPage.visible = pageCount > 1; + previousPage.active = page > 0; + nextPage.active = page < pageCount - 1; + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + renderTooltip(graphics, mouseX, mouseY); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + graphics.blit(BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, TEXTURE_SIZE, TEXTURE_SIZE); + if (pageCount > 0) { + String name = descriptionName(result()); + graphics.blit(pageTexture(name, page), leftPos + PAGE_X, topPos + PAGE_Y, PAGE_WIDTH / 2, PAGE_HEIGHT / 2, 0.0F, 0.0F, + PAGE_WIDTH, PAGE_HEIGHT, TEXTURE_SIZE, TEXTURE_SIZE); + } + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, TEXT, false); + RecipeHolder entry = current(); + if (entry == null) { + graphics.drawString(font, Component.translatable("gui.mcheli.drafting_table.empty"), 120, 20, TEXT, false); + return; + } + ItemStack result = result(); + graphics.drawString(font, font.plainSubstrByWidth(result.getHoverName().getString(), 85), 120, 20, WHITE, true); + graphics.drawString(font, (index + 1) + " / " + lists.get(list).size(), 120, 134, TEXT, false); + ingredients(graphics, entry.value(), mouseX, mouseY); + graphics.renderItem(result, 178, 52); + if (pageCount == 0) { + details(graphics, result); + } + } + + /** {@code drawItemRecipe}: the 3x3 grid, each slot cycling through what it accepts once a second. */ + private void ingredients(GuiGraphics graphics, CraftingRecipe recipe, int mouseX, int mouseY) { + List slots = recipe.getIngredients(); + int width = recipe instanceof ShapedRecipe shaped ? shaped.getWidth() : 3; + int tick = (int) (minecraft.level.getGameTime() / 20); + for (int i = 0; i < slots.size() && i < 9; i++) { + ItemStack[] options = slots.get(i).getItems(); + if (options.length == 0) { + continue; + } + ItemStack stack = options[tick % options.length]; + int x = GRID_X + (i % width) * 18; + int y = GRID_Y + (i / width) * 18; + graphics.renderItem(stack, x, y); + if (mouseX - leftPos >= x && mouseX - leftPos < x + 16 && mouseY - topPos >= y && mouseY - topPos < y + 16) { + graphics.renderTooltip(font, stack, mouseX - leftPos, mouseY - topPos); + } + } + } + + /** The vehicle's numbers, where the original turned its model. */ + private void details(GuiGraphics graphics, ItemStack result) { + String name = AircraftItem.vehicleOf(result); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + return; + } + List lines = new ArrayList<>(); + lines.add(Component.translatable("gui.mcheli.drafting_table.hp", def.maxHp)); + lines.add(Component.translatable("gui.mcheli.drafting_table.speed", "%.2f".formatted(def.speed))); + lines.add(Component.translatable("gui.mcheli.drafting_table.seats", def.seatCount())); + lines.add(Component.translatable("gui.mcheli.drafting_table.fuel", def.maxFuel)); + lines.add(Component.translatable("gui.mcheli.drafting_table.weapons")); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + String weaponName = weapon != null && weapon.displayName != null ? weapon.displayName : group.weaponType(); + lines.add(Component.literal(" " + weaponName + (group.mounts().size() > 1 ? " x" + group.mounts().size() : ""))); + } + int y = PAGE_Y + 20; + for (Component line : lines) { + if (y > PAGE_Y + 180) { + break; + } + graphics.drawString(font, font.split(line, 170).getFirst(), PAGE_X, y, TEXT, false); + y += 10; + } + } +} From 03131b5f4fe51c0426d728660bee98c8cbf52ed1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:24 +0800 Subject: [PATCH 1205/1461] Add client/screen/TeamManagerScreen.java to the 1.20.6 shared sources --- .../client/screen/TeamManagerScreen.java | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java new file mode 100644 index 00000000..3ec2606f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/TeamManagerScreen.java @@ -0,0 +1,136 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.TeamPacket; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.scores.PlayerTeam; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * The team manager, {@code MCH_GuiScoreboard_Main} and {@code MCH_GuiScoreboard_CreateTeam}, for + * operators setting up a match: switch PvP, shuffle everyone into the teams, make a team, send + * everyone to their spawn point and clear away every vehicle. The teams and their players are + * listed on the right. + */ +public class TeamManagerScreen extends Screen { + private static final int WIDTH = 150; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + + private @Nullable EditBox name; + private int color = TeamManager.COLORS.indexOf(ChatFormatting.RED); + private boolean friendlyFire; + private @Nullable Button colorButton; + private @Nullable Button friendlyFireButton; + private @Nullable Button createButton; + + public TeamManagerScreen() { + super(Component.translatable("gui.mcheli.team.title")); + } + + @Override + protected void init() { + int left = width / 2 - WIDTH - 10; + int y = 40; + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.pvp"), b -> send(TeamPacket.Action.PVP)) + .bounds(left, y, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.shuffle"), b -> send(TeamPacket.Action.SHUFFLE)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.shuffle_hint"))) + .bounds(left, y + 22, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.jump_spawn"), b -> send(TeamPacket.Action.JUMP_TO_SPAWN)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.jump_spawn_hint"))) + .bounds(left, y + 44, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.destroy_all"), b -> send(TeamPacket.Action.DESTROY_ALL)) + .tooltip(Tooltip.create(Component.translatable("gui.mcheli.team.destroy_all_hint"))) + .bounds(left, y + 66, WIDTH, 20).build()); + + int create = y + 104; + name = addRenderableWidget(new EditBox(font, left, create, WIDTH, 20, Component.translatable("gui.mcheli.team.name"))); + name.setMaxLength(16); + name.setHint(Component.translatable("gui.mcheli.team.name")); + name.setResponder(text -> updateButtons()); + colorButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + color = (color + 1) % TeamManager.COLORS.size(); + updateButtons(); + }).bounds(left, create + 22, WIDTH, 20).build()); + friendlyFireButton = addRenderableWidget(Button.builder(Component.empty(), b -> { + friendlyFire = !friendlyFire; + updateButtons(); + }).bounds(left, create + 44, WIDTH, 20).build()); + createButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.team.create"), b -> { + ModNetwork.sendToServer(new TeamPacket(TeamPacket.Action.CREATE_TEAM, name.getValue(), color, friendlyFire)); + name.setValue(""); + }).bounds(left, create + 66, WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 - 50, height - 28, 100, 20).build()); + updateButtons(); + } + + private void send(TeamPacket.Action action) { + ModNetwork.sendToServer(new TeamPacket(action)); + } + + private void updateButtons() { + if (colorButton == null || friendlyFireButton == null || createButton == null || name == null) { + return; + } + ChatFormatting teamColor = TeamManager.COLORS.get(color); + colorButton.setMessage(Component.translatable("gui.mcheli.team.color", + Component.literal(teamColor.getName()).withStyle(teamColor))); + friendlyFireButton.setMessage(Component.translatable(friendlyFire ? "gui.mcheli.team.friendly_fire_on" : "gui.mcheli.team.friendly_fire_off")); + String value = name.getValue().trim(); + createButton.active = !value.isEmpty() && !value.contains(" "); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 15, TEXT); + int left = width / 2 - WIDTH - 10; + graphics.drawString(font, Component.translatable("gui.mcheli.team.new_team"), left, 132, DIM); + teams(graphics, width / 2 + 10, 40); + } + + /** The scoreboard's teams and who is in them, as this client sees the scoreboard. */ + private void teams(GuiGraphics graphics, int x, int y) { + if (minecraft.level == null) { + return; + } + List teams = new ArrayList<>(minecraft.level.getScoreboard().getPlayerTeams()); + if (teams.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.team.none"), x, y, DIM); + return; + } + int bottom = height - 40; + for (PlayerTeam team : teams) { + if (y > bottom) { + break; + } + graphics.drawString(font, team.getFormattedDisplayName(), x, y, TEXT); + y += 11; + for (String player : team.getPlayers()) { + if (y > bottom) { + break; + } + graphics.drawString(font, " " + player, x, y, DIM); + y += 10; + } + y += 4; + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From caae442f0dfa09c5fe70db5c110dd66c4253bc5b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:24 +0800 Subject: [PATCH 1206/1461] Add client/screen/UavStationScreen.java to the 1.20.6 shared sources --- .../client/screen/UavStationScreen.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java new file mode 100644 index 00000000..118d3eb5 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/UavStationScreen.java @@ -0,0 +1,100 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_GuiUavStation}: the slot for the drone, the launch offset + * per axis with its buttons, and Continue to take the last drone flown from here again. + */ +public class UavStationScreen extends AbstractContainerScreen { + private static final ResourceLocation BACKGROUND = new ResourceLocation(MCHeli.MOD_ID, "textures/gui/uav_station.png"); + private static final String[] AXES = {"X", "Y", "Z"}; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF0000; + + private @Nullable Button continueButton; + + public UavStationScreen(UavStationMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = 166; + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + int x = width / 2 - 5; + int y = height / 2 - 76; + for (int axis = 0; axis < 3; axis++) { + for (int step = 0; step < UavStationMenu.STEPS.length; step++) { + int s = UavStationMenu.STEPS[step]; + int id = axis * 4 + step; + addRenderableWidget(Button.builder(Component.literal((s > 0 ? "+" : "") + s), b -> press(id)) + .bounds(x + step * 20, y + axis * 22, 20, 20).build()); + } + } + continueButton = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.uav_station.continue"), b -> press(UavStationMenu.BUTTON_CONTINUE)) + .bounds(x - 77, y + 44, 50, 20).build()); + continueButton.active = menu.canContinue(); + } + + @Override + protected void containerTick() { + super.containerTick(); + if (continueButton != null) { + continueButton.active = menu.canContinue(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + renderTooltip(graphics, mouseX, mouseY); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + graphics.blit(BACKGROUND, leftPos, topPos, 0.0F, 0.0F, imageWidth, imageHeight, 256, 256); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + UavStationEntity station = menu.station(); + ItemStack stack = menu.getSlot(0).getItem(); + AircraftDefinition def = stack.isEmpty() ? null : UavStationEntity.definitionOf(stack); + Component label; + int color = WHITE; + if (!stack.isEmpty() && (def == null || !def.uav)) { + label = Component.translatable("gui.mcheli.uav_station.not_uav"); + color = RED; + } else if (station != null && station.kind() == UavStationEntity.PORTABLE && def != null && !def.smallUav) { + label = Component.translatable("gui.mcheli.uav_station.small_only"); + color = RED; + } else { + label = title; + } + graphics.drawString(font, label, 8, 6, color, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, WHITE, false); + for (int axis = 0; axis < 3; axis++) { + graphics.drawString(font, String.format("%s.%+d", AXES[axis], menu.offset(axis)), 58, 15 + axis * 22, WHITE, false); + } + } +} From 96465c79d470599a0e091d6e2f49c24a255b76de Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:25 +0800 Subject: [PATCH 1207/1461] Add client/screen/VehicleScreen.java to the 1.20.6 shared sources --- .../mcheli/client/screen/VehicleScreen.java | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java new file mode 100644 index 00000000..006e92af --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/screen/VehicleScreen.java @@ -0,0 +1,186 @@ +package com.jnhprojects.mcheli.client.screen; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGui}: fuel and parachute slots on the left, the weapon + * panel on the right with its reload cost and button, and a button into the cargo chest for + * vehicles that have one. The panel is drawn from the chest texture so it looks like vanilla. + */ +public class VehicleScreen extends AbstractContainerScreen { + private static final ResourceLocation CHEST = new ResourceLocation("textures/gui/container/generic_54.png"); + private static final ResourceLocation SLOT = new ResourceLocation("container/slot"); + /** Height of the part above the player inventory. */ + private static final int TOP = 88; + private static final int PANEL_X = 72; + private static final int PANEL_WIDTH = 96; + private static final int LABEL = 0xFF404040; + private static final int WHITE = 0xFFFFFFFF; + private static final int RED = 0xFFFF5555; + private static final int GREEN = 0xFF55FF55; + + private int weapon; + private boolean cargoButton; + private @Nullable Button reload; + private @Nullable Button previous; + private @Nullable Button next; + + public VehicleScreen(VehicleMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = TOP + 96; + inventoryLabelY = imageHeight - 94; + } + + @Override + protected void init() { + super.init(); + previous = addRenderableWidget(Button.builder(Component.literal("<"), b -> step(-1)) + .bounds(leftPos + PANEL_X, topPos + 68, 16, 16).build()); + reload = addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.reload"), b -> press(weapon)) + .bounds(leftPos + PANEL_X + 18, topPos + 68, PANEL_WIDTH - 36, 16).build()); + next = addRenderableWidget(Button.builder(Component.literal(">"), b -> step(1)) + .bounds(leftPos + PANEL_X + PANEL_WIDTH - 16, topPos + 68, 16, 16).build()); + AircraftEntity vehicle = menu.vehicle(); + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + cargoButton = def != null && def.cargoRows() > 0; + if (cargoButton) { + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.vehicle.cargo"), b -> press(VehicleMenu.BUTTON_CARGO)) + .bounds(leftPos + imageWidth - 52, topPos + 3, 46, 13).build()); + } + updateButtons(); + } + + @Override + protected void containerTick() { + super.containerTick(); + updateButtons(); + } + + private int weaponCount() { + AircraftEntity vehicle = menu.vehicle(); + VehicleWeapons weapons = vehicle != null ? vehicle.weapons() : null; + return weapons != null ? Math.min(weapons.sets().size(), VehicleMenu.MAX_WEAPONS) : 0; + } + + private void step(int direction) { + int count = weaponCount(); + if (count > 0) { + weapon = Math.floorMod(weapon + direction, count); + updateButtons(); + } + } + + private void press(int button) { + if (minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, button); + } + } + + private void updateButtons() { + if (reload == null || previous == null || next == null) { + return; + } + int count = weaponCount(); + reload.visible = count > 0; + previous.visible = count > 1; + next.visible = count > 1; + reload.active = menu.canReload(weapon); + AircraftEntity vehicle = menu.vehicle(); + boolean grounded = vehicle != null && vehicle.canSupply(); + reload.setTooltip(grounded ? null : Tooltip.create(Component.translatable("gui.mcheli.vehicle.reload_hint"))); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + renderTooltip(graphics, mouseX, mouseY); + } + + @Override + protected void renderBg(GuiGraphics graphics, float a, int mouseX, int mouseY) { + int x = leftPos; + int y = topPos; + // The chest's title bar, its plain middle stretched down to the inventory, then the inventory. + graphics.blit(CHEST, x, y, 0.0F, 0.0F, imageWidth, 17, 256, 256); + graphics.blit(CHEST, x, y + 17, imageWidth, TOP - 17, 0.0F, 12.0F, imageWidth, 1, 256, 256); + graphics.blit(CHEST, x, y + TOP, 0.0F, 126.0F, imageWidth, 96, 256, 256); + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + graphics.blitSprite(SLOT, x + VehicleMenu.FUEL_X + i * 18 - 1, y + VehicleMenu.FUEL_Y - 1, 18, 18); + } + for (int i = 0; i < menu.parachuteSlots(); i++) { + graphics.blitSprite(SLOT, x + VehicleMenu.PARACHUTE_X + i * 18 - 1, y + VehicleMenu.PARACHUTE_Y - 1, 18, 18); + } + graphics.fill(x + PANEL_X - 2, y + 17, x + PANEL_X + PANEL_WIDTH + 2, y + 66, 0xFF373737); + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + AircraftEntity vehicle = menu.vehicle(); + // Long names only give way to the Cargo button, where there is one. + int titleWidth = cargoButton ? imageWidth - 64 : imageWidth - 16; + graphics.drawString(font, font.plainSubstrByWidth(title.getString(), titleWidth), titleLabelX, titleLabelY, LABEL, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, LABEL, false); + if (vehicle == null) { + return; + } + if (vehicle.maxFuel() > 0) { + // A percentage and a gauge: the exact units would run into the weapon panel. + int percent = Math.round(vehicle.fuelRatio() * 100.0F); + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.fuel", percent), VehicleMenu.FUEL_X, VehicleMenu.FUEL_Y - 11, LABEL, false); + int gaugeY = VehicleMenu.FUEL_Y + 18; + int width = VehicleInventory.FUEL_SLOTS * 18 - 2; + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width, gaugeY + 3, 0xFF373737); + graphics.fill(VehicleMenu.FUEL_X, gaugeY, VehicleMenu.FUEL_X + width * percent / 100, gaugeY + 3, percent <= 10 ? RED : GREEN); + } + if (menu.parachuteSlots() > 0) { + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.parachute"), VehicleMenu.PARACHUTE_X, VehicleMenu.PARACHUTE_Y - 11, LABEL, false); + } + weaponPanel(graphics, vehicle); + } + + /** Name, rounds carried against the maximum, and what one reload costs. */ + private void weaponPanel(GuiGraphics graphics, AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null && weaponCount() > 0 ? weapons.set(Math.min(weapon, weaponCount() - 1)) : null; + if (set == null) { + graphics.drawString(font, Component.translatable("gui.mcheli.vehicle.no_weapons"), PANEL_X, 20, WHITE, false); + return; + } + graphics.drawString(font, font.plainSubstrByWidth(set.displayName(), PANEL_WIDTH), PANEL_X, 20, WHITE, false); + int rounds = menu.rounds(weapon); + int max = set.maxAmmo(); + int color = rounds == 0 && max > 0 ? RED : rounds >= max ? GREEN : WHITE; + graphics.drawString(font, rounds + " / " + max, PANEL_X, 31, color, false); + int itemX = PANEL_X; + for (RoundItem cost : set.definition().roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null) { + continue; + } + graphics.renderItem(new ItemStack(item), itemX, 42); + graphics.drawString(font, String.valueOf(cost.count()), itemX + 17, 47, WHITE, false); + itemX += 32; + if (itemX > PANEL_X + PANEL_WIDTH - 16) { + break; + } + } + } +} From 0ff0a58fc4f7be190555df21bfffb70fe63bbd66 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:25 +0800 Subject: [PATCH 1208/1461] Add client/sound/EngineSound.java to the 1.20.6 shared sources --- .../mcheli/client/sound/EngineSound.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java new file mode 100644 index 00000000..b32040f5 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java @@ -0,0 +1,95 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** + * A looping sound of one vehicle: its engine, or the rotor loop some vehicles have alongside it. + * Ported from {@code MCH_SoundUpdater}. + * + *

Volume and pitch follow the vehicle's throttle every tick, the pitch along the definition's + * curve if it has one and otherwise the way the kind of vehicle always has. For listeners outside + * the vehicle the sound is placed between them and the vehicle, so that the 16 block range of a + * normal sound stretches to the definition's {@code SoundRange}, and the pitch bends slightly as the + * vehicle approaches or leaves, which is what the original did. + */ +final class EngineSound extends RuntimeSoundInstance { + private static final float MAX_DOPPLER = 0.2F; + + private final AircraftEntity vehicle; + private final EngineSoundSource source; + private final boolean rotor; + private float doppler; + private double lastDistance = -1.0; + + EngineSound(AircraftEntity vehicle, EngineSoundSource source, String sound, boolean rotor) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.vehicle = vehicle; + this.source = source; + this.rotor = rotor; + this.looping = true; + this.delay = 0; + update(); + } + + @Override + public void tick() { + if (vehicle.isRemoved() || vehicle.definition() == null) { + stop(); + return; + } + update(); + if (volume <= 0.0F) { + stop(); + } + } + + private void update() { + AircraftDefinition def = vehicle.definition(); + LocalPlayer player = Minecraft.getInstance().player; + if (def == null || player == null) { + volume = 0.0F; + return; + } + volume = (rotor ? def.rotorSoundVolume : def.soundVolume) * source.engineVolume(); + PitchCurve curve = rotor ? def.rotorSoundPitchCurve : def.soundPitchCurve; + float pitch = def.soundPitch * (curve != null ? curve.at(vehicle.visibleThrottle()) : source.enginePitch()); + boolean aboard = player.getRootVehicle() == vehicle.getRootVehicle(); + if (aboard) { + x = vehicle.getX(); + y = vehicle.getY(); + z = vehicle.getZ(); + } else { + double dx = vehicle.getX() - player.getX(); + double dz = vehicle.getZ() - player.getZ(); + double distance = Math.sqrt(dx * dx + dz * dz); + if (lastDistance >= 0.0) { + double closing = lastDistance - distance; + if (Math.abs(closing) > 0.3) { + doppler = Math.clamp(doppler + (float) (closing / 40.0), -MAX_DOPPLER, MAX_DOPPLER); + } + } + doppler *= 0.9F; + pitch += doppler; + lastDistance = distance; + + double scale = def.soundRange / 16.0; + x = player.getX() + dx / scale; + y = player.getY() + (vehicle.getY() - player.getY()) / scale; + z = player.getZ() + dz / scale; + } + this.pitch = Math.max(pitch, 0.0F); + } + + @Override + public boolean canStartSilent() { + return true; + } +} From 05af31cbaefcfc2d668a5df4c255aa122d4e7853 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:26 +0800 Subject: [PATCH 1209/1461] Add client/sound/EngineSoundPlayer.java to the 1.20.6 shared sources --- .../client/sound/EngineSoundPlayer.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java new file mode 100644 index 00000000..62e52284 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java @@ -0,0 +1,75 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.Entity; + +import java.util.HashMap; +import java.util.Map; + +/** + * Starts engine and rotor loops for vehicles near the player and restarts them after they fall + * silent. The sounds themselves track their vehicle until it stops or disappears. + */ +public final class EngineSoundPlayer { + /** Ticks to wait before restarting a loop, as the original did, so a stuttering engine does not spam. */ + private static final int RESTART_DELAY = 20; + + /** By {@link #key}, since a vehicle can have two loops. */ + private static final Map PLAYING = new HashMap<>(); + private static final Map COOLDOWN = new HashMap<>(); + + private EngineSoundPlayer() { + } + + public static void tick() { + Minecraft mc = Minecraft.getInstance(); + ClientLevel level = mc.level; + LocalPlayer player = mc.player; + if (level == null || player == null || mc.isPaused()) { + if (level == null) { + PLAYING.clear(); + COOLDOWN.clear(); + } + return; + } + PLAYING.values().removeIf(EngineSound::isStopped); + COOLDOWN.replaceAll((id, ticks) -> ticks - 1); + COOLDOWN.values().removeIf(ticks -> ticks <= 0); + + for (Entity entity : level.entitiesForRendering()) { + if (!(entity instanceof AircraftEntity vehicle) || !(entity instanceof EngineSoundSource source)) { + continue; + } + AircraftDefinition def = vehicle.definition(); + if (def == null) { + continue; + } + double range = def.soundRange * 1.2; + if (vehicle.distanceToSqr(player) > range * range) { + continue; + } + start(mc, vehicle, source, def.soundOrDefault(), def.soundVolume, false); + start(mc, vehicle, source, def.rotorSound, def.rotorSoundVolume, true); + } + } + + private static void start(Minecraft mc, AircraftEntity vehicle, EngineSoundSource source, String sound, float volume, boolean rotor) { + long key = key(vehicle, rotor); + if (sound.isEmpty() || PLAYING.containsKey(key) || COOLDOWN.containsKey(key) || volume * source.engineVolume() <= 0.01F) { + return; + } + EngineSound loop = new EngineSound(vehicle, source, sound, rotor); + mc.getSoundManager().play(loop); + PLAYING.put(key, loop); + COOLDOWN.put(key, RESTART_DELAY); + } + + private static long key(AircraftEntity vehicle, boolean rotor) { + return (long) vehicle.getId() << 1 | (rotor ? 1L : 0L); + } +} From 05354dfa168a65ddd2a9116d557409a752b4cdc2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:26 +0800 Subject: [PATCH 1210/1461] Add client/sound/OneShotSound.java to the 1.20.6 shared sources --- .../mcheli/client/sound/OneShotSound.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java new file mode 100644 index 00000000..31825a4f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/OneShotSound.java @@ -0,0 +1,22 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** A sound from a content pack played once at a point, such as a gun firing. */ +public final class OneShotSound extends RuntimeSoundInstance { + public OneShotSound(String sound, double x, double y, double z, float volume, float pitch) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.x = x; + this.y = y; + this.z = z; + this.volume = volume; + this.pitch = pitch; + } + + @Override + public void tick() { + // It stays where it was started and ends when the file does. + } +} From b65136212c0d6df0dd13ca7811108d65698e19f0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:27 +0800 Subject: [PATCH 1211/1461] Name the streamed sound by string on 1.20.6 --- .../client/sound/RuntimeSoundInstance.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java new file mode 100644 index 00000000..49c53d61 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import net.minecraft.Util; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.JOrbisAudioStream; +import net.minecraft.client.sounds.LoopingAudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.client.sounds.WeighedSoundEvents; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.util.valueproviders.ConstantFloat; +import org.jspecify.annotations.Nullable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * A sound that plays through the sound manager when a resource pack defines its event, and + * otherwise streams the OGG file a runtime root holds for it ({@link RuntimeAssets#soundFile}). + * Content added while the game runs can bring sounds this way without touching + * {@code sounds.json} or reloading resources. + */ +public abstract class RuntimeSoundInstance extends AbstractTickableSoundInstance { + private @Nullable Path file; + + protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSource random) { + super(event, source, random); + } + + @Override + public @Nullable WeighedSoundEvents resolve(SoundManager manager) { + WeighedSoundEvents events = manager.getSoundEvent(location); + if (events != null) { + sound = events.getSound(random); + return events; + } + file = RuntimeAssets.soundFile(location.getPath()); + if (file == null) { + // What vanilla does for an unknown event: play nothing. + sound = SoundManager.EMPTY_SOUND; + return null; + } + // Streamed, so the file is read as it plays and never cached as a buffer. + sound = new Sound(location.toString(), ConstantFloat.of(1.0F), ConstantFloat.of(1.0F), 1, Sound.Type.FILE, true, false, 16); + return new WeighedSoundEvents(location, null); + } + + /** + * Forge and NeoForge call this through their sound instance extension, which it overrides + * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + */ + public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { + return openStream(buffers, sound.getPath(), looping); + } + + /** Streams the file found on disk, or the resource {@code path} when there is none. */ + public CompletableFuture openStream(SoundBufferLibrary buffers, ResourceLocation path, boolean looping) { + Path source = file; + if (source == null) { + return buffers.getStream(path, looping); + } + return CompletableFuture.supplyAsync(() -> { + try { + InputStream in = new BufferedInputStream(Files.newInputStream(source)); + return looping ? new LoopingAudioStream(JOrbisAudioStream::new, in) : new JOrbisAudioStream(in); + } catch (IOException e) { + throw new CompletionException(e); + } + }, Util.nonCriticalIoPool()); + } +} From 524fc54f2be2d925b90997becfc38627ad605d91 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:27 +0800 Subject: [PATCH 1212/1461] Add client/workshop/WorkshopClient.java to the 1.20.6 shared sources --- .../client/workshop/WorkshopClient.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java new file mode 100644 index 00000000..26401727 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopClient.java @@ -0,0 +1,94 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackSubmitPacket; +import com.jnhprojects.mcheli.network.TestFlightPacket; +import net.minecraft.client.Minecraft; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; + +/** + * What the workshop does beyond its screens: takes the player to a test flight and back, and sends + * a pack to the server. The screen stays alive during a flight, so the undo history and the view are + * as they were when the pilot gets out. + */ +public final class WorkshopClient { + /** If no vehicle has come after this long, the server turned the flight down and said why. */ + private static final int SPAWN_TIMEOUT_TICKS = 100; + /** About 1.2 MB a second, so a download-sized pack does not crowd out the game's own packets. */ + private static final int SUBMIT_CHUNKS_PER_TICK = 2; + + private static @Nullable WorkshopScreen waiting; + private static boolean airborne; + private static int ticks; + private static byte @Nullable [] submitting; + private static int submitted; + + private WorkshopClient() { + } + + static void startTestFlight(WorkshopScreen screen, String vehicle) { + waiting = screen; + airborne = false; + ticks = 0; + Minecraft.getInstance().setScreen(null); + ModNetwork.sendToServer(new TestFlightPacket(vehicle)); + } + + /** Starts sending a pack's zip to the server, a few chunks a tick. */ + static void submit(byte[] zip) { + submitting = zip; + submitted = 0; + } + + /** How far along the zip being sent is, from 0 to 100, or -1 when none is. */ + static int submitProgress() { + byte[] zip = submitting; + return zip != null ? (int) (submitted * 100L / zip.length) : -1; + } + + public static void tick() { + tickSubmit(); + WorkshopScreen screen = waiting; + Minecraft mc = Minecraft.getInstance(); + if (screen == null || mc.player == null) { + return; + } + boolean flying = mc.player.getVehicle() instanceof AircraftEntity; + airborne |= flying; + ticks++; + if (airborne ? !flying : ticks > SPAWN_TIMEOUT_TICKS) { + waiting = null; + // Not over another screen the player has opened in flight, such as the pause menu. + if (mc.screen == null) { + mc.setScreen(screen); + } + } + } + + private static void tickSubmit() { + byte[] zip = submitting; + if (zip == null) { + return; + } + for (int i = 0; i < SUBMIT_CHUNKS_PER_TICK && submitted < zip.length; i++) { + int end = Math.min(submitted + PackSubmitPacket.CHUNK_SIZE, zip.length); + ModNetwork.sendToServer(new PackSubmitPacket(submitted, zip.length, Arrays.copyOfRange(zip, submitted, end))); + submitted = end; + } + if (submitted >= zip.length) { + submitting = null; + } + } + + public static void loggedOut() { + submitting = null; + WorkshopScreen screen = waiting; + waiting = null; + if (screen != null) { + screen.releaseAssets(); + } + } +} From c016dcc54e9a0a8ae9a66da40a3aa06761646665 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:28 +0800 Subject: [PATCH 1213/1461] Add client/workshop/WorkshopHomeScreen.java to the 1.20.6 shared sources --- .../client/workshop/WorkshopHomeScreen.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java new file mode 100644 index 00000000..cc44546a --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopHomeScreen.java @@ -0,0 +1,201 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.workshop.ModelImport; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopKind; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Where the workshop starts: the vehicles in the content packs on the left, ready to open, and on + * the right a form to start a new one from an OBJ file, which can be dropped on the window. + */ +public class WorkshopHomeScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int FORM_WIDTH = 190; + private static final int MAX_LIST_WIDTH = 300; + private static final int LINE_HEIGHT = 10; + + private final List entries; + private int scroll; + private WorkshopKind kind = WorkshopKind.HELICOPTER; + private String vehicleText = ""; + private String packText = ""; + private String modelText = ""; + private @Nullable Component error; + private @Nullable EditBox vehicleBox; + private @Nullable EditBox packBox; + private @Nullable EditBox modelBox; + + public WorkshopHomeScreen() { + super(Component.translatable("gui.mcheli.workshop.title")); + entries = WorkshopProject.list(MCHeli.packs().folder()); + } + + @Override + protected void init() { + listRows(); + form(); + } + + private int listWidth() { + return Math.clamp(width - FORM_WIDTH - 30, 100, MAX_LIST_WIDTH); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + private void listRows() { + int rows = visibleRows(); + for (int i = scroll; i < entries.size() && i < scroll + rows; i++) { + WorkshopProject.Entry entry = entries.get(i); + String suffix = entry.zipped() ? " (zip)" : ""; + addRenderableWidget(Button.builder(Component.literal(entry.pack() + " / " + entry.vehicle() + suffix), b -> open(entry)) + .bounds(10, TOP + (i - scroll) * ROW, listWidth(), 20).build()); + } + } + + private void form() { + int x = width - FORM_WIDTH - 10; + int y = TOP; + vehicleBox = box(x, y + 10, Component.translatable("gui.mcheli.workshop.new.vehicle"), vehicleText, 64); + vehicleBox.setResponder(text -> { + // The pack follows the vehicle's name until the player gives it its own. + if (packBox != null && packBox.getValue().equals(vehicleText)) { + packBox.setValue(text); + } + vehicleText = text; + }); + packBox = box(x, y + 44, Component.translatable("gui.mcheli.workshop.new.pack"), packText, 64); + packBox.setResponder(text -> packText = text); + addRenderableWidget(Button.builder(kindName(), b -> { + kind = WorkshopKind.values()[(kind.ordinal() + 1) % WorkshopKind.values().length]; + b.setMessage(kindName()); + }).bounds(x, y + 68, FORM_WIDTH, 20).build()); + modelBox = box(x, y + 102, Component.translatable("gui.mcheli.workshop.new.model"), modelText, 1024); + modelBox.setResponder(text -> modelText = text); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.new.create"), b -> create()) + .bounds(x, y + 126, FORM_WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.home.packs"), b -> minecraft.setScreen(new WorkshopPacksScreen())) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private EditBox box(int x, int y, Component name, String value, int maxLength) { + EditBox box = new EditBox(font, x, y, FORM_WIDTH, 18, name); + box.setMaxLength(maxLength); + box.setValue(value); + return addRenderableWidget(box); + } + + private Component kindName() { + return Component.translatable("gui.mcheli.workshop.new.kind", Component.translatable("gui.mcheli.workshop.kind." + kind.id())); + } + + private void open(WorkshopProject.Entry entry) { + try { + minecraft.setScreen(new WorkshopScreen(WorkshopProject.open(MCHeli.packs().folder(), entry))); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + private void create() { + String vehicle = vehicleText.trim(); + AircraftDefinition existing = MCHeli.content().any(vehicle); + if (existing != null && existing.pack().equals(ContentSource.ORIGINAL)) { + // The import's models come from a resource pack, which would win over this vehicle's. + error = Component.translatable("gui.mcheli.workshop.new.name_taken", vehicle); + return; + } + try { + Path obj = Path.of(modelText.trim().replace("\"", "")); + String author = minecraft.player != null ? minecraft.player.getGameProfile().getName() : ""; + WorkshopProject project = WorkshopProject.create(MCHeli.packs().folder(), packText.trim(), kind, vehicle, author, + obj, ModelImport.findTexture(obj)); + minecraft.setScreen(new WorkshopScreen(project)); + } catch (InvalidPathException e) { + error = Component.translatable("gui.mcheli.workshop.new.bad_path", modelText); + } catch (WorkshopException e) { + error = Component.literal(e.getMessage()); + } + } + + /** A file dropped on the window fills in the form, its name standing in for the vehicle's. */ + @Override + public void onFilesDrop(List files) { + if (files.isEmpty() || modelBox == null || vehicleBox == null) { + return; + } + Path file = files.getFirst(); + modelBox.setValue(file.toString()); + if (vehicleText.isEmpty()) { + String name = file.getFileName().toString(); + int dot = name.lastIndexOf('.'); + vehicleBox.setValue((dot > 0 ? name.substring(0, dot) : name).toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "-")); + } + error = null; + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int max = Math.max(entries.size() - visibleRows(), 0); + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, max); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 10, TEXT); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.open"), 10, TOP - 12, DIM); + if (entries.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.none"), 10, TOP, DIM); + } + int x = width - FORM_WIDTH - 10; + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.home.new"), x, TOP - 12, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.vehicle"), x, TOP, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.pack"), x, TOP + 34, DIM); + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.new.model"), x, TOP + 92, DIM); + int y = TOP + 150; + for (var line : font.split(Component.translatable("gui.mcheli.workshop.home.drop"), FORM_WIDTH)) { + graphics.drawString(font, line, x, y, DIM); + y += LINE_HEIGHT; + } + if (error != null) { + y += 2; + for (var line : font.split(error, FORM_WIDTH)) { + graphics.drawString(font, line, x, y, ERROR); + y += LINE_HEIGHT; + } + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From 0cf15f2fbf90a51e54b668dcefd75ef480c0674e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:28 +0800 Subject: [PATCH 1214/1461] Add client/workshop/WorkshopPacksScreen.java to the 1.20.6 shared sources --- .../client/workshop/WorkshopPacksScreen.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java new file mode 100644 index 00000000..b8abc314 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopPacksScreen.java @@ -0,0 +1,218 @@ +package com.jnhprojects.mcheli.client.workshop; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.ClientPacks; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import com.jnhprojects.mcheli.workshop.PackCatalog; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import net.minecraft.Util; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * The package browser: the packs in {@code mcheli/packs} and those the server in use sent, who made + * them and what is in them. A zip someone gave the player can be dropped on it to install. + * + *

This only ever reads the packs and adds to the folder. Removing one is left to the file manager + * the Open folder button opens, since a pack can be the only copy of someone's work. + */ +public class WorkshopPacksScreen extends Screen { + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int ERROR = 0xFFFF7070; + private static final int GOOD = 0xFF80E080; + private static final int ROW = 22; + private static final int TOP = 36; + private static final int LIST_WIDTH = 170; + private static final int LINE_HEIGHT = 10; + private static final int VEHICLES_SHOWN = 8; + + /** A pack in the list, and where it came from. */ + private record Row(PackCatalog.Info info, boolean fromServer) { + } + + private final List rows = new ArrayList<>(); + private final List problems = new ArrayList<>(); + private int scroll; + private int selected; + private @Nullable Component message; + private int messageColor = DIM; + + public WorkshopPacksScreen() { + super(Component.translatable("gui.mcheli.workshop.packs.title")); + read(); + } + + private void read() { + rows.clear(); + problems.clear(); + for (ContentPack pack : ClientPacks.INSTANCE.serverPacks()) { + try { + rows.add(new Row(PackCatalog.info(pack), true)); + } catch (IOException e) { + problems.add(new PackProblem(pack.id(), "could not be read: " + e)); + } + } + PackCatalog.Listing own = PackCatalog.read(MCHeli.packs().folder()); + for (PackCatalog.Info info : own.packs()) { + // A pack the player has that the server also uses is listed once, as the server's. + if (rows.stream().noneMatch(row -> row.info().hash().equals(info.hash()))) { + rows.add(new Row(info, false)); + } + } + problems.addAll(own.problems()); + selected = Math.clamp(selected, 0, Math.max(rows.size() - 1, 0)); + } + + private int visibleRows() { + return Math.max((height - TOP - 34) / ROW, 1); + } + + @Override + protected void init() { + scroll = Math.clamp(scroll, 0, Math.max(rows.size() - visibleRows(), 0)); + for (int i = scroll; i < rows.size() && i < scroll + visibleRows(); i++) { + int index = i; + Button row = addRenderableWidget(Button.builder(Component.literal(rows.get(i).info().manifest().name()), b -> { + selected = index; + rebuildWidgets(); + }).bounds(10, TOP + (i - scroll) * ROW, LIST_WIDTH, 20).build()); + row.active = i != selected; + } + addRenderableWidget(Button.builder(Component.translatable("gui.mcheli.workshop.packs.open_folder"), b -> openFolder()) + .bounds(width / 2 - 104, height - 26, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> onClose()) + .bounds(width / 2 + 4, height - 26, 100, 20).build()); + } + + private void openFolder() { + Path folder = MCHeli.packs().folder(); + try { + Files.createDirectories(folder); + Util.getPlatform().openFile(folder.toFile()); + } catch (IOException e) { + say(Component.literal(e.toString()), ERROR); + } + } + + private void say(Component text, int color) { + message = text; + messageColor = color; + } + + @Override + public void onFilesDrop(List files) { + for (Path file : files) { + if (!file.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) { + say(Component.translatable("gui.mcheli.workshop.packs.not_zip", file.getFileName().toString()), ERROR); + continue; + } + try { + PackManifest installed = PackCatalog.install(file, MCHeli.packs().folder()); + say(Component.translatable("gui.mcheli.workshop.packs.installed", installed.name()), GOOD); + read(); + rebuildWidgets(); + } catch (WorkshopException e) { + say(Component.literal(e.getMessage()), ERROR); + } + } + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int next = Math.clamp(scroll - (int) Math.signum(scrollY), 0, Math.max(rows.size() - visibleRows(), 0)); + if (next != scroll) { + scroll = next; + rebuildWidgets(); + } + return true; + } + + @Override + public void onClose() { + minecraft.setScreen(new WorkshopHomeScreen()); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + graphics.drawCenteredString(font, title, width / 2, 10, TEXT); + if (rows.isEmpty()) { + graphics.drawString(font, Component.translatable("gui.mcheli.workshop.packs.none"), 10, TOP, DIM); + } + int x = 10 + LIST_WIDTH + 10; + int w = width - x - 10; + int y = TOP; + if (selected < rows.size()) { + y = details(graphics, rows.get(selected), x, y, w) + LINE_HEIGHT; + } + for (PackProblem problem : problems) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.problem", problem.pack(), problem.message()), x, y, w, ERROR); + } + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.drop"), x, y + 4, w, DIM); + if (message != null) { + wrapped(graphics, message, x, y + 4, w, messageColor); + } + } + + private int details(GuiGraphics graphics, Row row, int x, int y, int w) { + PackCatalog.Info info = row.info(); + PackManifest m = info.manifest(); + y = wrapped(graphics, Component.literal(m.name() + " " + m.version()), x, y, w, TEXT); + if (!m.author().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.author", m.author()), x, y, w, TEXT); + } + if (!m.description().isEmpty()) { + y = wrapped(graphics, Component.literal(m.description()), x, y + 2, w, DIM); + } + y += 4; + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.id", m.id()), x, y, w, DIM); + // A small pack is still something, and 0.0 MB reads as an empty one. + String megabytes = "%.1f".formatted(Math.max(info.size() / 1048576.0, 0.1)); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.size", megabytes, ContentPack.shortHash(info.hash())), x, y, w, DIM); + Component source = row.fromServer() ? Component.translatable("gui.mcheli.workshop.packs.from_server") + : Component.literal(info.source().getFileName().toString() + (info.zipped() ? "" : "/")); + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.source", source), x, y, w, DIM); + y += 4; + if (!info.vehicles().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.vehicles", info.vehicles().size(), shortList(info.vehicles())), x, y, w, TEXT); + } + if (!info.weapons().isEmpty()) { + y = wrapped(graphics, Component.translatable("gui.mcheli.workshop.packs.weapons", info.weapons().size(), shortList(info.weapons())), x, y, w, TEXT); + } + return y; + } + + private static String shortList(List names) { + if (names.size() <= VEHICLES_SHOWN) { + return String.join(", ", names); + } + return String.join(", ", names.subList(0, VEHICLES_SHOWN)) + ", ..."; + } + + private int wrapped(GuiGraphics graphics, Component text, int x, int y, int w, int color) { + for (FormattedCharSequence line : font.split(text, w)) { + graphics.drawString(font, line, x, y, color); + y += LINE_HEIGHT; + } + return y; + } + + @Override + public boolean isPauseScreen() { + return false; + } +} From 46d52722880cb03c14d569ce06d5da52ecd4fb7e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:29 +0800 Subject: [PATCH 1215/1461] Add client/workshop/WorkshopScreen.java to the 1.20.6 shared sources --- .../client/workshop/WorkshopScreen.java | 1671 +++++++++++++++++ 1 file changed, 1671 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java new file mode 100644 index 00000000..2b29f220 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/client/workshop/WorkshopScreen.java @@ -0,0 +1,1671 @@ +package com.jnhprojects.mcheli.client.workshop; + +import net.minecraft.client.Minecraft; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import org.lwjgl.opengl.GL11; +import org.joml.Matrix4f; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.LightTexture; +import com.mojang.math.Axis; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.platform.Lighting; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.render.AircraftRenderState; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.sound.OneShotSound; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PitchCurve; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.workshop.AssetImport; +import com.jnhprojects.mcheli.workshop.EditHistory; +import com.jnhprojects.mcheli.workshop.PartField; +import com.jnhprojects.mcheli.workshop.PartType; +import com.jnhprojects.mcheli.workshop.TuningKey; +import com.jnhprojects.mcheli.workshop.WorkshopCamera; +import com.jnhprojects.mcheli.workshop.WorkshopException; +import com.jnhprojects.mcheli.workshop.WorkshopPart; +import com.jnhprojects.mcheli.workshop.WorkshopProject; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.TreeSet; +import java.util.function.Consumer; + +/** + * The workshop's editor. The vehicle turns in the middle, drawn by the game's own vehicle renderer + * through a GUI entity, so what shows is what will fly. The parts list is on the left, the values + * of what is selected on the right, and gizmos for every part lie over the model. + * + *

Gizmos are plain GUI drawing at the points {@link WorkshopCamera#project} gives. They always + * show, even when the part is inside the hull, which is where most seats and pivots are. + * + *

Left drag on empty space turns the view, right drag moves it, the wheel zooms. Dragging a + * handle moves the part in the plane of the screen and an arrow along its axis, snapping to a + * sixteenth of a block unless Shift is held. + */ +public class WorkshopScreen extends Screen { + private static final int LEFT = 112; + private static final int RIGHT = 144; + private static final int TOP = 26; + private static final int BOTTOM = 26; + private static final int LIST_ROW = 16; + private static final int FIELD_ROW = 20; + private static final int LABEL_ROW = 10; + + private static final int PANEL = 0xFF181818; + private static final int VIEW = 0xFF2A2F36; + private static final int GRID = 0xFF3A414A; + private static final int TEXT = 0xFFFFFFFF; + private static final int DIM = 0xFFA0A0A0; + private static final int WARNING = 0xFFFFC040; + private static final int INVALID = 0xFFFF6060; + private static final int X_COLOR = 0xFFFF5050; + private static final int Y_COLOR = 0xFF60E060; + private static final int Z_COLOR = 0xFF5090FF; + private static final int SELECTED = 0xFFFFFFFF; + private static final int HIT_BOX = 0xFF00E0FF; + private static final int AXIS = 0xFFFF60FF; + + private static final int HANDLE_RADIUS = 3; + private static final double PICK_DISTANCE = 6.0; + private static final double ARROW_LENGTH = 34.0; + private static final double SNAP = 16.0; + private static final int GRID_HALF_SIZE = 16; + private static final int GRID_STEP = 2; + private static final double ZOOM_STEP = 1.15; + + private static final int PREVIEW_SIZE = 64; + private static final int PLOT_HEIGHT = 56; + private static final float PITCH_SNAP = 20.0F; + private static final String DEFAULT_CURVE = "0:0.7, 0.25:0.85, 0.5:1, 0.75:1.15, 1:1.3"; + + private enum Tab { + PARTS, PAINT, SOUND, TUNING, PACK + } + + private enum Drag { + NONE, ORBIT, PAN, PLANE, AXIS_X, AXIS_Y, AXIS_Z, CURVE + } + + private record Label(Component text, int x, int y, int color) { + } + + private record WrappedLine(FormattedCharSequence text, int x, int y) { + } + + private record MergeKey(int line, String field, int component) { + } + + private final WorkshopProject project; + private final WorkshopCamera camera = new WorkshopCamera(); + private final EditHistory> history = new EditHistory<>(); + private final List

Movement is simulated wherever {@link #isControlledByLocalInstance()} is true. With a player + * at the controls that is the pilot's client, exactly like vanilla boats, and the server checks + * the result through the regular vehicle move packet. Without a pilot the server simulates the + * vehicle on its own. Roll, throttle and key state are not part of the vanilla packet, so the + * pilot sends them separately and the server republishes them through synched data. + * + *

Seats are explicit. Vanilla only knows an ordered passenger list, so the seat each passenger + * occupies is kept in a synched seat map of entity ids. Seat zero is the pilot and decides who + * controls the vehicle; crew seats follow, then cargo racks. + */ +public abstract class AircraftEntity extends Entity { + // VehicleEntity's hurt wobble. Vanilla 1.20.6 keeps that class package private where it matters, + // so vehicles extend Entity and carry their own. + private static final EntityDataAccessor DATA_HURT_TIME = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_HURT_DIR = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TYPE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TEXTURE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_HP = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_THROTTLE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_REVERSE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_ROLL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_HOVERING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_GUNNER_MODE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_INPUT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_PARTS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SEATS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_SELECTION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_USE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TV_MISSILE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_FLARE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SINKING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_FUEL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + /** The UAV station a drone is flown from, by entity id, or 0. */ + private static final EntityDataAccessor DATA_STATION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_REPELLING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SEARCH_LIGHT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + + /** Marks a player flying a drone from a station, so a stale hide can be undone at login. */ + public static final String REMOTE_PILOT_TAG = "mcheli_remote_pilot"; + + // Bits of DATA_PARTS, the same assignment the original used for its part status word. + protected static final int PART_CANOPY = 0; + protected static final int PART_NOZZLE = 1; + protected static final int PART_LANDING_GEAR = 2; + protected static final int PART_WING = 3; + protected static final int PART_HATCH = 4; + + private static final int EMPTY = -1; + /** How far above its hit box the original drew a vehicle's model; see {@link #waterDepth}. */ + private static final float LEGACY_MODEL_LIFT = 0.35F; + /** How fast a cooked-off boat goes under, in blocks per tick (about 0.4 blocks a second). */ + private static final double SINK_SPEED = 0.02; + /** The bow-up pitch and the list a sinking boat settles into, in degrees. */ + private static final float SINK_PITCH = 14.0F; + private static final float SINK_LIST = 12.0F; + + protected final ControlInput input = new ControlInput(); + public final AnimationState animation = new AnimationState(); + + /** Throttle in [0, 1] as seen by the simulating side. Other sides read {@link #DATA_THROTTLE}. */ + protected double throttle; + /** Reverse throttle in [0, 0.6] for vehicles with {@code EnableBack}. */ + protected float reverse; + protected float roll; + public float rollO; + protected int modeSwitchCooldown; + private int ticksAlive; + + private @Nullable AircraftDefinition cachedDefinition; + private String cachedName = ""; + private int cachedGeneration; + + protected @Nullable PartState canopy; + protected @Nullable PartState landingGear; + protected @Nullable PartState hatch; + private final List parts = new ArrayList<>(); + + private int[] seatIds = new int[0]; + private int pendingSeat = EMPTY; + private final Map savedSeats = new HashMap<>(); + private final Map lastSeats = new HashMap<>(); + /** The station a drone is flown from, and where its pilot goes back to. Server only. */ + private @Nullable UUID station; + private @Nullable Vec3 stationReturn; + /** Remote pilots leaving this tick, by entity id, with where they come back down. */ + private final Map remoteReturns = new HashMap<>(); + /** Where a workshop test flight started, or {@code null} for an ordinary vehicle. Server only. */ + private @Nullable Vec3 testFlightStart; + private boolean testFlightOver; + /** How far the ropes hang below their hooks, as a negative number; both sides let them out. */ + private float ropeLength; + private int repellingTicks; + private int lastRope; + /** Passengers are leaving one by one under parachutes. Server only. */ + private boolean droppingTroops; + + private @Nullable VehicleWeapons weapons; + private int @Nullable [] savedAmmo; + private @Nullable TvMissileEntity tvMissile; + + // Damage model + /** Damage factor of the extra hit box the last projectile ray struck, {@code lastBBDamageFactor}. */ + private float hitBoxFactor = 1.0F; + private int timeSinceHit; + /** The fuel load from which an aircraft counts as heavy: the H8K flying boat and up. */ + private static final int HEAVY_FUEL = 3000; + /** How long a wreck stays, burning, before it disappears: 30 seconds. */ + private static final int WRECK_TICKS = 600; + /** Ticks a server position update is eased in over. */ + private static final int LERP_STEPS = 3; + /** The wreck fire dies down over the last five seconds. */ + private static final int WRECK_FADE_TICKS = 100; + /** Ticks until a destroyed wreck disappears; above zero means destroyed. Server only. */ + private int despawnCount; + private int lerpSteps; + private double lerpX; + private double lerpY; + private double lerpZ; + private double lerpYRot; + private double lerpXRot; + /** Ticks the client has seen this vehicle as a wreck, for the fire to die down on time. */ + private int wreckTicks; + /** Ticks since this aircraft was shot down, for the crash spin. Counted on both sides. */ + private int spinTicks; + private boolean exploded; + /** Blasts still to come while a wrecked boat cooks off, and ticks until the next one. */ + private int cookOffBlasts; + private int cookOffDelay; + private @Nullable Entity cookOffCause; + private int alertCount; + + /** Fuel burnt but not yet taken from the tank, which only holds whole units. Server only. */ + private double fuelBurnt; + /** Ticks a fuel truck nearby keeps this vehicle from burning fuel. Server only. */ + private int fuelSuppliedCount; + /** One bit per seat whose occupant has night vision on. Server only, not saved. */ + private int nightVisionSeats; + /** Fuel and parachute slots and the cargo chest. Only the server's copy holds anything. */ + private final VehicleInventory inventory = new VehicleInventory(this); + + // Crashes, measured from position so they work whichever side simulates the vehicle. + /** Blocks per tick into terrain, sideways, that count as a crash: about 25 m/s. */ + private static final double TERRAIN_CRASH_SPEED = 1.25; + /** + * How far a hull reaches past its main box, at most, when looking for neighbours: other + * vehicles are found by their main box, and the longest hulls stretch about this far from it. + */ + private static final double HULL_REACH = 24.0; + /** Closing speed between two vehicles that counts as a crash: about 10 m/s. */ + private static final double VEHICLE_CRASH_SPEED = 0.5; + /** Highest point since a ground vehicle left the ground, or NaN while it is on it. */ + private double airborneFromY = Double.NaN; + private @Nullable Vec3 lastTickPos; + private Vec3 lastVelocity = Vec3.ZERO; + private int collisionCooldown; + + protected AircraftEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_HURT_TIME, 0); + builder.define(DATA_HURT_DIR, 1); + builder.define(DATA_TYPE, ""); + builder.define(DATA_TEXTURE, ""); + builder.define(DATA_HP, 0); + builder.define(DATA_THROTTLE, 0.0F); + builder.define(DATA_REVERSE, 0.0F); + builder.define(DATA_ROLL, 0.0F); + builder.define(DATA_HOVERING, false); + builder.define(DATA_GUNNER_MODE, false); + builder.define(DATA_INPUT, (byte) 0); + builder.define(DATA_PARTS, 0); + builder.define(DATA_SEATS, ""); + builder.define(DATA_WEAPON_SELECTION, ""); + builder.define(DATA_WEAPON_USE, 0); + builder.define(DATA_TV_MISSILE, 0); + builder.define(DATA_FLARE, false); + builder.define(DATA_SINKING, false); + builder.define(DATA_FUEL, 0); + builder.define(DATA_STATION, 0); + builder.define(DATA_REPELLING, false); + builder.define(DATA_SEARCH_LIGHT, false); + } + + // Definition + + /** + * Looks the definition up by name. Returns {@code null} for vehicles whose content was removed. + * After a content reload the next call picks up the new definition object. + */ + public @Nullable AircraftDefinition definition() { + String name = typeName(); + int generation = MCHeli.content().generation(); + if (!name.equals(cachedName) || generation != cachedGeneration) { + boolean reloaded = name.equals(cachedName); + cachedName = name; + cachedGeneration = generation; + AircraftDefinition previous = cachedDefinition; + cachedDefinition = name.isEmpty() ? null : lookup(name); + if (cachedDefinition != null && cachedDefinition != previous) { + if (reloaded && weapons != null) { + // The same vehicle with new numbers: keep the rounds it carries. + savedAmmo = weapons.saveAmmo(); + } + onDefinitionChanged(cachedDefinition); + } + } + return cachedDefinition; + } + + /** Finds a definition of this entity's kind. */ + protected abstract @Nullable AircraftDefinition lookup(String name); + + /** Builds the per vehicle state that depends on the definition: seats, parts, animation arrays. */ + protected void onDefinitionChanged(AircraftDefinition def) { + if (seatIds.length != def.seatAndRackCount()) { + int[] resized = new int[def.seatAndRackCount()]; + Arrays.fill(resized, EMPTY); + System.arraycopy(seatIds, 0, resized, 0, Math.min(seatIds.length, resized.length)); + seatIds = resized; + } + parts.clear(); + canopy = def.canopies.isEmpty() ? null : addPart(PART_CANOPY, 3.5F, PartState.Sounds.CANOPY); + landingGear = def.landingGear.isEmpty() ? null : addPart(PART_LANDING_GEAR, 2.5F, PartState.Sounds.LANDING_GEAR); + hatch = def.hatches.isEmpty() ? null : addPart(PART_HATCH, 1.5F, PartState.Sounds.HATCH); + animation.resize(def); + weapons = new VehicleWeapons(this, def); + if (savedAmmo != null) { + weapons.loadAmmo(savedAmmo); + savedAmmo = null; + } + } + + protected PartState addPart(int bit, float speed, PartState.Sounds sounds) { + PartState state = new PartState(this, bit, speed, sounds); + // A vehicle that comes into view with a part already switched should not animate it. + state.force(state.status()); + parts.add(state); + return state; + } + + /** Sets which vehicle this is. Call once, right after construction, before adding to the level. */ + public void setType(AircraftDefinition definition) { + entityData.set(DATA_TYPE, definition.name()); + entityData.set(DATA_TEXTURE, definition.textures().getFirst()); + entityData.set(DATA_HP, definition.maxHp); + entityData.set(DATA_FUEL, definition.maxFuel); + refreshDimensions(); + definition(); + } + + public String typeName() { + return entityData.get(DATA_TYPE); + } + + public String texture() { + String texture = entityData.get(DATA_TEXTURE); + return texture.isEmpty() ? typeName() : texture; + } + + public int hp() { + return entityData.get(DATA_HP); + } + + /** + * {@code repair}: adds hit points up to the maximum. A wreck stays a wreck. Server only. + * + * @return whether anything was repaired + */ + public boolean repair(int amount) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || hp() >= def.maxHp) { + return false; + } + entityData.set(DATA_HP, Math.min(hp() + amount, def.maxHp)); + return true; + } + + /** + * {@code switchNextTextureName}: the next paint scheme the definition lists. Server only. + * + * @return false when the vehicle has only one + */ + public boolean nextTexture() { + AircraftDefinition def = definition(); + if (def == null || def.textures().size() < 2) { + return false; + } + List textures = def.textures(); + int next = (textures.indexOf(texture()) + 1) % textures.size(); + entityData.set(DATA_TEXTURE, textures.get(next)); + return true; + } + + // Fuel + + public int fuel() { + return entityData.get(DATA_FUEL); + } + + public int maxFuel() { + AircraftDefinition def = definition(); + return def != null ? def.maxFuel : 0; + } + + /** {@code getFuelP}: how full the tank is, 0 to 1. Vehicles without a tank read as full. */ + public float fuelRatio() { + int max = maxFuel(); + return max <= 0 ? 1.0F : (float) fuel() / max; + } + + /** {@code canUseFuel}: a vehicle without a tank never runs dry. */ + public boolean hasFuel() { + return maxFuel() <= 0 || fuel() > 1; + } + + /** Sets the tank outright; a drone leaves its station with 5%. Server only. */ + public void setFuelLevel(int fuel) { + entityData.set(DATA_FUEL, Math.clamp(fuel, 0, Math.max(maxFuel(), 0))); + } + + /** + * Pours up to {@code amount} into the tank. Server only. + * + * @return how much went in + */ + public int refuel(int amount) { + int added = Math.clamp(maxFuel() - fuel(), 0, Math.max(amount, 0)); + if (added > 0) { + entityData.set(DATA_FUEL, fuel() + added); + } + return added; + } + + // Night vision + + /** A crew member switched night vision on or off. Server only. */ + public void setNightVision(Player player, boolean on) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (seat < 0 || seat >= 31) { + return; + } + if (on && def != null && def.nightVision && !isDestroyed()) { + nightVisionSeats |= 1 << seat; + } else if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(player); + } + } + + /** Keeps night vision on for everyone in the crew who switched it on, see {@link NightVision}. */ + private void tickNightVision() { + if (nightVisionSeats == 0) { + return; + } + if (isDestroyed()) { + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null) { + NightVision.end(crew); + } + } + nightVisionSeats = 0; + return; + } + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (hasNightVision(seat) && crew != null) { + NightVision.topUp(crew); + } + } + } + + /** Only the first 31 seats can switch it on; racks and the like come after them anyway. */ + private boolean hasNightVision(int seat) { + return seat >= 0 && seat < 31 && (nightVisionSeats & 1 << seat) != 0; + } + + /** + * {@code updateFuel}: once a second the engine burns its {@code FuelConsumption}, scaled by the + * throttle and topping out at 70% throttle. Nothing burns next to a fuel truck or with the + * server's infinite fuel on. + */ + private void burnFuel(AircraftDefinition def) { + if (def.maxFuel <= 0 || isDestroyed()) { + return; + } + if (fuelSuppliedCount > 0) { + fuelSuppliedCount--; + return; + } + if (ticksAlive % 20 != 0 || fuel() <= 1 || throttle <= 0.0 || ServerConfig.infiniteFuel()) { + return; + } + fuelBurnt += Math.min(throttle * 1.4, 1.0) * def.fuelConsumption; + if (fuelBurnt > 1.0) { + int whole = (int) fuelBurnt; + fuelBurnt -= whole; + entityData.set(DATA_FUEL, Math.max(fuel() - whole, 0)); + } + } + + /** + * {@code supplyFuel}: a fuel truck ({@code FuelSupplyRange}) tops up every vehicle within range, + * 30 units every half second. While it is on the ground it only reaches vehicles that are on + * the ground too. + */ + private void supplyFuel(AircraftDefinition def) { + float range = def.fuelSupplyRange; + if (range <= 0.0F || ticksAlive % 10 != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + if (other == this) { + continue; + } + if (!onGround() || other.onGround()) { + other.refuel(30); + } + other.fuelSuppliedCount = 40; + } + } + + // Inventory and supply + + public VehicleInventory inventory() { + return inventory; + } + + /** + * {@code canSupply}: fuel cans and resupply only work while the vehicle stands on something, + * within three blocks; a vehicle that cannot float must not be in the water either. + */ + public boolean canSupply() { + AircraftDefinition def = definition(); + return def != null && hasBlockBelow(1, 3, false) && (def.floats || !isInWater()); + } + + /** {@code openGui}: shows a crew member the vehicle screen. Server only. */ + public void openScreen(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || isDestroyed() || player.getVehicle() != this) { + return; + } + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new VehicleMenu(id, playerInventory, this), displayName(def))); + } + + /** + * {@code openInventory}: shows the cargo chest of a vehicle with an {@code InventorySize}. + * Server only. + * + * @return false when the vehicle has none + */ + public boolean openCargo(ServerPlayer player) { + AircraftDefinition def = definition(); + if (def == null || def.cargoRows() <= 0 || isDestroyed() || !inventory.stillValid(player)) { + return false; + } + int rows = Math.min(def.cargoRows(), 6); + MenuType type = switch (rows) { + case 1 -> MenuType.GENERIC_9x1; + case 2 -> MenuType.GENERIC_9x2; + case 3 -> MenuType.GENERIC_9x3; + case 4 -> MenuType.GENERIC_9x4; + case 5 -> MenuType.GENERIC_9x5; + default -> MenuType.GENERIC_9x6; + }; + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> new ChestMenu(type, id, playerInventory, inventory.cargo(), rows), + displayName(def))); + return true; + } + + /** The vehicle's name as its item shows it. */ + private static Component displayName(AircraftDefinition def) { + return Component.translatableWithFallback("item.mcheli." + def.name(), def.displayName); + } + + /** + * {@code supplyAmmoToOtherAircraft}: an ammunition truck ({@code AmmoSupplyRange}) tops up the + * weapons of every vehicle within range that stands on something, a tenth of each weapon's + * maximum every two seconds. + */ + private void supplyAmmo(AircraftDefinition def) { + float range = def.ammoSupplyRange; + if (range <= 0.0F || ticksAlive % SupplyRules.AMMO_TRUCK_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + VehicleWeapons theirs = other.weapons(); + if (other == this || theirs == null || other.isDestroyed() || !other.canSupply()) { + continue; + } + for (WeaponSet set : theirs.sets()) { + int rounds = SupplyRules.roundsFromTruck(set.ammo() + set.restAmmo(), set.maxAmmo()); + if (rounds > 0) { + set.setRestAmmo(set.restAmmo() + rounds); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + } + } + } + } + + /** {@code repairOtherAircraft}: a repair vehicle ({@code RepairOtherVehicles}) mends those in range. */ + private void repairOthers(AircraftDefinition def) { + float range = def.repairOtherVehiclesRange; + if (range <= 0.0F || ticksAlive % SupplyRules.REPAIR_INTERVAL != 0 || isDestroyed()) { + return; + } + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(range))) { + AircraftDefinition theirs = other.definition(); + if (other != this && theirs != null) { + other.repair(SupplyRules.repair(other.hp(), theirs.maxHp, def.repairOtherVehiclesValue)); + } + } + } + + /** Whether the flight model holds a hover: in hover mode, or while the pilot mans the guns. */ + public boolean isHovering() { + return isHoveringMode() || isGunnerMode(); + } + + /** The hover mode the helicopter pilot switches with jump. */ + public boolean isHoveringMode() { + return entityData.get(DATA_HOVERING); + } + + protected void setHovering(boolean hovering) { + entityData.set(DATA_HOVERING, hovering); + } + + // Pilot gunner mode + + /** + * Pilot gunner mode ({@code EnableGunnerMode}): the pilot leaves the controls to man the + * weapons from the camera position. Helicopters hold a hover; planes level off and circle. + */ + public boolean isGunnerMode() { + return entityData.get(DATA_GUNNER_MODE); + } + + /** {@code canSwitchGunnerMode}: the kinds add their own attitude and speed limits. */ + public boolean canSwitchGunnerMode() { + AircraftDefinition def = definition(); + if (def == null || !def.gunnerMode || !isCanopyClosed() || isDestroyed() || isHoveringMode()) { + return false; + } + // Without ConcurrentGunnerMode a player in the gunner seat has the guns already. + return def.concurrentGunnerMode || !(occupant(1) instanceof Player); + } + + /** Server side. Leaving gunner mode is always allowed; entering it only when {@link #canSwitchGunnerMode}. */ + public void setGunnerMode(boolean on) { + if (!on || canSwitchGunnerMode()) { + entityData.set(DATA_GUNNER_MODE, on); + } + } + + /** Whether {@code entity} is the pilot and currently in gunner mode. */ + public boolean isGunnerMode(Entity entity) { + return isGunnerMode() && seatOf(entity) == 0; + } + + /** Drops gunner mode once its conditions no longer hold. Server side, every tick. */ + private void checkGunnerMode(AircraftDefinition def) { + if (isGunnerMode() && (occupant(0) == null || isDestroyed() + || (!def.concurrentGunnerMode && occupant(1) instanceof Player))) { + entityData.set(DATA_GUNNER_MODE, false); + } + } + + // Throttle, roll and input + + /** Throttle for animation and sound, valid on every side. */ + public float visibleThrottle() { + return isSimulatingHere() ? (float) throttle : entityData.get(DATA_THROTTLE); + } + + public float visibleReverse() { + return isSimulatingHere() ? reverse : entityData.get(DATA_REVERSE); + } + + public float roll() { + return isSimulatingHere() ? roll : entityData.get(DATA_ROLL); + } + + public float roll(float partialTicks) { + return Mth.rotLerp(partialTicks, rollO, roll()); + } + + public void setRoll(float roll) { + this.roll = roll; + } + + protected boolean isSimulatingHere() { + return isControlledByLocalInstance() && !isPassenger(); + } + + public ControlInput input() { + return input; + } + + /** + * Applies the pilot's view change for one frame. Only called on the pilot's client, and only + * for kinds that steer with the mouse. + * + * @param dYaw change of the player's yaw since the last frame, in degrees + * @param dPitch change of the player's pitch since the last frame, in degrees + */ + public void applyPilotLook(float dYaw, float dPitch) { + } + + /** Whether the pilot steers with the mouse, as in helicopters and planes. */ + public boolean steersWithMouse() { + return false; + } + + /** Server side entry point for the pilot's control packet. */ + public void acceptPilotState(float throttle, float reverse, float roll, boolean hovering, byte keys) { + this.throttle = Mth.clamp(throttle, 0.0F, 1.0F); + this.reverse = Mth.clamp(reverse, 0.0F, 1.0F); + this.roll = AircraftRotation.wrap(roll); + input.read(keys); + entityData.set(DATA_THROTTLE, (float) this.throttle); + entityData.set(DATA_REVERSE, this.reverse); + entityData.set(DATA_ROLL, this.roll); + entityData.set(DATA_HOVERING, hovering); + entityData.set(DATA_INPUT, keys); + } + + public float reverse() { + return reverse; + } + + // Tick + + /** + * Makes this a workshop test flight: it takes no damage, is not saved with the world, and goes + * away once its pilot gets out, putting them back where they started. + */ + public void startTestFlight(Vec3 start) { + testFlightStart = start; + } + + @Override + public boolean shouldBeSaved() { + return testFlightStart == null && super.shouldBeSaved(); + } + + @Override + public void tick() { + if (testFlightOver) { + discard(); + return; + } + rollO = roll(); + if (getHurtTime() > 0) { + setHurtTime(getHurtTime() - 1); + } + super.tick(); + if (level().isClientSide()) { + tickLerp(); + } + ticksAlive++; + lastSeats.clear(); + remoteReturns.clear(); + if (modeSwitchCooldown > 0) { + modeSwitchCooldown--; + } + + AircraftDefinition def = definition(); + if (def == null) { + // Content for this vehicle no longer exists. Keep the entity inert rather than crash. + setDeltaMovement(Vec3.ZERO); + return; + } + if (timeSinceHit > 0) { + timeSinceHit--; + } + if (alertCount > 0) { + alertCount--; + } + boolean groundedBefore = onGround(); + double fallBefore = getDeltaMovement().y; + tickDestroyed(); + + if (!level().isClientSide()) { + supplyFuel(def); + supplyAmmo(def); + repairOthers(def); + burnFuel(def); + if (def.maxFuel > 0 && fuel() < def.maxFuel && !isDestroyed()) { + inventory.pourFuelCans(ticksAlive); + } + tickNightVision(); + tickStation(); + } + tickRopes(def); + + if (isSimulatingHere()) { + if (!(getControllingPassenger() instanceof Player) || isDestroyed()) { + input.clear(); + } + if (isRepelling()) { + // {@code MCH_AircraftClientTickHandler}: the pilot holds a hover while the ropes are out. + input.throttleUp = false; + input.throttleDown = false; + input.left = false; + input.right = false; + } + if (!hasFuel()) { + // An empty tank: the throttle runs down by itself, as with the pilot's hand off it. + input.throttleUp = false; + } + if (entityData.get(DATA_SINKING)) { + sink(); + } else { + simulate(); + } + float[] kick = weapons != null ? weapons.recoilKick() : null; + if (kick != null) { + setXRot(getXRot() + kick[0]); + roll += kick[1]; + } + if (!level().isClientSide()) { + entityData.set(DATA_THROTTLE, (float) throttle); + entityData.set(DATA_REVERSE, reverse); + entityData.set(DATA_ROLL, roll); + entityData.set(DATA_INPUT, input.write()); + } + } else { + if (!isControlledByLocalInstance()) { + input.read(entityData.get(DATA_INPUT)); + } + setDeltaMovement(Vec3.ZERO); + } + for (PartState part : parts) { + part.tick(); + } + if (!level().isClientSide()) { + updatePartsServer(def); + checkGunnerMode(def); + } + animation.tick(this, def); + tickAnimation(); + if (weapons != null) { + weapons.tick(); + if (!level().isClientSide()) { + entityData.set(DATA_FLARE, weapons.flares.isActive()); + warnIncomingMissiles(); + } + } + if (level().isClientSide()) { + damageSmoke(def); + VehicleParticles.tick(this, def); + } else { + tickImpacts(def, groundedBefore, fallBefore); + } + } + + /** + * Server side crashes. A wreck explodes when it lands; an intact vehicle takes damage when it + * meets the ground steeply tilted ({@code onUpdate_CollisionGroundDamage}), sits under water + * without floats, flies into terrain, or rams another vehicle. + */ + private void tickImpacts(AircraftDefinition def, boolean groundedBefore, double fallBefore) { + Vec3 velocity = lastTickPos != null ? position().subtract(lastTickPos) : Vec3.ZERO; + Vec3 previous = lastVelocity; + lastTickPos = position(); + lastVelocity = velocity; + if (collisionCooldown > 0) { + collisionCooldown--; + } + double speed = Math.max(velocity.length(), previous.length()); + + if (isDestroyed()) { + boolean falling = fallBefore < -0.2 || previous.y < -0.2; + boolean landed = onGround() && !groundedBefore; + boolean smashed = horizontalCollision && previous.horizontalDistance() > 0.5; + if (!exploded && (landed && falling || smashed)) { + crashExplosion(); + } + return; + } + + if (isNearGround(3)) { + float roll = Math.abs(Mth.wrapDegrees(roll())); + float pitch = Math.abs(Mth.wrapDegrees(getXRot())); + if (roll > crashTilt() || pitch > crashTilt()) { + float damage = roll + pitch; + damage *= damage < 90.0F ? 0.4F * (float) velocity.length() : 0.4F; + if (damage > 1.0F && random.nextInt(4) == 0) { + hurt(damageSources().inWall(), damage); + } + } + } + if (ticksAlive % 30 == 0 && !def.floats && level().getFluidState( + BlockPos.containing(getX() + 0.5, getY() + 1.5 + def.submergedDamageHeight, getZ() + 0.5)).is(FluidTags.WATER)) { + hurt(damageSources().inWall(), Math.max(def.maxHp / 10, 1)); + } + + if (takesGroundImpacts()) { + groundImpacts(def, previous); + } + + // Not in the original: flying into terrain or another vehicle at speed is a crash. + if (horizontalCollision && previous.horizontalDistance() > TERRAIN_CRASH_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurt(damageSources().flyIntoWall(), crashDamage(previous.horizontalDistance())); + } + if (speed > VEHICLE_CRASH_SPEED && collisionCooldown == 0) { + // A blocked move can leave the hulls up to a tick of travel apart; when the vehicle + // stopped short this tick, look that far for what stopped it. + boolean stopped = velocity.length() < previous.length() * 0.5; + double reach = stopped ? 0.5 + previous.length() : 0.5; + for (AircraftEntity other : solidNeighbours(hullBounds().inflate(reach))) { + if (!hullsTouch(other, reach)) { + continue; + } + double closing = velocity.subtract(other.lastVelocity).length(); + closing = Math.max(closing, previous.subtract(other.lastVelocity).length()); + if (closing > VEHICLE_CRASH_SPEED && other.collisionCooldown == 0) { + collisionCooldown = 20; + other.collisionCooldown = 20; + float damage = crashDamage(closing); + hurt(damageSources().flyIntoWall(), damage); + other.hurt(damageSources().flyIntoWall(), damage); + break; + } + } + } + } + + /** {@code getGiveDamageRot}: how far a vehicle may lean near the ground before it scrapes. */ + protected float crashTilt() { + return 40.0F; + } + + /** Crash damage grows with the square of the impact speed, like the energy involved. */ + private static float crashDamage(double speed) { + return (float) (speed * speed * 60.0); + } + + /** Whether hitting walls and falling hurt this vehicle. Tanks and cars, which live on the ground. */ + protected boolean takesGroundImpacts() { + return false; + } + + /** + * A ground vehicle that hits a wall or lands after a long drop takes a share of its hit points + * by the speed or the height, see {@link GroundImpact}. The same height costs a car and a tank + * the same share before armour. + */ + private void groundImpacts(AircraftDefinition def, Vec3 previous) { + if (horizontalCollision && previous.horizontalDistance() > GroundImpact.WALL_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurt(damageSources().flyIntoWall(), GroundImpact.wall(previous.horizontalDistance()) * def.maxHp); + } + if (isInWater() || isPassenger()) { + airborneFromY = Double.NaN; + } else if (!onGround()) { + airborneFromY = Double.isNaN(airborneFromY) ? getY() : Math.max(airborneFromY, getY()); + } else if (!Double.isNaN(airborneFromY)) { + double drop = airborneFromY - getY(); + airborneFromY = Double.NaN; + if (drop > GroundImpact.SAFE_DROP) { + hurt(damageSources().fall(), GroundImpact.drop(drop) * def.maxHp); + } + } + } + + /** {@code MCH_MissileDetector}: the crew hears a warning while a missile homes on them. */ + private void warnIncomingMissiles() { + if (alertCount > 0 || isDestroyed() || weapons == null || !weapons.flares.isFitted() || ticksAlive % 5 != 0) { + return; + } + boolean tracked = !level().getEntitiesOfClass(ProjectileEntity.class, getBoundingBox().inflate(60.0), + p -> p.target() != null && (p.target() == this || hasPassenger(p.target()))).isEmpty(); + if (tracked) { + alertCount = 20; + ModSounds.playAt(this, "alert", 1.0F, 1.0F); + } + } + + /** One tick of control and physics on the simulating side. */ + protected abstract void simulate(); + + /** Visual state that every side updates, for example rotor angles. */ + protected void tickAnimation() { + } + + /** Ticks since this entity instance was created, used to delay controls right after spawning. */ + protected int ticksAlive() { + return ticksAlive; + } + + // Moving parts + + int partBits() { + return entityData.get(DATA_PARTS); + } + + void setPartBit(int bit, boolean on) { + int bits = entityData.get(DATA_PARTS); + entityData.set(DATA_PARTS, on ? bits | (1 << bit) : bits & ~(1 << bit)); + } + + public @Nullable PartState canopy() { + return canopy; + } + + public @Nullable PartState landingGear() { + return landingGear; + } + + public @Nullable PartState hatch() { + return hatch; + } + + public boolean isCanopyClosed() { + return canopy == null || canopy.isOff(); + } + + protected void openCanopy() { + if (canopy != null && modeSwitchCooldown <= 0) { + canopy.set(true, true); + modeSwitchCooldown = 20; + } + } + + protected void closeCanopy() { + if (canopy != null && modeSwitchCooldown <= 0 && canopy.status()) { + canopy.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean isLandingGearUp() { + return landingGear != null && landingGear.status(); + } + + /** Gear can be raised once the vehicle is at least ten blocks above anything solid. */ + public boolean canRaiseLandingGear() { + return landingGear != null && landingGear.angle() < 1.0F && !landingGear.status() && !hasBlockBelow(3, 10, true); + } + + public boolean canLowerLandingGear() { + return landingGear != null && landingGear.angle() >= 89.0F && landingGear.status(); + } + + public void toggleLandingGear() { + if (landingGear == null || modeSwitchCooldown > 0) { + return; + } + if (canRaiseLandingGear()) { + landingGear.set(true, true); + modeSwitchCooldown = 20; + } else if (canLowerLandingGear()) { + landingGear.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean canFoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOff(); + } + + public boolean canUnfoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOn(); + } + + /** The fold key: hatches by default, planes fold or sweep their wings instead. */ + public void toggleFold() { + if (canFoldHatch()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldHatch()) { + hatch.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Throttle below which the landing gear comes down by itself near the ground. */ + protected float autoLowerGearThrottle() { + return 0.8F; + } + + /** {@code updateParts}: automatic landing gear, plus whatever a kind adds. Server only. */ + protected void updatePartsServer(AircraftDefinition def) { + if (landingGear == null) { + return; + } + if (!landingGear.status() && landingGear.factor() <= 0.1F) { + // Amphibians retract the gear when they settle on water. + boolean onWater = isInWater() || isWaterBelow(20); + if (def.floats && onWater && (throttle <= 0.8 || onGround() || hasBlockBelow(3, 20, true))) { + landingGear.set(true, true); + } + } else if (landingGear.status() && landingGear.factor() >= 0.9F) { + boolean ground = hasBlockBelow(3, 10, true); + if (throttle < autoLowerGearThrottle() && ground) { + if (!def.floats || !isWaterBelow(150)) { + landingGear.set(false, true); + } + } else if (vtolMode() == 2 && ground) { + landingGear.set(false, true); + } + } + } + + /** 0 off, 1 tilting, 2 fully in VTOL mode. Only planes have one. */ + public int vtolMode() { + return 0; + } + + // Seats + + /** Seat (or rack) index of {@code entity}, or -1. */ + public int seatOf(Entity entity) { + int id = entity.getId(); + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == id) { + return i; + } + } + return EMPTY; + } + + public @Nullable Entity occupant(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] == EMPTY) { + return null; + } + int id = seatIds[seat]; + for (Entity passenger : getPassengers()) { + if (passenger.getId() == id) { + return passenger; + } + } + return null; + } + + public @Nullable SeatDefinition seat(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && seat >= 0 && seat < def.seatCount() ? def.seats.get(seat) : null; + } + + private boolean isSeatFree(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] != EMPTY) { + return false; + } + AircraftDefinition def = definition(); + if (def != null) { + for (int[] group : def.exclusiveSeats) { + if (contains(group, seat)) { + for (int other : group) { + if (other != seat && other >= 0 && other < seatIds.length && seatIds[other] != EMPTY) { + return false; + } + } + } + } + } + return true; + } + + private static boolean contains(int[] values, int value) { + for (int v : values) { + if (v == value) { + return true; + } + } + return false; + } + + /** First crew seat a player boarding by hand would take. Drones keep their pilot seat for the remote. */ + private int firstFreeCrewSeat() { + AircraftDefinition def = definition(); + if (def == null) { + return EMPTY; + } + for (int i = def.uav ? 1 : 0; i < def.seatCount(); i++) { + if (isSeatFree(i)) { + return i; + } + } + return EMPTY; + } + + private void assignSeat(Entity entity, int seat) { + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == entity.getId()) { + seatIds[i] = EMPTY; + if (weapons != null) { + weapons.onLeft(i); + } + } + } + if (seat >= 0 && seat < seatIds.length) { + seatIds[seat] = entity.getId(); + } + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onSeated(entity, seat); + weapons.resendAmmo(); + } + } + + private String encodeSeats() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < seatIds.length; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(seatIds[i]); + } + return sb.toString(); + } + + private void decodeSeats(String encoded) { + if (encoded.isEmpty()) { + Arrays.fill(seatIds, EMPTY); + return; + } + String[] parts = encoded.split(","); + if (parts.length != seatIds.length) { + seatIds = new int[parts.length]; + } + for (int i = 0; i < parts.length; i++) { + try { + seatIds[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + seatIds[i] = EMPTY; + } + } + } + + /** Moves {@code entity} to the next free crew seat, wrapping around. Server only. */ + public void switchSeat(Entity entity) { + AircraftDefinition def = definition(); + int current = seatOf(entity); + if (def == null || current < 0 || current >= def.seatCount() || current == 0 && station != null) { + // A remote pilot stays at the controls; they are not really aboard. + return; + } + int count = def.seatCount(); + seatIds[current] = EMPTY; + for (int step = 1; step < count; step++) { + int seat = (current + step) % count; + if (def.uav && seat == 0) { + continue; + } + if (isSeatFree(seat)) { + assignSeat(entity, seat); + if (seat == 0) { + closeCanopy(); + } + return; + } + } + assignSeat(entity, current); + } + + // UAV stations + + /** Whether a player at a station could take this drone's controls now. */ + public boolean canTakeRemotePilot() { + AircraftDefinition def = definition(); + return def != null && def.uav && !def.targetDrone && !isDestroyed() && isSeatFree(0); + } + + /** + * Seats {@code player}, who sits at {@code from}, at this drone's controls. The pilot is hidden + * and takes no harm while flying, since in the original they never left the station. Server only. + */ + public boolean takeRemotePilot(ServerPlayer player, UavStationEntity from) { + if (!canTakeRemotePilot()) { + return false; + } + pendingSeat = 0; + boolean seated = player.startRiding(this, true); + pendingSeat = EMPTY; + if (!seated) { + return false; + } + station = from.getUUID(); + stationReturn = from.returnPosition(); + entityData.set(DATA_STATION, from.getId()); + player.addTag(REMOTE_PILOT_TAG); + player.setInvisible(true); + player.setInvulnerable(true); + return true; + } + + /** Sends the remote pilot back to the station, if one is flying. Server only. */ + public void releaseRemotePilot() { + Entity pilot = occupant(0); + if (station != null && pilot != null) { + pilot.stopRiding(); + } + } + + /** The station this drone is flown from, as the client sees it. */ + public @Nullable UavStationEntity stationEntity() { + return level().getEntity(entityData.get(DATA_STATION)) instanceof UavStationEntity s ? s : null; + } + + /** Flat distance to the station this drone is flown from, or -1 without one. */ + public double stationDistance() { + UavStationEntity from = stationEntity(); + if (from == null) { + return -1.0; + } + double dx = getX() - from.getX(); + double dz = getZ() - from.getZ(); + return Math.sqrt(dx * dx + dz * dz); + } + + /** Whether {@code entity} flies this drone from a station rather than sitting in it. */ + public boolean isRemotePilot(Entity entity) { + return (station != null || entityData.get(DATA_STATION) != 0) && occupant(0) == entity; + } + + private void endRemoteControl(Entity pilot) { + UavStationEntity from = level() instanceof ServerLevel server && station != null + && server.getEntity(station) instanceof UavStationEntity s ? s : null; + Vec3 home = from != null ? from.returnPosition() : stationReturn; + if (home != null) { + remoteReturns.put(pilot.getId(), home); + } + clearRemotePilot(pilot); + if (from != null) { + from.setLinked(false); + } + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + } + + /** Undoes what {@link #takeRemotePilot} did to the player. */ + public static void clearRemotePilot(Entity pilot) { + if (pilot.removeTag(REMOTE_PILOT_TAG)) { + pilot.setInvisible(false); + pilot.setInvulnerable(false); + } + } + + /** + * {@code updateUAV}: the link breaks when the station is gone, and flying out of its range + * loses the drone, as in the original. + */ + private void tickStation() { + if (station == null || !(level() instanceof ServerLevel server)) { + return; + } + Entity pilot = occupant(0); + if (pilot == null) { + station = null; + stationReturn = null; + entityData.set(DATA_STATION, 0); + return; + } + UavStationEntity from = server.getEntity(station) instanceof UavStationEntity s && s.isAlive() ? s : null; + if (from == null) { + pilot.stopRiding(); + return; + } + entityData.set(DATA_STATION, from.getId()); + if (!from.inRange(this) && !isDestroyed()) { + if (pilot instanceof Player player) { + player.displayClientMessage(Component.translatable("message.mcheli.uav.lost"), true); + } + pilot.stopRiding(); + hurt(damageSources().fellOutOfWorld(), hp() + 10.0F); + } + } + + @Override + public @Nullable LivingEntity getControllingPassenger() { + // A wreck is nobody's to fly; the server lets it fall, so its crash is seen the same way by everyone. + return occupant(0) instanceof Player player && !isDestroyed() ? player : null; + } + + @Override + protected boolean canAddPassenger(Entity passenger) { + AircraftDefinition def = definition(); + if (def == null || !def.canRide) { + return false; + } + if (pendingSeat >= 0) { + return isSeatFree(pendingSeat); + } + // Coming back aboard after a reload, a remote pilot included. + Integer saved = savedSeats.get(passenger.getUUID()); + if (saved != null && isSeatFree(saved)) { + return true; + } + // Cargo only boards through a rack, never into a crew seat. + return !(passenger instanceof AircraftEntity || passenger instanceof ContainerEntity) && firstFreeCrewSeat() >= 0; + } + + @Override + protected void addPassenger(Entity passenger) { + super.addPassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = pendingSeat; + pendingSeat = EMPTY; + if (seat < 0) { + Integer saved = savedSeats.remove(passenger.getUUID()); + seat = saved != null && isSeatFree(saved) ? saved : firstFreeCrewSeat(); + } + assignSeat(passenger, seat); + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = seatOf(passenger); + if (seat >= 0) { + if (hasNightVision(seat)) { + nightVisionSeats &= ~(1 << seat); + NightVision.end(passenger); + } + lastSeats.put(passenger.getId(), seat); + seatIds[seat] = EMPTY; + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onLeft(seat); + } + } + if (seat == 0) { + input.clear(); + if (station != null) { + endRemoteControl(passenger); + } + if (testFlightStart != null) { + remoteReturns.put(passenger.getId(), testFlightStart); + testFlightOver = true; + } + } + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + AircraftDefinition def = definition(); + int seat = seatOf(passenger); + if (def == null || seat < 0 || seat >= def.seatAndRackCount()) { + return super.getPassengerAttachmentPoint(passenger, dimensions, scale); + } + Vector3f p = seatPosition(def, seat, 1.0F); + return new Vec3(p.x, p.y, p.z); + } + + /** World offset of a seat from the vehicle origin. Turret seats turn with the pilot's view. */ + public Vector3f seatPosition(AircraftDefinition def, int seat, float partialTicks) { + Vector3 pos; + boolean turret; + if (seat < def.seatCount()) { + SeatDefinition s = def.seats.get(seat); + pos = s.position(); + turret = s.rotatingSeat(); + } else { + RackDefinition rack = def.rack(seat); + pos = rack.position(); + turret = rack.rotatingSeat(); + } + return toWorldOffset(turret ? onTurret(def, pos, partialTicks) : pos); + } + + /** Rotates a hull-space point around {@code TurretPosition} by the pilot's view yaw. */ + public Vector3 onTurret(AircraftDefinition def, Vector3 pos, float partialTicks) { + Vector3 t = def.turretPosition; + Vector3f v = AircraftRotation.toWorld(animation.seatYaw(0, partialTicks), 0.0F, 0.0F, + pos.x() - t.x(), pos.y() - t.y(), pos.z() - t.z()); + return new Vector3(v.x + t.x(), v.y + t.y(), v.z + t.z()); + } + + /** Rotates a hull-space point into the world frame, relative to the vehicle origin. */ + public Vector3f toWorldOffset(Vector3 local) { + return AircraftRotation.toWorld(getYRot(), getXRot(), roll(), local.x(), local.y(), local.z()); + } + + public Vec3 toWorld(Vector3 local) { + Vector3f v = toWorldOffset(local); + return position().add(v.x, v.y, v.z); + } + + @Override + protected void positionRider(Entity passenger, MoveFunction moveFunction) { + super.positionRider(passenger, moveFunction); + AircraftDefinition def = definition(); + if (def != null && passenger instanceof AircraftEntity cargo) { + RackDefinition rack = def.rack(seatOf(passenger)); + float yaw = rack != null ? rack.yaw() : 0.0F; + float pitch = rack != null ? rack.pitch() : 0.0F; + cargo.setYRot(getYRot() + yaw); + cargo.setXRot(getXRot() + pitch); + cargo.roll = roll(); + cargo.setDeltaMovement(Vec3.ZERO); + } else if (def != null && passenger instanceof ContainerEntity container) { + RackDefinition rack = def.rack(seatOf(passenger)); + container.setYRot(getYRot() + (rack != null ? rack.yaw() : 0.0F)); + container.setDeltaMovement(Vec3.ZERO); + } + } + + @Override + public Vec3 getDismountLocationForPassenger(LivingEntity passenger) { + Vec3 home = remoteReturns.remove(passenger.getId()); + if (home != null) { + return home; + } + AircraftDefinition def = definition(); + Integer seat = lastSeats.get(passenger.getId()); + if (def != null) { + Vector3 local; + if (def.unmountPosition != null) { + local = def.unmountPosition; + } else { + Vector3 seatPos = seat != null && seat < def.seatCount() ? def.seats.get(seat).position() : def.seats.getFirst().position(); + double x = seatPos.x() >= 0.0 ? seatPos.x() + 3.0 : seatPos.x() - 3.0; + local = new Vector3(x, 2.0, seatPos.z()); + } + Vec3 at = toWorld(local); + AABB box = passenger.getDimensions(Pose.STANDING).makeBoundingBox(at); + if (level().noCollision(passenger, box)) { + return at; + } + } + return super.getDismountLocationForPassenger(passenger); + } + + /** + * 1.20.6 hands the click position to {@code interactAt}, which the client sends before the + * plain {@code interact}; 26.x merged the two. + */ + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + ItemStack held = player.getItemInHand(hand); + if (held.getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (definition() != null && (held.getItem() instanceof FuelItem || held.getItem() instanceof WrenchItem)) { + if (level().isClientSide()) { + return InteractionResult.SUCCESS; + } + return held.getItem() instanceof FuelItem ? FuelItem.pour(held, this, player) : WrenchItem.useOn(held, this, player, hand); + } + InteractionResult result = super.interact(player, hand); + AircraftDefinition def = definition(); + if (result == InteractionResult.PASS && player.isSecondaryUseActive() && def != null && def.cargoRows() > 0 + && !isDestroyed() && player.getVehicle() != this) { + // Sneaking opens the cargo chest from outside, as in the original. + if (player instanceof ServerPlayer server) { + openCargo(server); + } + return InteractionResult.SUCCESS; + } + if (result != InteractionResult.PASS || player.isSecondaryUseActive()) { + return result; + } + if (def == null || !def.canRide || player.getVehicle() == this) { + return InteractionResult.PASS; + } + boolean boardingPilot = !def.uav && isSeatFree(0); + if (boardingPilot && canopy != null && isCanopyClosed()) { + // The first click opens the canopy, the next one climbs in. + if (!level().isClientSide()) { + openCanopy(); + } + return InteractionResult.SUCCESS; + } + if (boardingPilot && modeSwitchCooldown > 0) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + if (!player.startRiding(this)) { + return InteractionResult.PASS; + } + if (seatOf(player) == 0) { + closeCanopy(); + boardNearbyMobs(); + } + } + return InteractionResult.SUCCESS; + } + + // Racks + + /** Loads {@code cargo} into the given rack seat. Server only. */ + private boolean loadCargo(Entity cargo, int seat) { + if (!isSeatFree(seat) || cargo.isPassenger()) { + return false; + } + pendingSeat = seat; + boolean loaded = cargo.startRiding(this, true); + pendingSeat = EMPTY; + if (loaded) { + level().playSound(null, getX(), getY(), getZ(), SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.NEUTRAL, 1.0F, 1.0F); + } + return loaded; + } + + /** Whether {@code cargo} may ride rack {@code seat} of this vehicle. */ + public boolean acceptsCargo(AircraftEntity cargo, int seat) { + AircraftDefinition def = definition(); + AircraftDefinition cargoDef = cargo.definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + if (rack == null || cargoDef == null || cargo == this || isPassenger() || cargo.isPassenger()) { + return false; + } + // A vehicle that carries cargo itself cannot be loaded. + for (Entity p : cargo.getPassengers()) { + if (p instanceof AircraftEntity) { + return false; + } + } + if (rack.accepts(cargoDef.name(), cargoDef.kind())) { + return true; + } + for (RideRack rr : cargoDef.rideRacks) { + if (rr.carrier().equalsIgnoreCase(def.name()) && def.seatCount() + rr.rack() - 1 == seat) { + return true; + } + } + return false; + } + + /** Whether rack {@code seat} carries containers, {@code AddRack} naming {@code container}. */ + public boolean acceptsContainer(ContainerEntity container, int seat) { + AircraftDefinition def = definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + return rack != null && !isPassenger() && !container.isPassenger() + && rack.accepts(ContainerEntity.CARGO_NAME, ContainerEntity.CARGO_NAME); + } + + private boolean isNearRackEntry(AircraftEntity cargo, RackDefinition rack) { + Vec3 entry = toWorld(rack.entry()); + float r = rack.range(); + return Math.abs(cargo.getX() - entry.x) <= r && Math.abs(cargo.getY() - entry.y) <= r && Math.abs(cargo.getZ() - entry.z) <= r; + } + + /** Carrier pilot loads every nearby vehicle or container that fits a free rack. Server only. */ + public void loadNearbyCargo() { + AircraftDefinition def = definition(); + if (def == null || def.racks.isEmpty() || throttle > 0.3 || !hasBlockBelow(1, 3, true)) { + return; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (!isSeatFree(seat)) { + continue; + } + RackDefinition rack = def.rack(seat); + Vec3 entry = toWorld(rack.entry()); + AABB area = new AABB(entry, entry).inflate(rack.range()); + boolean loaded = false; + for (AircraftEntity cargo : level().getEntitiesOfClass(AircraftEntity.class, area)) { + if (acceptsCargo(cargo, seat) && loadCargo(cargo, seat)) { + loaded = true; + break; + } + } + if (!loaded) { + for (ContainerEntity container : level().getEntitiesOfClass(ContainerEntity.class, area)) { + if (acceptsContainer(container, seat) && loadCargo(container, seat)) { + break; + } + } + } + } + } + + /** Drops the last loaded piece of cargo at its rack's entry point. Server only. */ + public void unloadCargo() { + AircraftDefinition def = definition(); + if (def == null) { + return; + } + for (int seat = def.seatAndRackCount() - 1; seat >= def.seatCount(); seat--) { + Entity cargo = occupant(seat); + if (cargo == null) { + continue; + } + RackDefinition rack = def.rack(seat); + Vector3 drop = rack.entry(); + if (cargo instanceof AircraftEntity) { + // Aircraft roll out well clear of the carrier's tail or nose. + drop = drop.add(0.0, 0.0, drop.z() >= centreZ(def) ? 12.0 : -12.0); + } + Vec3 at = toWorld(drop); + cargo.stopRiding(); + cargo.moveTo(at.x, at.y, at.z, getYRot() + rack.yaw(), 0.0F); + // As in the original, with no ground in the 20 blocks below it goes down under a canopy. + if (!(cargo instanceof AircraftEntity) && !hasBlockBelow(3, 20, false)) { + ParachuteEntity.open(level(), cargo, ParachuteEntity.Kind.DROP); + } + return; + } + } + + /** Leaves the rack this vehicle rides on, at the rack entry point. Server only. */ + public void leaveRack() { + if (!(getVehicle() instanceof AircraftEntity carrier)) { + return; + } + AircraftDefinition carrierDef = carrier.definition(); + RackDefinition rack = carrierDef != null ? carrierDef.rack(carrier.seatOf(this)) : null; + Vec3 at = rack != null ? carrier.toWorld(rack.entry()) : position(); + stopRiding(); + moveTo(at.x, at.y, at.z, getYRot(), 0.0F); + } + + /** + * Pilot of a vehicle near a free rack of a carrier boards that rack. Server only. + * + * @return whether a rack was found + */ + public boolean boardNearbyRack() { + if (isPassenger()) { + return false; + } + for (AircraftEntity carrier : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(60.0))) { + AircraftDefinition def = carrier.definition(); + if (carrier == this || def == null) { + continue; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (carrier.isSeatFree(seat) && carrier.acceptsCargo(this, seat) && carrier.isNearRackEntry(this, def.rack(seat)) + && carrier.loadCargo(this, seat)) { + return true; + } + } + } + return false; + } + + private static double centreZ(AircraftDefinition def) { + if (def.boundingBoxes.isEmpty()) { + return 0.0; + } + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + for (var box : def.boundingBoxes) { + min = Math.min(min, box.offset().z() - box.width() / 2.0); + max = Math.max(max, box.offset().z() + box.width() / 2.0); + } + return (min + max) / 2.0; + } + + // Ejection + + /** Seats zero and one of vehicles with {@code EnableEjectionSeat} can bail out. */ + public boolean canEject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && def.ejectionSeat && seat >= 0 && seat < 2 && !(seat == 0 && def.uav); + } + + /** + * {@code ejectSeat}: throws the occupant clear of the aircraft in an ejection seat, whose + * parachute opens once it starts to fall. Each ejection takes a parachute from the parachute + * slots of the vehicle screen; without one the seat stays put. Creative players need none. + * Server only. + */ + public void eject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + if (def == null || !canEject(entity)) { + return; + } + boolean creative = entity instanceof Player player && player.getAbilities().instabuild; + if (!creative && !inventory.takeParachute()) { + if (entity instanceof ServerPlayer player) { + ModSounds.playTo(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.no_parachute"), true); + } + return; + } + Vector3 seatPos = def.seats.get(seat).position(); + Vec3 out = toWorld(seatPos.add(0.0, 2.0, 0.0)); + Vector3f push = toWorldOffset(new Vector3(0.0, 2.0, 0.0)); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement().add(push.x + (random.nextFloat() - 0.5) * 0.1, push.y, + push.z + (random.nextFloat() - 0.5) * 0.1)); + entity.hurtMarked = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.EJECTION_SEAT); + if (canopy != null && !canopy.status()) { + canopy.set(true, false); + modeSwitchCooldown = 40; + } + ModSounds.playAt(entity, "eject_seat", 5.0F, 1.0F); + } + + // Search lights + + public boolean isSearchLightOn() { + AircraftDefinition def = definition(); + return def != null && !def.searchLights.isEmpty() && entityData.get(DATA_SEARCH_LIGHT); + } + + /** {@code canSwitchSearchLight}: the pilot or the first crew seat, in vehicles that have one. */ + public boolean canSwitchSearchLight(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && !def.searchLights.isEmpty() && seat >= 0 && seat <= 1; + } + + /** Server only. */ + public void toggleSearchLight(Entity entity) { + if (canSwitchSearchLight(entity)) { + entityData.set(DATA_SEARCH_LIGHT, !entityData.get(DATA_SEARCH_LIGHT)); + } + } + + /** + * Where a turning search light points: the first crew seat's view, else the pilot's, as + * {@code renderLight} took them. Null with nobody aboard to aim it. + */ + public @Nullable Entity searchLightAimer() { + Entity crew = occupant(1); + return crew != null ? crew : occupant(0); + } + + // Troops: parachute drops and ropes + + public boolean isRepelling() { + return entityData.get(DATA_REPELLING); + } + + /** How far the ropes hang below their hooks, as a negative number. */ + public float ropeLength() { + return ropeLength; + } + + /** Where rope {@code index} hangs from, in the world. */ + public Vec3 ropePosition(int index) { + AircraftDefinition def = definition(); + return def != null && index >= 0 && index < def.repellingHooks.size() ? toWorld(def.repellingHooks.get(index).position()) : position(); + } + + /** + * {@code onUpdate_Repelling} and {@code onUpdate_UnmountCrew}: the ropes run out towards the + * ground, then the mobs aboard slide down them one per hook interval; a parachute drop lets one + * passenger out per {@code MobDropOption} interval while the hatch is open. + */ + private void tickRopes(AircraftDefinition def) { + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + int clear = airBelow(50); + if (ropeLength > -50.0F && ropeLength > -clear) { + ropeLength -= level().isClientSide() ? 0.3F : 0.25F; + } + } else { + ropeLength = 0.0F; + } + } + if (level().isClientSide()) { + return; + } + if (isRepelling() && isDestroyed()) { + entityData.set(DATA_REPELLING, false); + } + if (!isRepelling()) { + repellingTicks = 0; + } else if (repellingTicks < 60) { + repellingTicks++; + } else { + for (int rope = 0; rope < def.repellingHooks.size(); rope++) { + if (ticksAlive % def.repellingHooks.get(rope).interval() != 0) { + continue; + } + for (int seat = 1; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + rappel(mob, rope); + break; + } + } + } + } + if (droppingTroops) { + if (hasBlockBelow(3, 10, false) || isDestroyed()) { + droppingTroops = false; + } else if ((hatch == null || hatch.isOn()) && ticksAlive % def.dropInterval == 0 && !dropNextTroop(def)) { + droppingTroops = false; + } + } + } + + /** Air blocks straight below, up to {@code max}. */ + private int airBelow(int max) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + int i = 0; + while (i < max && pos.getY() - i > level().getMinBuildHeight() && level().getBlockState(pos.setY(blockPosition().getY() - i - 1)).isAir()) { + i++; + } + return i; + } + + /** + * {@code canStartRepelling}: a helicopter in hover mode, level and holding still. The original + * compared against where it was a few ticks before. + */ + private boolean canStartRepelling() { + return this instanceof HelicopterEntity && isHoveringMode() && Math.abs(getXRot()) < 3.0F && Math.abs(roll()) < 3.0F + && lastTickPos != null && position().distanceTo(lastTickPos) < 0.05; + } + + /** + * The pilot's drop key, {@code unmountCrew}: with ropes, lets them down from a steady hover + * or pulls them up again; with {@code EnableParachuting}, starts or stops dropping the mobs + * aboard under parachutes; otherwise, or close to the ground, simply lets the mobs out. + * Server only. + * + * @return false if nothing could be done, so the pilot hears it + */ + public boolean dropCrew() { + AircraftDefinition def = definition(); + if (def == null || isDestroyed()) { + return false; + } + if (!def.repellingHooks.isEmpty()) { + if (isRepelling()) { + entityData.set(DATA_REPELLING, false); + return true; + } + if (hasBlockBelow(3, 4, false)) { + return unloadMobs(); + } + if (!canStartRepelling()) { + return false; + } + entityData.set(DATA_REPELLING, true); + repellingTicks = 0; + return true; + } + if (droppingTroops) { + droppingTroops = false; + return true; + } + if (def.parachuting && !hasBlockBelow(3, 10, false)) { + droppingTroops = true; + if (hatch != null && hatch.isOff()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } + return true; + } + return unloadMobs(); + } + + /** Lets every mob in a crew seat out, as in {@code unmountCrew(false)}. */ + private boolean unloadMobs() { + AircraftDefinition def = definition(); + boolean any = false; + for (int seat = 1; def != null && seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + mob.stopRiding(); + any = true; + } + } + return any; + } + + /** The next mob behind the second seat leaves at the drop point under a parachute. */ + private boolean dropNextTroop(AircraftDefinition def) { + for (int seat = 2; seat < def.seatCount(); seat++) { + if (occupant(seat) instanceof Mob mob) { + parachuteOut(mob, def); + return true; + } + } + return false; + } + + private void parachuteOut(Entity entity, AircraftDefinition def) { + Vec3 out = toWorld(def.dropPosition); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement()); + entity.hurtMarked = true; + ParachuteEntity.open(level(), entity, ParachuteEntity.Kind.DROP); + } + + /** + * A passenger's own drop key, {@code MCH_ClientSeatTickHandler}: slide down a rope once the + * ropes are out, or jump with a parachute from behind the second seat of a vehicle with + * {@code EnableParachuting} and room below. Server only. + * + * @return false if neither is possible right now + */ + public boolean jumpOut(Player player) { + AircraftDefinition def = definition(); + int seat = seatOf(player); + if (def == null || seat <= 0 || seat >= def.seatCount() || isDestroyed()) { + return false; + } + if (isRepelling() && repellingTicks > 50 && !def.repellingHooks.isEmpty()) { + lastRope = (lastRope + 1) % def.repellingHooks.size(); + rappel(player, lastRope); + return true; + } + if (def.parachuting && seat >= 2 && !hasBlockBelow(3, 13, false)) { + parachuteOut(player, def); + return true; + } + return false; + } + + /** {@code unmountEntityRepelling}: two blocks under the hook, onto a rope rider that lets them down. */ + private void rappel(Entity entity, int rope) { + Vec3 at = ropePosition(rope).add(0.0, -2.0, 0.0); + entity.stopRiding(); + entity.teleportTo(at.x, at.y, at.z); + entity.setDeltaMovement(Vec3.ZERO); + entity.resetFallDistance(); + RappelEntity rider = new RappelEntity(ModEntities.RAPPEL.get(), level()); + rider.attach(this, rope); + rider.setPos(at); + level().addFreshEntity(rider); + entity.startRiding(rider, true); + lastRope = rope; + } + + /** + * {@code mountMobToSeats}: when the pilot climbs in, mobs standing close by take the free crew + * seats, so villagers and the like can be flown somewhere. Server only. + */ + private void boardNearbyMobs() { + AircraftDefinition def = definition(); + if (def == null || def.uav) { + return; + } + for (Mob mob : level().getEntitiesOfClass(Mob.class, getBoundingBox().inflate(3.0, 2.0, 3.0), m -> !m.isPassenger() && !m.isVehicle())) { + for (int seat = 1; seat < def.seatCount(); seat++) { + if (isSeatFree(seat)) { + pendingSeat = seat; + mob.startRiding(this, true); + pendingSeat = EMPTY; + break; + } + } + } + } + + // Terrain helpers + + /** True if any solid block is within {@code depth} blocks below the vehicle. */ + protected boolean isNearGround(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(0, -1, 0); + if (!level().getBlockState(pos).getCollisionShape(level(), pos).isEmpty()) { + return true; + } + } + return false; + } + + /** + * Port of {@code MCH_Lib.getBlockY}: scans a {@code size} x {@code size} column from the + * vehicle's block down {@code depth} layers (the first layer being the vehicle's own). + * + * @param solidOnly ignore blocks without collision; fluids still count when this is false + */ + public boolean hasBlockBelow(int size, int depth, boolean solidOnly) { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < depth; y++) { + for (int x = -size / 2; x <= size / 2; x++) { + for (int z = -size / 2; z <= size / 2; z++) { + pos.set(px + x, py - y, pz + z); + BlockState state = level().getBlockState(pos); + if (state.isAir()) { + continue; + } + if (!solidOnly || !state.getCollisionShape(level(), pos).isEmpty() || !state.getFluidState().isEmpty()) { + return true; + } + } + } + } + return false; + } + + /** True if the first non-air block within {@code depth} below is water. */ + protected boolean isWaterBelow(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + BlockState state = level().getBlockState(pos); + if (!state.isAir()) { + return !state.getFluidState().isEmpty(); + } + pos.move(0, -1, 0); + } + return false; + } + + /** {@code getBlockY(this, 3, -2, false)}: standing on something that is neither air nor water. */ + protected boolean isOnSolidGround() { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < 2; y++) { + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + BlockState state = level().getBlockState(pos.set(px + x, py - y, pz + z)); + if (!state.isAir()) { + return state.getFluidState().isEmpty(); + } + } + } + } + return false; + } + + /** + * Port of {@code getWaterDepth}: the share of the hit box that is in water, from 0 to 1, sampled + * in five horizontal slices. {@code FloatOffset} raises the slices, so a hull with an offset rides + * higher. The float physics only pushes up once this reaches 1, which sets the waterline. + * + *

The original drew the model {@value #LEGACY_MODEL_LIFT} above the bottom of the hit box + * (its {@code yOffset}, half the 0.7 default height); this port draws it at the bottom. The + * slices are lowered by the same amount so boats sit in the water as they did in 1.0.3. + */ + protected double waterDepth() { + if (entityData.get(DATA_SINKING)) { + return 0.0; + } + AircraftDefinition def = definition(); + float offset = (def != null ? def.floatOffset : 0.0F) - LEGACY_MODEL_LIFT; + AABB box = getBoundingBox(); + double height = box.maxY - box.minY; + double depth = 0.0; + for (int i = 0; i < 5; i++) { + double bottom = box.minY + height * i / 5.0 - 0.125 + offset; + double top = box.minY + height * (i + 1) / 5.0 - 0.125 + offset; + AABB slice = new AABB(box.minX, bottom, box.minZ, box.maxX, top, box.maxZ); + if (level().getBlockStates(slice).anyMatch(state -> state.getFluidState().is(FluidTags.WATER))) { + depth += 1.0 / 5.0; + } + } + return depth; + } + + /** {@code applyOnGroundPitch}: eases pitch towards {@code OnGroundPitch} and roll towards level. */ + protected void applyOnGroundPitch(float factor) { + AircraftDefinition def = definition(); + if (def != null) { + float ground = def.onGroundPitch; + setXRot((getXRot() - ground) * factor + ground); + } + roll *= factor; + } + + /** {@code StepHeight}: how tall a ledge the vehicle drives up without jumping. */ + @Override + public float maxUpStep() { + AircraftDefinition def = definition(); + return def != null ? def.stepHeight : 0.0F; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith() { + return true; + } + + @Override + public boolean isPushable() { + return false; + } + + // Weapons + + public @Nullable VehicleWeapons weapons() { + definition(); + return weapons; + } + + public void publishWeaponSelection() { + if (weapons != null && !level().isClientSide()) { + entityData.set(DATA_WEAPON_SELECTION, weapons.encodeSelection()); + } + } + + public void publishWeaponUse(int stat) { + entityData.set(DATA_WEAPON_USE, stat); + } + + public int syncedWeaponUse() { + return entityData.get(DATA_WEAPON_USE); + } + + /** + * Whether {@code set} fires along the user's view instead of the hull: the shot direction and + * the muzzle positions are worked out as if the vehicle faced where the user looks. Static + * mounts do this; see {@link GroundVehicleEntity#aimsWithUser}. + */ + public boolean aimsWithUser(WeaponSet set) { + return false; + } + + /** {@code isInfinityAmmo}: creative players never run dry, nor does anyone when the server config says so. */ + public boolean hasInfiniteAmmo(@Nullable Entity user) { + return ServerConfig.infiniteAmmo() || user instanceof Player player && player.getAbilities().instabuild; + } + + /** Where the pilot looks relative to the hull: the turret's traverse. */ + public float turretYaw() { + return animation.seatYaw(0, 1.0F); + } + + public float stealth() { + AircraftDefinition def = definition(); + return def != null ? def.stealth : 0.0F; + } + + /** The TV missile the crew is flying, if it still exists. */ + public @Nullable TvMissileEntity tvMissile() { + if (level().isClientSide()) { + return level().getEntity(entityData.get(DATA_TV_MISSILE)) instanceof TvMissileEntity m && !m.isRemoved() ? m : null; + } + return tvMissile != null && !tvMissile.isRemoved() ? tvMissile : null; + } + + public void setTvMissile(@Nullable TvMissileEntity missile) { + tvMissile = missile; + entityData.set(DATA_TV_MISSILE, missile != null ? missile.getId() : 0); + } + + /** Whether the flares are burning right now, for the crew's display. */ + public boolean isFlareActive() { + return entityData.get(DATA_FLARE); + } + + /** Someone is locking on to this vehicle; its crew hears the warning. Server only. */ + public void warnLocked() { + if (alertCount == 0 && !isDestroyed() && weapons != null && weapons.flares.isFitted()) { + alertCount = 10; + for (int seat = 0; seat < 2; seat++) { + if (occupant(seat) instanceof ServerPlayer player) { + ModSounds.playTo(player, "locked", 1.0F, 1.0F); + } + } + } + } + + // Hit boxes + + /** + * Clips a ray against the main hit box and every {@code BoundingBox}, remembering the damage + * factor of the box hit first, as the original's composite bounding box did. + */ + public Optional clipHitBoxes(Vec3 from, Vec3 to, double inflate) { + hitBoxFactor = 1.0F; + Optional best = clip(getBoundingBox().inflate(inflate), from, to); + double bestDistance = best.map(from::distanceToSqr).orElse(Double.MAX_VALUE); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + Optional hit = clip(hullBox(box).inflate(inflate), from, to); + if (hit.isPresent() && from.distanceToSqr(hit.get()) < bestDistance) { + best = hit; + bestDistance = from.distanceToSqr(hit.get()); + hitBoxFactor = box.damageFactor(); + } + } + } + return best; + } + + /** + * One {@code BoundingBox} in world space. Like the original's, it stays axis aligned: the + * centre turns with the hull, the box itself does not. + */ + private AABB hullBox(BoundingBoxDefinition box) { + Vec3 c = toWorld(box.offset()); + double hw = box.width() / 2.0; + double hh = box.height() / 2.0; + return new AABB(c.x - hw, c.y - hh, c.z - hw, c.x + hw, c.y + hh, c.z + hw); + } + + /** The whole hull: the main hit box, every {@code BoundingBox} and the generated boxes, in world space. */ + public List hullBoxes() { + AircraftDefinition def = definition(); + if (def == null || def.hull().isEmpty()) { + return List.of(getBoundingBox()); + } + List boxes = new ArrayList<>(def.hull().size() + 1); + boxes.add(getBoundingBox()); + for (BoundingBoxDefinition box : def.hull()) { + boxes.add(hullBox(box)); + } + return boxes; + } + + /** The smallest box around the whole hull. */ + private AABB hullBounds() { + AABB bounds = getBoundingBox(); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.hull()) { + bounds = bounds.minmax(hullBox(box)); + } + } + return bounds; + } + + /** Whether any part of this hull touches any part of {@code other}'s, grown by {@code inflate}. */ + private boolean hullsTouch(AircraftEntity other, double inflate) { + // Hulls generated from big models run to a thousand boxes, so skip ours that are nowhere near. + AABB around = other.hullBounds().inflate(inflate); + List theirs = null; + for (AABB mine : hullBoxes()) { + AABB grown = mine.inflate(inflate); + if (!grown.intersects(around)) { + continue; + } + if (theirs == null) { + theirs = other.hullBoxes(); + } + for (AABB box : theirs) { + if (grown.intersects(box)) { + return true; + } + } + } + return false; + } + + /** + * Vehicles whose hulls block this one: everything nearby except its own carrier or cargo, and + * vehicles either of the two could load onto a rack, which have to drive into each other. + */ + private List solidNeighbours(AABB area) { + return level().getEntitiesOfClass(AircraftEntity.class, area.inflate(HULL_REACH), other -> other != this && !other.isRemoved() + && other.definition() != null && !other.isPassenger() && !isPassengerOfSameVehicle(other) + && other.getVehicle() != this && getVehicle() != other && !canCarry(other) && !other.canCarry(this)); + } + + /** Whether a free rack of this vehicle would take {@code cargo}. */ + private boolean canCarry(AircraftEntity cargo) { + AircraftDefinition def = definition(); + if (def == null) { + return false; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (acceptsCargo(cargo, seat)) { + return true; + } + } + return false; + } + + /** + * Not in the original, where only the main box at a vehicle's feet collided: the whole hull + * blocks other vehicles. A move that would push this hull into another vehicle's is undone, + * sideways first and then completely, so a helicopter can also set down on a parked truck. + * Hulls that already overlapped, say after two vehicles were placed into each other, may move + * apart. + */ + @Override + public void move(MoverType type, Vec3 delta) { + if (type != MoverType.SELF || isPassenger() || definition() == null) { + super.move(type, delta); + return; + } + List near = solidNeighbours(hullBounds().expandTowards(delta)); + if (near.isEmpty()) { + super.move(type, delta); + return; + } + List overlapping = new ArrayList<>(); + for (AircraftEntity other : near) { + if (hullsTouch(other, 0.0)) { + overlapping.add(other); + } + } + Vec3 before = position(); + super.move(type, delta); + if (!entersHull(near, overlapping)) { + return; + } + Vec3 m = getDeltaMovement(); + setPos(before.x, getY(), before.z); + setDeltaMovement(0.0, m.y, 0.0); + if (entersHull(near, overlapping)) { + setPos(before); + setDeltaMovement(Vec3.ZERO); + } + } + + private boolean entersHull(List near, List overlapping) { + for (AircraftEntity other : near) { + if (!overlapping.contains(other) && hullsTouch(other, 0.0)) { + return true; + } + } + return false; + } + + /** A ray that starts inside a box hits it where it starts, as it does for vanilla entity picking. */ + private static Optional clip(AABB box, Vec3 from, Vec3 to) { + return box.contains(from) ? Optional.of(from) : box.clip(from, to); + } + + public float consumeHitBoxFactor() { + float f = hitBoxFactor; + hitBoxFactor = 1.0F; + return f; + } + + // Damage + + public boolean isDestroyed() { + return definition() != null && hp() <= 0; + } + + /** + * Port of the original's {@code attackEntityFrom}. Armour caps a hit at {@code ArmorMaxDamage}, + * scales it by {@code ArmorDamageFactor} and the struck box's factor, then subtracts + * {@code ArmorMinDamage}. A player punching an empty vehicle packs it up instead. + */ + @Override + public boolean hurt(DamageSource source, float damage) { + // 1.20.6 also calls this on the attacking client; only the server decides. + if (!(level() instanceof ServerLevel level)) { + return false; + } + float boxFactor = consumeHitBoxFactor(); + if (testFlightStart != null || isRemoved() || isInvulnerableTo(source) || timeSinceHit > 0) { + return false; + } + if (source.is(DamageTypes.IN_FIRE) || source.is(DamageTypes.CACTUS)) { + return false; + } + boolean lava = source.is(DamageTypes.LAVA); + boolean onFire = source.is(DamageTypes.ON_FIRE); + if (lava) { + damage *= random.nextInt(8) + 2; + timeSinceHit = 2; + } + if (source.is(DamageTypeTags.IS_EXPLOSION)) { + timeSinceHit = 1; + } else if (source.getEntity() != null && hasPassenger(source.getEntity())) { + return false; + } + if (onFire) { + timeSinceHit = 10; + } + + boolean creative = false; + boolean sneaking = false; + boolean packUp = false; + if (source.getEntity() instanceof Player player) { + creative = player.getAbilities().instabuild; + sneaking = player.isShiftKeyDown(); + if (source.is(DamageTypes.PLAYER_ATTACK)) { + packUp = creative || !hasPlayerPassenger(); + } + ModSounds.playAt(this, "hit", 1.0F, 1.0F); + } else { + ModSounds.playAt(this, "helidmg", 1.0F, 0.9F + random.nextFloat() * 0.1F); + } + + if (!isDestroyed()) { + if (!packUp) { + AircraftDefinition def = definition(); + if (def != null && !lava && !onFire) { + damage = Math.min(damage, def.armorMaxDamage); + if (boxFactor <= 1.0F) { + damage *= boxFactor; + } + damage *= def.armorDamageFactor; + damage -= def.armorMinDamage; + if (damage <= 0.0F) { + return false; + } + if (boxFactor > 1.0F) { + damage *= boxFactor; + } + } + damage *= ServerConfig.vehicleDamageFactor(); + entityData.set(DATA_HP, Math.max(hp() - (int) damage, 0)); + } + setHurtDir(-getHurtDir()); + // No markHurt(): it sends the server's motion to every client, which snaps a vehicle + // its pilot is flying back a step and looks like knockback on every hit. + setHurtTime(10); + if (hp() <= 0 || packUp) { + if (!packUp) { + entityData.set(DATA_HP, 0); + destroyVehicle(); + timeSinceHit = 20; + if (isAfloat() || isHeavy() && isNearGround(3)) { + startCookOff(source.getEntity()); + } else if (source.is(DamageTypes.IN_WALL) || source.is(DamageTypes.FLY_INTO_WALL) || source.is(DamageTypes.FALL)) { + crashExplosion(); + } else { + Explosions.explode(level, getX(), getY(), getZ(), + new Explosions.Params(null, source.getEntity(), 2.0F, 2.0F, true, true, 5, false, null)); + } + } else { + // In creative the item only drops when sneaking, as with the original's defaults. + if (!creative || sneaking) { + spawnAtLocation(getPickResult() != null ? getPickResult() : ItemStack.EMPTY); + } + discard(); + } + } + } else if (packUp && creative) { + discard(); + } + return true; + } + + private boolean hasPlayerPassenger() { + for (Entity passenger : getPassengers()) { + if (passenger instanceof Player) { + return true; + } + } + return false; + } + + /** {@code destroyAircraft}: the wreck tumbles, burns and disappears after 30 seconds. Server only. */ + private void destroyVehicle() { + AircraftDefinition def = definition(); + despawnCount = def != null && def.targetDrone ? 50 : WRECK_TICKS; + setHovering(false); + if (def != null && def.uav) { + Entity remote = occupant(0); + if (remote != null) { + remote.stopRiding(); + } + } + for (int seat = 0; seat < 2; seat++) { + Entity crew = occupant(seat); + if (crew != null && canEject(crew)) { + eject(crew); + } + } + } + + /** {@code explosionByCrash}: a wreck hitting the ground goes up in a fireball sized by its fuel. */ + private void crashExplosion() { + if (!(level() instanceof ServerLevel level)) { + return; + } + if (isHeavy()) { + startCookOff(null); + return; + } + exploded = true; + AircraftDefinition def = definition(); + float size = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + Vec3 at = def != null && def.fuelTankPosition != null ? toWorld(def.fuelTankPosition) : position(); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, null, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + if (!isInWater()) { + Explosions.crashFireball(level, at.x, at.y, at.z, size); + } + } + + /** + * Airliners, bombers and transports ({@code MaxFuel} of {@value HEAVY_FUEL} or more): like a + * boat, they go up in three blasts rather than one fireball. + */ + private boolean isHeavy() { + AircraftDefinition def = definition(); + return def != null && def.maxFuel >= HEAVY_FUEL; + } + + /** A floating vehicle sitting on the water, such as a patrol boat or a float plane at rest. */ + private boolean isAfloat() { + AircraftDefinition def = definition(); + return def != null && def.floats && waterDepth() > 0.0; + } + + /** + * A boat destroyed on the water, or a heavy aircraft wrecked on the ground, does not go up in + * one crash fireball; its fuel and magazine cook off in three blasts over about five seconds, + * then a hull in the water sinks. Not in the original, which only had the crash explosion. + */ + private void startCookOff(@Nullable Entity cause) { + exploded = true; + cookOffBlasts = 3; + cookOffDelay = 1; + cookOffCause = cause; + } + + private void tickCookOff(ServerLevel level) { + if (cookOffBlasts <= 0 || --cookOffDelay > 0) { + return; + } + AircraftDefinition def = definition(); + int blast = 4 - cookOffBlasts--; + float fuel = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + // The fuel tank first, then whatever the magazine still holds, then the hull goes up. + float size = switch (blast) { + case 1 -> fuel * 0.6F; + case 2 -> Math.max(munitionSize(), fuel * 0.4F); + default -> fuel; + }; + size = Mth.clamp(size, 1.0F, 15.0F); + Vec3 at = cookOffPoint(def); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, cookOffCause, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + // The blasts happen at the waterline, but the fire is above it, so show the full fireball. + Explosions.crashFireball(level, at.x, at.y, at.z, size); + if (cookOffBlasts > 0) { + // Three blasts inside five seconds: about 1.5 to 2.5 seconds apart. + cookOffDelay = 30 + random.nextInt(20); + } else { + // A boat or a flying boat goes under; a heavy aircraft on land stays where it burns. + if (isInWater() || waterDepth() > 0.0) { + entityData.set(DATA_SINKING, true); + } + cookOffCause = null; + } + } + + /** + * A cooked-off wreck going under, in place of the vehicle's own physics: it settles slowly, + * stern first with the bow raised and listing to one side, drifts to a stop and comes to rest + * on the bottom. Out of the water it simply falls. + */ + private void sink() { + Vec3 m = getDeltaMovement(); + double my = isInWater() ? Math.max(m.y - 0.003, -SINK_SPEED) : m.y - 0.04; + Vec3 motion = new Vec3(m.x * 0.9, my, m.z * 0.9); + move(MoverType.SELF, motion); + setDeltaMovement(onGround() ? motion.multiply(0.5, 0.0, 0.5) : motion); + if (!onGround()) { + // Which way it lists only has to be the same on every side, so take it from the id. + float list = (getId() & 1) == 0 ? SINK_LIST : -SINK_LIST; + setXRot(Mth.approach(getXRot(), -SINK_PITCH, 0.12F)); + roll = Mth.approach(roll, list, 0.15F); + } + } + + /** + * How big the ammunition aboard goes up: each weapon's {@code Explosion} scaled by how full its + * magazine and reserve still are. Guns and empty racks add nothing. + */ + private float munitionSize() { + if (weapons == null) { + return 0.0F; + } + float size = 0.0F; + for (WeaponSet set : weapons.sets()) { + int rounds = set.ammo() + set.restAmmo(); + if (rounds > 0 && set.definition().explosion > 0) { + size += set.definition().explosion * Math.min(1.0F, (float) rounds / Math.max(set.maxAmmo(), 1)); + } + } + return size; + } + + /** A random spot on the hull: one of its hit boxes, or the middle of the body. */ + private Vec3 cookOffPoint(@Nullable AircraftDefinition def) { + if (def == null || def.boundingBoxes.isEmpty() || random.nextInt(def.boundingBoxes.size() + 1) == 0) { + return position().add(0.0, def != null ? def.bodyHeight / 2.0 : 0.5, 0.0); + } + return toWorld(def.boundingBoxes.get(random.nextInt(def.boundingBoxes.size())).offset()); + } + + /** Destroyed state each tick: the wreck spins, sets its crew alight and explodes on impact. */ + private void tickDestroyed() { + if (!isDestroyed()) { + return; + } + input.clear(); + throttle *= 0.98; + crashSpin(); + if (!(level() instanceof ServerLevel level)) { + return; + } + tickCookOff(level); + if (ticksAlive % 20 == 0) { + AircraftDefinition def = definition(); + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null && !(seat == 0 && def != null && def.uav)) { + crew.igniteForSeconds(5); + } + } + } + if (despawnCount > 0 && --despawnCount <= 1) { + ejectPassengers(); + discard(); + } + } + + /** + * Not in the original: a shot down plane rolls into a nose-down spiral and a helicopter spins + * with some wobble, until it is close to the ground. Tanks and cars just stop. + */ + private void crashSpin() { + if (!(this instanceof HelicopterEntity) && !(this instanceof PlaneEntity) || isNearGround(3) || isInWater()) { + return; + } + int direction = CrashSpin.direction(getId()); + int ticks = spinTicks++; + if (this instanceof HelicopterEntity) { + setYRot(getYRot() + CrashSpin.helicopterYawRate(ticks, direction)); + setXRot(Mth.approach(getXRot(), CrashSpin.helicopterWobblePitch(ticks, getId()), 2.0F)); + roll = Mth.approach(roll, CrashSpin.helicopterWobbleRoll(ticks, getId()), 3.0F); + } else { + setYRot(getYRot() + CrashSpin.planeYawRate(ticks, direction)); + setXRot(CrashSpin.planePitch(getXRot())); + roll = AircraftRotation.wrap(roll + CrashSpin.planeRollRate(ticks, direction)); + } + } + + /** {@code onUpdate_Particle2}: smoke from a damaged hull, thicker the lower its hit points. */ + private void damageSmoke(AircraftDefinition def) { + float ratio = (float) hp() / def.maxHp; + if (ratio > 0.5F) { + return; + } + int sources = 1 + def.boundingBoxes.size(); + int pick = random.nextInt(sources); + Vec3 at = pick == 0 ? position().add(0.0, def.bodyHeight / 2.0, 0.0) : toWorld(def.boundingBoxes.get(pick - 1).offset()); + if (ratio < random.nextFloat() / 2.0F) { + level().addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, at.x, at.y, at.z, + (random.nextDouble() - 0.5) * 0.3, random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.3); + } + if (isDestroyed()) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, at.x, at.y, at.z, 0.0, 0.07, 0.0); + if (random.nextInt(3) == 0) { + level().addParticle(ParticleTypes.FLAME, at.x + random.nextGaussian() * 0.5, at.y, at.z + random.nextGaussian() * 0.5, 0.0, 0.05, 0.0); + } + wreckFire(def); + } + } + + /** + * Not in the original: the wreck keeps burning until it disappears, with a fire glow that + * lights up the smoke at night and dies down over the last few seconds. Nothing burns under + * water. Client only. + */ + private void wreckFire(AircraftDefinition def) { + wreckTicks++; + if (isInWater()) { + return; + } + float left = Math.clamp((float) (WRECK_TICKS - wreckTicks) / WRECK_FADE_TICKS, 0.15F, 1.0F); + Vec3 centre = def.enginePosition != null ? toWorld(def.enginePosition) : position().add(0.0, def.bodyHeight / 2.0, 0.0); + if (wreckTicks % 8 == 0) { + // Each glow outlives the gap to the next, so the fire never goes dark in between. + float size = Math.clamp(def.bodyWidth * 0.8F, 2.0F, 8.0F) * left; + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size, 24), true, + centre.x, centre.y, centre.z, 0.0, 0.02, 0.0); + } + if (random.nextFloat() < left) { + double spread = def.bodyWidth * 0.4; + level().addParticle(ParticleTypes.FLAME, centre.x + random.nextGaussian() * spread, centre.y, + centre.z + random.nextGaussian() * spread, 0.0, 0.04, 0.0); + } + if (random.nextInt(10) == 0) { + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 20 + random.nextInt(20)), true, centre.x, centre.y, centre.z, + (random.nextDouble() - 0.5) * 0.15, 0.1 + random.nextDouble() * 0.15, (random.nextDouble() - 0.5) * 0.15); + } + } + + @Override + public @Nullable ItemStack getPickResult() { + AircraftDefinition def = definition(); + return def != null ? AircraftItem.stackOf(def) : null; + } + + // Dimensions and syncing + + @Override + public EntityDimensions getDimensions(Pose pose) { + AircraftDefinition def = definition(); + return def != null ? EntityDimensions.scalable(def.bodyWidth, def.bodyHeight) : super.getDimensions(pose); + } + + @Override + public void onSyncedDataUpdated(EntityDataAccessor accessor) { + super.onSyncedDataUpdated(accessor); + if (DATA_TYPE.equals(accessor)) { + refreshDimensions(); + definition(); + } else if (DATA_SEATS.equals(accessor) && level().isClientSide()) { + decodeSeats(entityData.get(DATA_SEATS)); + } else if (DATA_WEAPON_SELECTION.equals(accessor) && level().isClientSide() && weapons() != null) { + weapons.decodeSelection(entityData.get(DATA_WEAPON_SELECTION), ClientAccess.localPlayer()); + } + } + + /** + * Server position updates are eased in over three ticks, as 26.x's linear interpolation does, + * with the step vanilla's boat uses on 1.20.6. + */ + @Override + public void lerpTo(double x, double y, double z, float yRot, float xRot, int steps) { + lerpX = x; + lerpY = y; + lerpZ = z; + lerpYRot = yRot; + lerpXRot = xRot; + lerpSteps = LERP_STEPS; + } + + @Override + public double lerpTargetX() { + return lerpSteps > 0 ? lerpX : getX(); + } + + @Override + public double lerpTargetY() { + return lerpSteps > 0 ? lerpY : getY(); + } + + @Override + public double lerpTargetZ() { + return lerpSteps > 0 ? lerpZ : getZ(); + } + + @Override + public float lerpTargetXRot() { + return lerpSteps > 0 ? (float) lerpXRot : getXRot(); + } + + @Override + public float lerpTargetYRot() { + return lerpSteps > 0 ? (float) lerpYRot : getYRot(); + } + + private void tickLerp() { + if (isControlledByLocalInstance()) { + lerpSteps = 0; + syncPacketPositionCodec(getX(), getY(), getZ()); + } + if (lerpSteps > 0) { + lerpPositionAndRotationStep(lerpSteps, lerpX, lerpY, lerpZ, lerpYRot, lerpXRot); + lerpSteps--; + } + } + + /** A vehicle that is destroyed, packed up or killed spills what it carried; one that unloads keeps it. */ + @Override + public void remove(RemovalReason reason) { + if (reason.shouldDestroy() && !level().isClientSide() && !isRemoved()) { + inventory.dropAll(); + } + super.remove(reason); + } + + @Override + protected void addAdditionalSaveData(CompoundTag tag) { + tag.putString("Type", typeName()); + tag.putString("Texture", texture()); + tag.putInt("Hp", hp()); + tag.putInt("Fuel", fuel()); + tag.putFloat("Roll", roll); + tag.putBoolean("Hovering", isHoveringMode()); + tag.putBoolean("GunnerMode", isGunnerMode()); + tag.putInt("Parts", partBits()); + tag.putInt("DespawnCount", despawnCount); + tag.putInt("CookOff", cookOffBlasts); + tag.putBoolean("Sinking", entityData.get(DATA_SINKING)); + tag.putBoolean("SearchLight", entityData.get(DATA_SEARCH_LIGHT)); + if (weapons != null) { + tag.putIntArray("WeaponsAmmo", weapons.saveAmmo()); + } + inventory.save(tag); + if (station != null && stationReturn != null) { + tag.putUUID("Station", station); + tag.putDouble("StationReturnX", stationReturn.x); + tag.putDouble("StationReturnY", stationReturn.y); + tag.putDouble("StationReturnZ", stationReturn.z); + } + ListTag seats = new ListTag(); + for (int i = 0; i < seatIds.length; i++) { + Entity e = occupant(i); + seats.add(StringTag.valueOf(e != null ? e.getUUID().toString() : "")); + } + tag.put("Seats", seats); + } + + @Override + protected void readAdditionalSaveData(CompoundTag tag) { + entityData.set(DATA_TYPE, tag.getString("Type")); + entityData.set(DATA_TEXTURE, tag.getString("Texture")); + entityData.set(DATA_PARTS, tag.getInt("Parts")); + AircraftDefinition def = definition(); + entityData.set(DATA_HP, tag.contains("Hp") ? tag.getInt("Hp") : def != null ? def.maxHp : 1); + // 1.0.0 and the earlier betas had no fuel; their vehicles come back with a full tank. + entityData.set(DATA_FUEL, tag.contains("Fuel") ? tag.getInt("Fuel") : def != null ? def.maxFuel : 0); + roll = tag.getFloat("Roll"); + setHovering(tag.getBoolean("Hovering")); + entityData.set(DATA_GUNNER_MODE, tag.getBoolean("GunnerMode")); + despawnCount = tag.getInt("DespawnCount"); + if (hp() <= 0 && despawnCount <= 0) { + despawnCount = WRECK_TICKS; + } + cookOffBlasts = tag.getInt("CookOff"); + cookOffDelay = cookOffBlasts > 0 ? 20 : 0; + entityData.set(DATA_SINKING, tag.getBoolean("Sinking")); + entityData.set(DATA_SEARCH_LIGHT, tag.getBoolean("SearchLight")); + exploded |= cookOffBlasts > 0 || entityData.get(DATA_SINKING); + if (tag.contains("WeaponsAmmo", Tag.TAG_INT_ARRAY)) { + int[] ammo = tag.getIntArray("WeaponsAmmo"); + if (weapons != null) { + weapons.loadAmmo(ammo); + } else { + savedAmmo = ammo; + } + } + inventory.load(tag); + station = tag.hasUUID("Station") ? tag.getUUID("Station") : null; + stationReturn = station != null ? new Vec3(tag.getDouble("StationReturnX"), tag.getDouble("StationReturnY"), tag.getDouble("StationReturnZ")) : null; + savedSeats.clear(); + ListTag seats = tag.getList("Seats", Tag.TAG_STRING); + for (int i = 0; i < seats.size(); i++) { + if (!seats.getString(i).isEmpty()) { + try { + savedSeats.put(UUID.fromString(seats.getString(i)), i); + } catch (IllegalArgumentException ignored) { + // A hand edited save; that occupant simply takes the first free seat. + } + } + } + for (PartState part : parts) { + part.force(part.status()); + } + refreshDimensions(); + } + + /** Raw pilot input, filled in by the client controller every tick. */ + public static final class ControlInput { + public boolean throttleUp; + public boolean throttleDown; + public boolean left; + public boolean right; + public boolean brake; + /** The pilot is looking around instead of steering; planes and tanks yaw with A/D then. */ + public boolean freeLook; + + public void clear() { + throttleUp = false; + throttleDown = false; + left = false; + right = false; + brake = false; + freeLook = false; + } + + public byte write() { + return (byte) ((throttleUp ? 1 : 0) | (throttleDown ? 2 : 0) | (left ? 4 : 0) | (right ? 8 : 0) + | (brake ? 16 : 0) | (freeLook ? 32 : 0)); + } + + public void read(byte bits) { + throttleUp = (bits & 1) != 0; + throttleDown = (bits & 2) != 0; + left = (bits & 4) != 0; + right = (bits & 8) != 0; + brake = (bits & 16) != 0; + freeLook = (bits & 32) != 0; + } + } + + public int getHurtTime() { + return entityData.get(DATA_HURT_TIME); + } + + public void setHurtTime(int ticks) { + entityData.set(DATA_HURT_TIME, ticks); + } + + public int getHurtDir() { + return entityData.get(DATA_HURT_DIR); + } + + public void setHurtDir(int dir) { + entityData.set(DATA_HURT_DIR, dir); + } + + /** The yaw for this frame, as 26.x's {@code Entity#getYRot(float)} gives it. */ + public float getYRot(float partialTicks) { + return partialTicks == 1.0F ? getYRot() : Mth.rotLerp(partialTicks, yRotO, getYRot()); + } + + /** The pitch for this frame, as 26.x's {@code Entity#getXRot(float)} gives it. */ + public float getXRot(float partialTicks) { + return partialTicks == 1.0F ? getXRot() : Mth.lerp(partialTicks, xRotO, getXRot()); + } +} From f1c7744e44657f9807457b6466e0d12fff81e24b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:29 +0800 Subject: [PATCH 1217/1461] Add entity/AnimationState.java to the 1.20.6 shared sources --- .../mcheli/entity/AnimationState.java | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java new file mode 100644 index 00000000..eda5ed7b --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java @@ -0,0 +1,202 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.RackDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; + +/** + * Purely visual state that every side advances once per tick: wheels, tracks, spinning parts and + * where each crew member is looking relative to the hull. + * + *

Ported from {@code updatePartWheel}, {@code updatePartCrawlerTrack} and + * {@code onUpdate_PartRotation}. Nothing in here affects physics, so it is never synced; each + * client derives it from the synced throttle, input and crew rotations. + */ +public final class AnimationState { + /** Road wheel roll in degrees. */ + public float wheelRoll; + public float prevWheelRoll; + /** Steering from -1 (full right) to 1 (full left). */ + public float steer; + public float prevSteer; + /** Track roller angle per side, index 1 is positive X. */ + public final float[] roller = new float[2]; + public final float[] prevRoller = new float[2]; + /** Track run per side, in link spacings from 0 to 1. */ + public final float[] track = new float[2]; + public final float[] prevTrack = new float[2]; + private final float[] trackSpeed = new float[2]; + public float[] rotatingParts = new float[0]; + public float[] prevRotatingParts = new float[0]; + public float throttle; + public float prevThrottle; + /** Per seat and rack: view yaw and pitch of the occupant relative to the hull. */ + public float[] seatYaw = new float[0]; + public float[] prevSeatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public float[] prevSeatPitch = new float[0]; + + void resize(AircraftDefinition def) { + rotatingParts = new float[def.rotatingParts.size()]; + prevRotatingParts = new float[def.rotatingParts.size()]; + int seats = def.seatAndRackCount(); + seatYaw = new float[seats]; + prevSeatYaw = new float[seats]; + seatPitch = new float[seats]; + prevSeatPitch = new float[seats]; + for (int i = def.seatCount(); i < seats; i++) { + RackDefinition rack = def.rack(i); + seatYaw[i] = prevSeatYaw[i] = rack.yaw(); + seatPitch[i] = prevSeatPitch[i] = rack.pitch(); + } + } + + void tick(AircraftEntity vehicle, AircraftDefinition def) { + AircraftEntity.ControlInput input = vehicle.input(); + float visibleThrottle = vehicle.visibleThrottle(); + float reverse = vehicle.visibleReverse(); + prevThrottle = throttle; + throttle = visibleThrottle; + + tickWheels(def, input, visibleThrottle, reverse); + tickTracks(def, input, visibleThrottle, reverse); + tickRotatingParts(def, vehicle.getControllingPassenger() != null); + tickCrewAim(vehicle, def); + } + + private void tickWheels(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + prevWheelRoll = wheelRoll; + prevSteer = steer; + if (def.enableBack && reverse > 0.01F && throttle <= 0.0) { + throttle = -reverse * 15.0F; + } + if (input.left && !input.right) { + steer = Math.min(steer + 0.1F, 1.0F); + } else if (input.right && !input.left) { + steer = Math.max(steer - 0.1F, -1.0F); + } else { + steer *= 0.9F; + } + wheelRoll += (float) (throttle * def.partWheelRot); + if (wheelRoll >= 360.0F) { + wheelRoll -= 360.0F; + prevWheelRoll -= 360.0F; + } else if (wheelRoll < 0.0F) { + wheelRoll += 360.0F; + prevWheelRoll += 360.0F; + } + } + + private void tickTracks(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + System.arraycopy(roller, 0, prevRoller, 0, 2); + System.arraycopy(track, 0, prevTrack, 0, 2); + double pivot = def.pivotTurnThrottle <= 0.0F ? 1.0 : def.pivotTurnThrottle * 0.1F; + boolean left = input.left; + boolean right = input.right; + int dir = 1; + if (def.enableBack && reverse > 0.0F && throttle <= 0.0) { + throttle = -reverse * 5.0F; + // Reversing swaps which track leads a turn. + if (left != right) { + boolean t = left; + left = right; + right = t; + dir = -1; + } + } + if (left && !right) { + double t = 0.2 * dir; + trackSpeed[0] += (float) t; + trackSpeed[1] -= (float) (pivot * t); + } else if (right && !left) { + double t = 0.2 * dir; + trackSpeed[0] -= (float) (pivot * t); + trackSpeed[1] += (float) t; + } else { + float t = (float) Mth.clamp(throttle, -0.2, 0.2); + trackSpeed[0] += t; + trackSpeed[1] += t; + } + + for (int i = 0; i < 2; i++) { + trackSpeed[i] = Mth.clamp(trackSpeed[i], -0.72F, 0.72F); + roller[i] += trackSpeed[i] * def.trackRollerRot; + if (roller[i] >= 360.0F) { + roller[i] -= 360.0F; + prevRoller[i] -= 360.0F; + } else if (roller[i] < 0.0F) { + roller[i] += 360.0F; + prevRoller[i] += 360.0F; + } + track[i] -= trackSpeed[i]; + while (track[i] >= 1.0F) { + track[i]--; + prevTrack[i]--; + } + while (track[i] < 0.0F) { + track[i]++; + } + while (prevTrack[i] < 0.0F) { + prevTrack[i]++; + } + trackSpeed[i] *= 0.75F; + } + } + + private void tickRotatingParts(AircraftDefinition def, boolean piloted) { + for (int i = 0; i < rotatingParts.length && i < def.rotatingParts.size(); i++) { + RotatingPart part = def.rotatingParts.get(i); + prevRotatingParts[i] = rotatingParts[i]; + if (part.always() || piloted) { + float r = rotatingParts[i] + part.speed(); + if (r < 0.0F) { + r += 360.0F; + prevRotatingParts[i] += 360.0F; + } else if (r >= 360.0F) { + r -= 360.0F; + prevRotatingParts[i] -= 360.0F; + } + rotatingParts[i] = r; + } + } + } + + private void tickCrewAim(AircraftEntity vehicle, AircraftDefinition def) { + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + float hullRoll = vehicle.roll(); + for (int i = 0; i < seatYaw.length && i < def.seatCount(); i++) { + prevSeatYaw[i] = seatYaw[i]; + prevSeatPitch[i] = seatPitch[i]; + Entity crew = vehicle.occupant(i); + if (crew == null) { + continue; + } + float yaw = Mth.wrapDegrees(crew.getYRot() - hullYaw); + double rad = Math.toRadians(yaw); + // The hull tilt along the crew member's view direction, as in the original tank code. + float tilt = (float) (hullPitch * Math.cos(rad) + hullRoll * Math.sin(rad)); + float pitch = crew.getXRot() - tilt; + // Keep the previous value on the same side of the seam so interpolation never spins. + prevSeatYaw[i] = yaw + Mth.wrapDegrees(prevSeatYaw[i] - yaw); + seatYaw[i] = yaw; + seatPitch[i] = pitch; + } + } + + public float seatYaw(int seat, float partialTicks) { + if (seat < 0 || seat >= seatYaw.length) { + return 0.0F; + } + return prevSeatYaw[seat] + (seatYaw[seat] - prevSeatYaw[seat]) * partialTicks; + } + + public float seatPitch(int seat, float partialTicks) { + if (seat < 0 || seat >= seatPitch.length) { + return 0.0F; + } + return prevSeatPitch[seat] + (seatPitch[seat] - prevSeatPitch[seat]) * partialTicks; + } +} From cd55c7ce9b5645c7c7eb9a081ef6ea175add6368 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:30 +0800 Subject: [PATCH 1218/1461] Add entity/ChainEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/ChainEntity.java | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java new file mode 100644 index 00000000..205e250c --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ChainEntity.java @@ -0,0 +1,191 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * A tow chain, {@code MCH_EntityChain}: it hangs above the vehicle that pulls and drags the towed + * one along, letting it drift up to the chain's length away and never more than two blocks past + * that. It goes when either end does. + */ +public class ChainEntity extends Entity { + public static final int MIN_LENGTH = 3; + public static final int MAX_LENGTH = 15; + /** Ticks a chain loaded from disk waits for both ends to load before it gives up. */ + private static final int SEARCH_TICKS = 50; + + private static final EntityDataAccessor DATA_TOW = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TOWED = SynchedEntityData.defineId(ChainEntity.class, EntityDataSerializers.INT); + + private @Nullable UUID towId; + private @Nullable UUID towedId; + private @Nullable Entity tow; + private @Nullable Entity towed; + private int length = 4; + private int searchTicks = SEARCH_TICKS; + + public ChainEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + /** Chains {@code towed} to {@code tow}, at the distance they are apart now. Server only. */ + public static ChainEntity connect(Level level, Entity towed, Entity tow) { + ChainEntity chain = new ChainEntity(ModEntities.CHAIN.get(), level); + chain.setPos(tow.position().add(towed.position()).scale(0.5)); + chain.length = Mth.clamp((int) towed.distanceTo(tow), MIN_LENGTH, MAX_LENGTH); + chain.attach(towed, tow); + level.addFreshEntity(chain); + ModSounds.playAt(chain.tow, "chain_ct", 1.0F, 1.0F); + return chain; + } + + /** Whether {@code entity} can pull the other end: only vehicles can. */ + public static boolean canTow(Entity entity) { + return entity instanceof AircraftEntity; + } + + /** + * Only a vehicle pulls. Hooked the other way round, a container would hold a helicopter down, + * so the ends swap whichever order they were clicked in (and chains saved that way are fixed + * when they load). + */ + private void attach(Entity towed, Entity tow) { + if (!canTow(tow) && canTow(towed)) { + Entity swap = tow; + tow = towed; + towed = swap; + } + this.towed = towed; + this.tow = tow; + this.towedId = towed.getUUID(); + this.towId = tow.getUUID(); + entityData.set(DATA_TOWED, towed.getId()); + entityData.set(DATA_TOW, tow.getId()); + searchTicks = 0; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_TOW, -1); + builder.define(DATA_TOWED, -1); + } + + /** The end that pulls; on a client, as far as it has loaded. */ + public @Nullable Entity tow() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOW)) : tow; + } + + /** The end that is pulled. */ + public @Nullable Entity towed() { + return level().isClientSide() ? level().getEntity(entityData.get(DATA_TOWED)) : towed; + } + + /** Whether {@code entity} is either end of this chain. */ + public boolean holds(Entity entity) { + return entity == tow() || entity == towed(); + } + + /** Takes the chain off, with the sound of it coming loose. Server only. */ + public void disconnect() { + ModSounds.playAt(this, "chain_ct", 1.0F, 1.0F); + discard(); + } + + /** The chain on {@code entity}, at either end, if there is one within reach. */ + public static @Nullable ChainEntity on(Entity entity) { + for (ChainEntity chain : entity.level().getEntitiesOfClass(ChainEntity.class, entity.getBoundingBox().inflate(25.0))) { + if (chain.holds(entity)) { + return chain; + } + } + return null; + } + + @Override + public void tick() { + super.tick(); + if (!(level() instanceof ServerLevel server)) { + return; + } + if (searchTicks > 0) { + searchTicks--; + Entity foundTowed = towedId != null ? server.getEntity(towedId) : null; + Entity foundTow = towId != null ? server.getEntity(towId) : null; + if (foundTowed != null && foundTow != null) { + attach(foundTowed, foundTow); + } + return; + } + if (tow == null || towed == null || tow.isRemoved() || towed.isRemoved() || tow == towed) { + disconnect(); + return; + } + towed.resetFallDistance(); + setPos(tow.getX(), tow.getY() + 2.0, tow.getZ()); + pull(tow, towed); + } + + /** {@code updateTowingEntityPosRot}: turns the towed end to face along the chain and drags it in. */ + private void pull(Entity tow, Entity towed) { + Vec3 d = towed.position().subtract(tow.position()); + double dist = d.length(); + if (dist <= length) { + return; + } + towed.setYRot((float) Math.toDegrees(Math.atan2(d.z, d.x)) + 90.0F); + double slack = dist - length; + towed.setDeltaMovement(towed.getDeltaMovement().subtract(d.scale(0.001 / slack))); + double max = length + 2.0; + if (dist > max) { + Vec3 at = tow.position().add(d.scale(max / dist)); + towed.setPos(at.x, at.y, at.z); + } + } + + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + towId = input.hasUUID("Tow") ? input.getUUID("Tow") : null; + towedId = input.hasUUID("Towed") ? input.getUUID("Towed") : null; + length = Mth.clamp((input.contains("Length") ? input.getInt("Length") : 4), MIN_LENGTH, MAX_LENGTH); + searchTicks = SEARCH_TICKS; + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + if (towId != null && towedId != null) { + output.putUUID("Tow", towId); + output.putUUID("Towed", towedId); + } + output.putInt("Length", length); + } +} From 86419d3a965bb9605f5ebad32c45122ce60701b6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:30 +0800 Subject: [PATCH 1219/1461] Add entity/ClientAccess.java to the 1.20.6 shared sources --- .../mcheli/entity/ClientAccess.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java new file mode 100644 index 00000000..28f14851 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackSoundPacket; +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * The few client facts common code needs, such as who the local player is. The client installs + * the real supplier at startup; a dedicated server keeps the empty default and never loads a + * client class through here. + */ +public final class ClientAccess { + private static Supplier<@Nullable Entity> localPlayer = () -> null; + private static PackReceiver packs = new PackReceiver() { + @Override + public void onPackList(PackListPacket packet) { + } + + @Override + public void onPackData(PackDataPacket packet) { + } + + @Override + public void onPackSound(PackSoundPacket packet) { + } + }; + + private ClientAccess() { + } + + public static void install(Supplier<@Nullable Entity> player, PackReceiver receiver) { + localPlayer = player; + packs = receiver; + } + + public static @Nullable Entity localPlayer() { + return localPlayer.get(); + } + + /** Where the server's content pack packets go. Called on the client's main thread. */ + public static PackReceiver packs() { + return packs; + } + + /** The client half of the content pack sync. */ + public interface PackReceiver { + void onPackList(PackListPacket packet); + + void onPackData(PackDataPacket packet); + + void onPackSound(PackSoundPacket packet); + } +} From 2e7bf0dce5d4080e2cce47a698963571273b9f85 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:31 +0800 Subject: [PATCH 1220/1461] Add entity/ContainerEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/ContainerEntity.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java new file mode 100644 index 00000000..3c4ebc5b --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ContainerEntity.java @@ -0,0 +1,145 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A cargo container, {@code MCH_EntityContainer}: a crate with a large chest's worth of room that + * sits on the ground, floats, can be pushed around, and rides the racks of the vehicles that carry + * {@code container}. Dropped from a rack high enough it comes down under a parachute. + */ +public class ContainerEntity extends Entity { + /** What racks call it in their {@code AddRack} lines. */ + public static final String CARGO_NAME = "container"; + private static final int SLOTS = 54; + /** Punches add up; the original broke it past 40 with 20 per point of damage. */ + private static final int BREAK_DAMAGE = 40; + + private final SimpleContainer items = new SimpleContainer(SLOTS); + private int damageTaken; + + public ContainerEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + } + + @Override + public void tick() { + super.tick(); + if (level().isClientSide() || isPassenger()) { + return; + } + if (damageTaken > 0) { + damageTaken--; + } + Vec3 m = getDeltaMovement(); + double my = m.y; + if (isInWater() || isInLava()) { + // Bobs up to the surface rather than sinking, as the original's boat physics did. + my = (my < 0.0 ? my / 2.0 : my) + (isEyeInFluid(FluidTags.WATER) ? 0.04 : 0.007); + } else { + my -= 0.04; + } + double mx = m.x; + double mz = m.z; + if (onGround()) { + mx *= 0.9; + mz *= 0.9; + } + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand) { + if (player.getItemInHand(hand).getItem() instanceof ChainItem) { + return ChainItem.useOn(this, player); + } + if (player.isSecondaryUseActive()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer serverPlayer) { + serverPlayer.openMenu(new SimpleMenuProvider((id, inventory, p) -> ChestMenu.sixRows(id, inventory, items), + Component.translatable("entity.mcheli.container"))); + } + return InteractionResult.SUCCESS; + } + + @Override + public boolean hurt(DamageSource source, float damage) { + if (!(level() instanceof ServerLevel level)) { + return false; + } + if (isInvulnerableTo(source) || isRemoved() || !(source.getEntity() instanceof Player player)) { + // Only players take it apart; explosions and stray rounds leave the cargo alone. + return false; + } + ModSounds.playAt(this, "hit", 1.0F, 1.3F); + damageTaken += (int) (damage * 20.0F); + boolean creative = player.getAbilities().instabuild; + if (creative || damageTaken > BREAK_DAMAGE) { + if (!creative) { + spawnAtLocation(ModItems.CONTAINER.get()); + } + Containers.dropContents(level, this, items); + discard(); + } + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean isPushable() { + return !isPassenger(); + } + + @Override + public boolean canBeCollidedWith() { + return !isPassenger(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.CONTAINER.get()); + } + @Override + protected void readAdditionalSaveData(CompoundTag input) { + items.clearContent(); + ContainerHelper.loadAllItems(input, items.getItems(), registryAccess()); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + ContainerHelper.saveAllItems(output, items.getItems(), false, registryAccess()); + } +} From 62c320f63991cc7a20df8a20de4525722901b02e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:31 +0800 Subject: [PATCH 1221/1461] Add entity/EngineSoundSource.java to the 1.20.6 shared sources --- .../mcheli/entity/EngineSoundSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java new file mode 100644 index 00000000..6b25a388 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.entity; + +/** + * A vehicle with an engine loop. The client sound player polls these values every tick; the + * definition's {@code SoundVolume} and {@code SoundPitch} are applied on top. + */ +public interface EngineSoundSource { + /** Loudness before the definition's multiplier. Zero silences the loop. */ + float engineVolume(); + + /** Pitch before the definition's multiplier. */ + float enginePitch(); +} From 09baaa101ab59c0a0d35feffb0b48e463ae4607d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:32 +0800 Subject: [PATCH 1222/1461] Add entity/GltdEntity.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/entity/GltdEntity.java | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java new file mode 100644 index 00000000..0f83cacd --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/GltdEntity.java @@ -0,0 +1,206 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.weapon.CasStrike; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The ground laser target designator, {@code MCH_EntityGLTD}: a tripod a player sits behind. Its + * sight looks up to 70 degrees either side of where it was set up, zooms and has night vision, + * and marking a spot calls in an A-10 for three gun runs over it. + */ +public class GltdEntity extends Entity { + /** How far the sight turns from the tripod's heading, either way, and up or down. */ + public static final float VIEW_LIMIT = 70.0F; + /** The CAS weapon the A-10s fire; the original hard coded it too. */ + private static final String WEAPON = "a10gau8"; + /** Added to the weapon's own {@code Delay} between two calls. */ + private static final int EXTRA_WAIT = 150; + private static final double SIGHT_RANGE = 300.0; + + private static final EntityDataAccessor DATA_WAIT = SynchedEntityData.defineId(GltdEntity.class, EntityDataSerializers.INT); + + private final CasStrike strike = new CasStrike(WEAPON); + private boolean nightVision; + + public GltdEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WAIT, 0); + } + + /** Ticks until the next A-10 can be called; the sight shows it. */ + public int waitTicks() { + return entityData.get(DATA_WAIT); + } + + /** The eye of the sight, a little in front of and above the tripod. */ + public Vec3 sightPosition(float partialTicks) { + float yaw = getViewYRot(partialTicks) * Mth.DEG_TO_RAD; + Vec3 pos = getPosition(partialTicks); + return new Vec3(pos.x - Mth.sin(yaw) * 0.6, pos.y + 0.7, pos.z + Mth.cos(yaw) * 0.6); + } + + @Override + public void tick() { + super.tick(); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.04, 0.0); + if (onGround()) { + m = m.scale(0.5); + } + move(MoverType.SELF, m); + setDeltaMovement(m.x * 0.99, m.y * 0.95, m.z * 0.99); + + if (waitTicks() > 0) { + entityData.set(DATA_WAIT, waitTicks() - 1); + } + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (def != null && level() instanceof ServerLevel server) { + strike.tick(server, def); + } + Entity viewer = getFirstPassenger(); + if (viewer == null) { + nightVision = false; + } else if (nightVision) { + NightVision.topUp(viewer); + } + } + + /** + * {@code useCurrentWeapon}: marks the block {@code player} looks at through the sight and calls + * the A-10 on it. Server only. + * + * @return whether the strike was called; the sight may be waiting, or see nothing in range + */ + public boolean callStrike(ServerPlayer player) { + WeaponDefinition def = MCHeli.content().weapon(WEAPON); + if (getFirstPassenger() != player || waitTicks() > 0 || def == null || !(level() instanceof ServerLevel server)) { + return false; + } + Vec3 from = sightPosition(1.0F); + Vec3 to = from.add(player.getLookAngle().scale(SIGHT_RANGE)); + BlockHitResult hit = server.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + strike.start(hit.getLocation(), player.getYRot(), this, player); + entityData.set(DATA_WAIT, def.delay + EXTRA_WAIT); + ModSounds.playAt(server, getX(), getY(), getZ(), "gltd", 0.5F, 1.0F); + if (def.sound != null) { + ModSounds.playTo(player, def.sound, 1.0F, 1.0F); + } + return true; + } + + public void setNightVision(Player player, boolean on) { + if (getFirstPassenger() != player) { + return; + } + nightVision = on; + if (!on) { + NightVision.end(player); + } + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand) { + if (player.isSecondaryUseActive() || isVehicle()) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + player.setYRot(getYRot()); + player.setXRot(0.0F); + player.startRiding(this); + } + return InteractionResult.SUCCESS; + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (!level().isClientSide() && nightVision) { + NightVision.end(passenger); + nightVision = false; + } + } + + /** The viewer sits behind the tripod. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, -0.5).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + /** The original clamped the viewer's head the same way every tick. */ + @Override + public void onPassengerTurned(Entity passenger) { + float yaw = getYRot() + Mth.clamp(Mth.wrapDegrees(passenger.getYRot() - getYRot()), -VIEW_LIMIT, VIEW_LIMIT); + passenger.setYRot(yaw); + passenger.setYHeadRot(yaw); + passenger.setXRot(Mth.clamp(passenger.getXRot(), -VIEW_LIMIT, VIEW_LIMIT)); + } + + @Override + public boolean hurt(DamageSource source, float damage) { + if (!(level() instanceof ServerLevel level)) { + return false; + } + if (isInvulnerableTo(source) || isRemoved()) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + if (!creative) { + spawnAtLocation(ModItems.GLTD.get()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), "hit", 1.0F, 1.0F); + discard(); + return true; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(ModItems.GLTD.get()); + } + @Override + protected void readAdditionalSaveData(CompoundTag input) { + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + } +} From 50721db3d06cdcfe9654729e6ae05dc10826abdf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:32 +0800 Subject: [PATCH 1223/1461] Add entity/GroundVehicleEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/GroundVehicleEntity.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java new file mode 100644 index 00000000..30290141 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java @@ -0,0 +1,130 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Static guns and launchers, ported from {@code MCH_EntityVehicle}. + * + *

The mount itself does not turn; the gunner looks around and the {@code AddPart} tree turns + * with the view. {@code CanMove} lets W/S push the mount and A/D turn it, for the few that set it. + */ +public class GroundVehicleEntity extends AircraftEntity implements EngineSoundSource { + private static final double MIN_SPEED = 0.07; + private double currentSpeed = MIN_SPEED; + + public GroundVehicleEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable VehicleDefinition lookup(String name) { + return MCHeli.content().vehicle(name); + } + + @Override + public @Nullable VehicleDefinition definition() { + return (VehicleDefinition) super.definition(); + } + + /** + * {@code MCH_EntityVehicle.useCurrentWeapon}: the mount stays put while its {@code AddPart} + * tree turns with the gunner's view, so a weapon fires where the gunner looks. Only weapons + * with both yaw limits set aim like turrets instead. The original checked the first + * {@code AddWeapon} line of that weapon, and so does this. + */ + @Override + public boolean aimsWithUser(WeaponSet set) { + WeaponMount mount = set.mounts().getFirst(); + return mount.minYaw() == 0.0F || mount.maxYaw() == 0.0F; + } + + @Override + protected void simulate() { + VehicleDefinition def = definition(); + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + + if (getControllingPassenger() == null) { + throttle = Math.max(throttle - 0.00125, 0.0); + } else if (def.canMove) { + double x = 0.0; + double z = 0.0; + double yaw = Math.toRadians(getYRot()); + if (input.throttleUp) { + x += Math.sin(yaw); + z += Math.cos(yaw); + } + if (input.throttleDown) { + x -= Math.sin(yaw); + z -= Math.cos(yaw); + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + double d = Math.sqrt(x * x + z * z); + if (d > 1.0E-6) { + mx -= x / d * 0.03; + mz += z / d * 0.03; + } + } + if (isInWater()) { + setXRot(getXRot() * 0.9F); + } + + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public float engineVolume() { + return visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return visibleThrottle() * 0.5F; + } +} From 7c5243e78fbd33f4ae2d7fdd860c6ddffce8fca3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:33 +0800 Subject: [PATCH 1224/1461] Add entity/HelicopterEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/HelicopterEntity.java | 552 ++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java new file mode 100644 index 00000000..96758a2c --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java @@ -0,0 +1,552 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A helicopter, ported from {@code MCH_EntityHeli}. + * + *

The flight model is the original one: throttle produces lift that weakens as the aircraft + * tilts, and pitching the nose down converts throttle into forward speed through a cubic curve. + * Hover mode holds full throttle, levels the aircraft and lets W/A/S/D slide it around instead. + * + *

Two deliberate differences from 1.7.10: + *

    + *
  • The original ran roll damping once per rendered frame, so it depended on frame rate. It + * now runs once per tick with constants tuned to match the feel at 60 FPS.
  • + *
  • Blades fold and unfold one after another at the same pace on both sides, from the + * synced fold state, instead of each client stepping its own copy.
  • + *
+ */ +public class HelicopterEntity extends AircraftEntity implements EngineSoundSource { + /** + * Rotation per mouse count. The original used {@code 0.06 * partialTicks} with the partial tick + * low-pass filtered to roughly 0.5. + */ + private static final float LOOK_SCALE = 0.03F; + /** Vanilla turns the view 0.15 degrees per mouse count, so this recovers the raw count. */ + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final float ROLL_DAMPING = 0.06F; + private static final float ROLL_DAMPING_HOVER = 0.1F; + private static final float ROLL_RATE = 1.8F; + private static final double MIN_SPEED = 0.07; + + private static final EntityDataAccessor DATA_FOLD = SynchedEntityData.defineId(HelicopterEntity.class, EntityDataSerializers.BYTE); + // DATA_FOLD values, as the original numbered them. + private static final byte FOLDED = 0; + private static final byte FOLDING = 1; + private static final byte UNFOLDED = 2; + private static final byte UNFOLDING = 3; + /** {@code MCH_Blade}: degrees a blade swings per tick while folding. */ + private static final float FOLD_SPEED = 2.0F; + private static final int FOLD_COOLDOWN = 40; + + private double currentSpeed = MIN_SPEED; + private float rotorAngle; + public float rotorAngleO; + /** Per rotor, per blade: where each blade points while they fold, and whether that rotor is folded. */ + private float[][] blades = new float[0][]; + private float[][] bladesO = new float[0][]; + private boolean[] rotorFolded = new boolean[0]; + private int foldCooldown; + private byte lastFold = -1; + + public HelicopterEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable HelicopterDefinition lookup(String name) { + return MCHeli.content().helicopter(name); + } + + @Override + public @Nullable HelicopterDefinition definition() { + return (HelicopterDefinition) super.definition(); + } + + @Override + public boolean steersWithMouse() { + return true; + } + + @Override + public float engineVolume() { + HelicopterDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return 0.2F + visibleThrottle() * 0.2F; + } + + /** Hovering or slow helicopters drop their gear sooner than ones passing over at speed. */ + @Override + protected float autoLowerGearThrottle() { + HelicopterDefinition def = definition(); + double dx = getX() - xo; + double dy = getY() - yo; + double dz = getZ() - zo; + float slow = def != null ? def.speed / 3.5F : 0.0F; + return dx * dx + dy * dy + dz * dz <= slow ? 0.8F : 0.3F; + } + + public float rotorAngle(float partialTicks) { + float prev = rotorAngleO; + if (rotorAngle - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - rotorAngle < -180.0F) { + prev += 360.0F; + } + return prev + (rotorAngle - prev) * partialTicks; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_FOLD, UNFOLDED); + } + + // Blade folding + + private byte foldState() { + return entityData.get(DATA_FOLD); + } + + private boolean hasFoldingRotors() { + HelicopterDefinition def = definition(); + return def != null && def.foldableBlades && !def.rotors.isEmpty(); + } + + /** {@code isFoldBlades}: folded, or on the way. */ + public boolean isFoldedBlades() { + return hasFoldingRotors() && foldState() == FOLDED; + } + + /** {@code canUseBlades}: the blades are out and none is still swinging into place. */ + public boolean canUseBlades() { + if (foldState() != UNFOLDED) { + return false; + } + for (int r = 0; r < rotorFolded.length; r++) { + if (rotorFolded[r]) { + return false; + } + } + return true; + } + + /** {@code canSwitchFoldBlades}: with the engine stopped and the last switch finished. */ + private boolean canSwitchFoldBlades() { + byte state = foldState(); + return hasFoldingRotors() && visibleThrottle() <= 0.01F && foldCooldown == 0 && (state == UNFOLDED || state == FOLDED); + } + + /** The fold key folds the blades of helicopters that can, and works the hatches otherwise. */ + @Override + public void toggleFold() { + if (canSwitchFoldBlades()) { + entityData.set(DATA_FOLD, foldState() == FOLDED ? UNFOLDING : FOLDING); + foldCooldown = FOLD_COOLDOWN; + return; + } + super.toggleFold(); + } + + /** + * {@code onUpdate_Rotor} with {@code MCH_Rotor.update} and {@code MCH_Blade}: spinning blades + * follow the rotor; folding ones swing one by one onto the tail, each a few degrees further + * than the one before, and swing out again until they meet the spinning rotor's angle. + */ + private void tickBlades(HelicopterDefinition def) { + int rotors = def.rotors.size(); + if (blades.length != rotors) { + blades = new float[rotors][]; + bladesO = new float[rotors][]; + rotorFolded = new boolean[rotors]; + for (int r = 0; r < rotors; r++) { + int count = Math.max(def.rotors.get(r).blades(), 1); + blades[r] = new float[count]; + bladesO[r] = new float[count]; + } + if (foldState() == FOLDED || foldState() == FOLDING) { + forceFold(def); + } + } + byte state = foldState(); + if (state != lastFold) { + lastFold = state; + foldCooldown = FOLD_COOLDOWN; + } else if (foldCooldown > 0) { + foldCooldown--; + } + boolean target = state == FOLDING || state == FOLDED; + boolean settled = true; + for (int r = 0; r < rotors; r++) { + RotorDefinition rotor = def.rotors.get(r); + boolean wanted = target && rotor.foldable(); + boolean done = true; + for (int b = 0; b < blades[r].length; b++) { + float base = b * Math.max(rotor.bladeStep(), 1); + float fold = 5.0F + b * 3.0F; + bladesO[r][b] = blades[r][b]; + if (!rotorFolded[r]) { + if (!wanted) { + blades[r][b] = wrap(rotorAngle + base); + done = false; + } else if (!foldBlade(r, b, fold)) { + done = false; + } + } else if (wanted) { + done = false; + } else if (!unfoldBlade(r, b, base)) { + done = false; + } + } + if (done) { + rotorFolded[r] = wanted; + } + if (rotorFolded[r] != wanted) { + settled = false; + } + } + if (settled && !level().isClientSide()) { + if (state == FOLDING) { + entityData.set(DATA_FOLD, FOLDED); + } else if (state == UNFOLDING) { + entityData.set(DATA_FOLD, UNFOLDED); + } + } + } + + /** {@code MCH_Blade.folding}: towards the nearer side of the tail. */ + private boolean foldBlade(int r, int b, float fold) { + float angle = blades[r][b]; + if (angle > fold && angle < 360.0F - fold) { + blades[r][b] = wrap(angle < 180.0F ? angle - FOLD_SPEED : angle + FOLD_SPEED); + return false; + } + return true; + } + + /** {@code MCH_Blade.unfolding}: back round until the blade meets its place on the turning rotor. */ + private boolean unfoldBlade(int r, int b, float base) { + float target = wrap(rotorAngle + base); + float before = blades[r][b]; + if (target <= 180.0F) { + blades[r][b] = wrap(before + FOLD_SPEED); + if (blades[r][b] >= target && before <= target) { + blades[r][b] = target; + return true; + } + } else { + blades[r][b] = wrap(before - FOLD_SPEED); + if (blades[r][b] <= target && before >= target) { + blades[r][b] = target; + return true; + } + } + return false; + } + + /** {@code forceFold}: a helicopter loaded with its blades folded starts with them folded. */ + private void forceFold(HelicopterDefinition def) { + for (int r = 0; r < blades.length; r++) { + if (!def.rotors.get(r).foldable()) { + continue; + } + for (int b = 0; b < blades[r].length; b++) { + float fold = 5.0F + b * 3.0F; + float angle = wrap(rotorAngle + b * Math.max(def.rotors.get(r).bladeStep(), 1)); + if (angle > fold && angle < 360.0F - fold) { + angle = angle < 180.0F ? fold : 360.0F - fold; + } + blades[r][b] = angle; + bladesO[r][b] = angle; + } + rotorFolded[r] = true; + } + } + + private static float wrap(float degrees) { + float d = degrees % 360.0F; + return d < 0.0F ? d + 360.0F : d; + } + + /** Where blade {@code blade} of rotor {@code rotor} points, for drawing. */ + public float bladeAngle(int rotor, int blade, int step, float partialTicks) { + if (rotor >= blades.length || blade >= blades[rotor].length) { + return rotorAngle(partialTicks) + blade * step; + } + float prev = bladesO[rotor][blade]; + float now = blades[rotor][blade]; + if (now - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - now < -180.0F) { + prev += 360.0F; + } + return prev + (now - prev) * partialTicks; + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + super.addAdditionalSaveData(output); + output.putBoolean("FoldBlade", foldState() == FOLDED || foldState() == FOLDING); + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + super.readAdditionalSaveData(input); + entityData.set(DATA_FOLD, (input.contains("FoldBlade") ? input.getBoolean("FoldBlade") : false) ? FOLDED : UNFOLDED); + } + + /** Pilot toggles hover mode. Only allowed while fairly level and not in gunner mode, as in the original. */ + public void toggleHovering() { + if (isHoveringMode()) { + setHovering(false); + } else if (!isGunnerMode() && isLevelEnough() && canUseBlades()) { + setHovering(true); + } + } + + private boolean isLevelEnough() { + return Math.abs(Mth.wrapDegrees(roll())) < 40.0F && Math.abs(Mth.wrapDegrees(getXRot())) < 40.0F; + } + + @Override + public boolean canSwitchGunnerMode() { + return super.canSwitchGunnerMode() && isLevelEnough() && canUseBlades(); + } + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + HelicopterDefinition def = definition(); + if (def == null) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + + float yaw = 0.0F; + float pitch = 0.0F; + float rollDelta = 0.0F; + if (!isNearGround(2)) { + yaw = clamp(x, 40.0F * def.mobilityYaw) * LOOK_SCALE; + } + // Standing on its skids or wheels a helicopter stays level; the mouse only tilts it in the air. + if (!isHovering() && !onGround()) { + pitch = clamp(y, 40.0F * def.mobilityPitch) * LOOK_SCALE; + rollDelta = clamp(x, 40.0F * def.mobilityRoll) * LOOK_SCALE; + } + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + // Keep yaw continuous so the pilot's own yaw never jumps across the +-180 seam. + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + @Override + protected void simulate() { + HelicopterDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + + updateThrottle(def, piloted); + updateAttitude(def); + updateMotion(def); + } + + /** {@code onUpdate_Control} and the hover/non-hover branches. */ + private void updateThrottle(HelicopterDefinition def, boolean piloted) { + if (piloted) { + Vec3 motion = getDeltaMovement(); + if (!canUseBlades()) { + // Folded blades give no lift; on the ground A/D taxi the helicopter round slowly. + throttle = Math.max(0.0, throttle - 0.02); + if (onGround() && isFoldedBlades() && !isDestroyed()) { + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + } + return; + } + if (!isHovering()) { + if (input.throttleUp) { + throttle = Math.min(1.0, throttle + 0.02 * def.throttleUpDown); + } else if (input.throttleDown) { + throttle = Math.max(0.0, throttle - (1.0 / 70.0) * def.throttleUpDown); + } + motion = strafe(motion, false, input.left, input.right, 0.02 * def.speed); + } else { + throttle = Math.min(1.0, throttle + 1.0 / 30.0); + motion = strafe(motion, input.throttleUp, input.left, input.right, 0.01 * def.speed); + if (input.throttleDown) { + motion = push(motion, getYRot() - 180.0F, 0.01 * def.speed); + } + } + setDeltaMovement(motion); + } else { + throttle = Math.max(0.0, throttle - 0.00125); + if (isHoveringMode() && !level().isClientSide()) { + setHovering(false); + } + } + } + + private Vec3 strafe(Vec3 motion, boolean forward, boolean left, boolean right, double amount) { + if (forward) { + motion = push(motion, getYRot(), amount); + } + if (left && !right) { + motion = push(motion, getYRot() - 90.0F, amount); + } else if (right && !left) { + motion = push(motion, getYRot() + 90.0F, amount); + } + return motion; + } + + /** Adds {@code amount} of horizontal motion towards {@code yaw}, Minecraft style (0 = south). */ + private static Vec3 push(Vec3 motion, float yaw, double amount) { + double rad = Math.toRadians(yaw); + return motion.add(-Math.sin(rad) * amount, 0.0, Math.cos(rad) * amount); + } + + /** Per-tick version of {@code onUpdateAngles}: roll levels out, A/D bank in the air. */ + private void updateAttitude(HelicopterDefinition def) { + float damping = 1.0F - (isHovering() ? ROLL_DAMPING_HOVER : ROLL_DAMPING); + if (Math.abs(roll) > 0.1F && Math.abs(roll) < 65.0F) { + roll *= damping; + } + if (getControllingPassenger() != null && !isNearGround(3)) { + if (input.left && !input.right) { + roll -= ROLL_RATE; + } else if (input.right && !input.left) { + roll += ROLL_RATE; + } + } + if (isHovering() && Math.abs(getXRot()) < 70.0F) { + setXRot(getXRot() * 0.95F); + } + if (def.limitRotation) { + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + /** {@code onUpdate_Server}: gravity, lift, forward thrust, speed limit and ground friction. */ + private void updateMotion(HelicopterDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + float groundPitch = def.onGroundPitch; + + if (!isHovering()) { + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + double yawRad = Math.toRadians(getYRot()); + float pitch = getXRot(); + if (isNearGround(3)) { + pitch -= groundPitch; + } + double thrust = 0.1 * currentSpeed * (pitch * pitch * pitch / 30000.0F) * throttle; + mx += Math.sin(yawRad) * -thrust; + mz += Math.cos(yawRad) * thrust; + + double tilt = (Math.abs(getXRot()) + Math.abs(roll)) * 0.6; + double lift = tilt <= 50.0 ? 1.0 - tilt / 50.0 : 0.0; + my += (lift * 0.025 + 0.03) * throttle; + } else { + levelOut(groundPitch); + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my *= 0.7; + } + my += 0.007; + } + } + } else { + if (random.nextInt(50) == 0) { + mx += (random.nextDouble() - 0.5) / 30.0; + } + if (random.nextInt(50) == 0) { + my += (random.nextDouble() - 0.5) / 50.0; + } + if (random.nextInt(50) == 0) { + mz += (random.nextDouble() - 0.5) / 30.0; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + double limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + levelOut(groundPitch); + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + private void levelOut(float groundPitch) { + if (Math.abs(getXRot()) < 40.0F) { + setXRot((getXRot() - groundPitch) * 0.9F + groundPitch); + } + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + } + + @Override + protected void tickAnimation() { + HelicopterDefinition def = definition(); + rotorAngleO = rotorAngle; + float spool = 1.0F - visibleThrottle(); + rotorAngle = (rotorAngle + (1.0F - spool * spool * spool) * def.rotorSpeed) % 360.0F; + tickBlades(def); + } +} From e8934afe3f215a492f241dce5c4281110e2e591a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:33 +0800 Subject: [PATCH 1225/1461] Add entity/NightVision.java to the 1.20.6 shared sources --- .../mcheli/entity/NightVision.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java new file mode 100644 index 00000000..f52c4227 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/NightVision.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; + +/** + * Night vision through a sight, {@code MCH_Camera.updateViewer}: a short effect kept topped up while + * the viewer has it switched on, and taken away again when they switch it off or leave. + */ +public final class NightVision { + private static final int TICKS = 300; + + private NightVision() { + } + + /** Called every tick for each viewer. It never runs below ten seconds, where vanilla starts to flicker the view. */ + public static void topUp(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current == null || current.getDuration() < TICKS - 40) { + living.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, TICKS, 0, true, false, false)); + } + } + } + + /** Removes ours, leaving a longer one from a potion alone. */ + public static void end(Entity viewer) { + if (viewer instanceof LivingEntity living) { + MobEffectInstance current = living.getEffect(MobEffects.NIGHT_VISION); + if (current != null && current.getDuration() <= TICKS) { + living.removeEffect(MobEffects.NIGHT_VISION); + } + } + } +} From b1174bb46249dd3645934219a90b3da3cf436ad8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:33 +0800 Subject: [PATCH 1226/1461] Add entity/ParachuteEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/ParachuteEntity.java | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java new file mode 100644 index 00000000..084f860b --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/ParachuteEntity.java @@ -0,0 +1,179 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A parachute with someone hanging under it, {@code MCH_EntityParachute}. It sinks at about two + * blocks a second, the rider steers it with the forward key, and it folds away a moment after it + * touches the ground or the water. + */ +public class ParachuteEntity extends Entity { + /** What the parachute looks like and how it opens, the original's model type. */ + public enum Kind { + /** From the parachute item: open from the start. */ + ITEM, + /** An ejection seat: the rider falls in the seat and the canopy opens once it drops. */ + EJECTION_SEAT, + /** Troops dropped from a rack or a cargo door: open from the start, no seat. */ + DROP + } + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(ParachuteEntity.class, EntityDataSerializers.INT); + private static final double MAX_SPEED = 0.35; + private static final double MIN_STEER = 0.07; + + private double steer = MIN_STEER; + private int onGroundCount; + /** Who boards on the next server tick; boarding in the same tick as the spawn can be lost. */ + private @Nullable Entity user; + + public ParachuteEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Opens a parachute above {@code user}, carrying over their speed, and puts them under it. + * Server only. + */ + public static ParachuteEntity open(Level level, Entity user, Kind kind) { + ParachuteEntity parachute = new ParachuteEntity(ModEntities.PARACHUTE.get(), level); + double lift = kind == Kind.ITEM ? 3.5 : 0.0; + parachute.setPos(user.getX(), user.getY() + lift, user.getZ()); + parachute.setYRot(user.getYRot()); + parachute.setDeltaMovement(user.getDeltaMovement()); + parachute.entityData.set(DATA_KIND, kind.ordinal()); + parachute.user = user; + user.resetFallDistance(); + level.addFreshEntity(parachute); + return parachute; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, Kind.ITEM.ordinal()); + } + + public Kind kind() { + Kind[] kinds = Kind.values(); + int i = entityData.get(DATA_KIND); + return i >= 0 && i < kinds.length ? kinds[i] : Kind.ITEM; + } + + /** An ejection seat only opens its canopy once it has started to fall. */ + public boolean isOpen() { + return kind() != Kind.EJECTION_SEAT || getDeltaMovement().y < -0.1; + } + + @Override + public void tick() { + super.tick(); + if (level().isClientSide()) { + return; + } + if (user != null) { + if (!user.isPassenger() && user.isAlive()) { + user.startRiding(this, true); + setYRot(user.getYRot()); + } + user = null; + } + Entity rider = getFirstPassenger(); + if (rider == null && tickCount > 20 || isInWater()) { + ejectPassengers(); + discard(); + return; + } + if (rider != null) { + rider.resetFallDistance(); + } + simulate(rider); + } + + /** {@code onUpdateServer}. */ + private void simulate(@Nullable Entity rider) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevSpeed = Math.sqrt(mx * mx + mz * mz); + boolean seatLaunch = kind() == Kind.EJECTION_SEAT && tickCount < 20; + my -= onGround() || seatLaunch ? 0.01 : 0.03; + + if (isOpen()) { + if (rider instanceof ServerPlayer player && player.zza > 0.0F) { + double yaw = Math.toRadians(rider.getYRot()); + mx += -Math.sin(yaw) * steer * 0.05; + mz += Math.cos(yaw) * steer * 0.05; + } + double speed = Math.sqrt(mx * mx + mz * mz); + if (speed > MAX_SPEED) { + mx *= MAX_SPEED / speed; + mz *= MAX_SPEED / speed; + speed = MAX_SPEED; + } + // Holding course builds up speed; letting go settles back to a slow drift. + if (speed > prevSpeed) { + steer = Math.min(steer + (MAX_SPEED - steer) / 35.0, MAX_SPEED); + } else { + steer = Math.max(steer - (steer - MIN_STEER) / 35.0, MIN_STEER); + } + } + + if (onGround() && ++onGroundCount > 5) { + ejectPassengers(); + discard(); + return; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + my *= seatLaunch ? 0.95 : 0.9; + double drag = isOpen() ? 0.95 : 0.99; + setDeltaMovement(mx * drag, my, mz * drag); + if (rider != null) { + setYRot(rider.getYRot()); + } + } + + /** + * The server moves the parachute; clients ease towards each position it sends over three + * ticks, as for the vehicles. Without this a slow descent steps from one update to the next. + */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + entityData.set(DATA_KIND, (input.contains("Kind") ? input.getInt("Kind") : Kind.ITEM.ordinal())); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + output.putInt("Kind", kind().ordinal()); + } +} From cae793010c0a74eee4b6e58d95d319b007a6be4c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:34 +0800 Subject: [PATCH 1227/1461] Add entity/PartState.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/entity/PartState.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/PartState.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/PartState.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/PartState.java new file mode 100644 index 00000000..54755d56 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/PartState.java @@ -0,0 +1,122 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; + +/** + * Open/closed state of a group of moving parts (canopy, landing gear, hatches, wings, VTOL nozzles). + * Ported from {@code MCH_Parts}. + * + *

The target state is one bit of a synched integer, so only the server changes it and every + * side sees the same value. The angle runs from 0 to {@link #max} at {@link #speed} degrees per tick + * and is advanced locally on every side, which keeps the animation smooth without sending angles. + */ +public final class PartState { + private final AircraftEntity owner; + private final int bit; + private final float max; + private final float speed; + private final Sounds sounds; + + private float angle; + private float prevAngle; + + PartState(AircraftEntity owner, int bit, float speed, Sounds sounds) { + this.owner = owner; + this.bit = bit; + this.max = 90.0F; + this.speed = speed; + this.sounds = sounds; + } + + /** Target state: {@code true} means open, folded, retracted or tilted, depending on the part. */ + public boolean status() { + return (owner.partBits() & (1 << bit)) != 0; + } + + /** Fully back in the default position. */ + public boolean isOff() { + return !status() && angle <= 0.02F; + } + + /** Fully in the switched position. */ + public boolean isOn() { + return status() && angle >= max - 0.02F; + } + + public float angle() { + return angle; + } + + public float angle(float partialTicks) { + return prevAngle + (angle - prevAngle) * partialTicks; + } + + /** Progress from 0 to 1. */ + public float factor() { + return angle / max; + } + + public float factor(float partialTicks) { + return angle(partialTicks) / max; + } + + /** Changes the target state. Server only; clients follow through synched data. */ + void set(boolean on, boolean playSound) { + if (owner.level().isClientSide() || status() == on) { + return; + } + owner.setPartBit(bit, on); + if (playSound) { + play(sounds.switching); + play(on ? sounds.startOn : sounds.startOff); + } + tick(); + } + + /** Jumps straight to {@code on} without animating, used for parts that start deployed. */ + void force(boolean on) { + if (!owner.level().isClientSide()) { + owner.setPartBit(bit, on); + } + angle = prevAngle = on ? max : 0.0F; + } + + void tick() { + prevAngle = angle; + if (status()) { + if (angle < max) { + angle = Math.min(angle + speed, max); + if (angle >= max) { + play(sounds.endOn); + } + } + } else if (angle > 0.0F) { + angle = Math.max(angle - speed, 0.0F); + if (angle <= 0.0F) { + play(sounds.endOff); + } + } + } + + private void play(Cue cue) { + if (cue != null && !owner.level().isClientSide()) { + ModSounds.playAt(owner, cue.sound(), 1.0F, cue.pitch()); + } + } + + /** One sound from {@code sounds.json} with its pitch. */ + record Cue(String sound, float pitch) { + } + + /** What plays when the part starts and finishes moving each way; {@code null} entries stay silent. */ + record Sounds(Cue startOn, Cue endOn, Cue startOff, Cue endOff, Cue switching) { + private static final Cue CLICK = new Cue("plane_cc", 1.0F); + private static final Cue CLICK_LOW = new Cue("plane_cc", 0.5F); + + static final Sounds HATCH = new Sounds(null, CLICK, null, CLICK, new Cue("plane_cv", 0.5F)); + static final Sounds CANOPY = new Sounds(null, CLICK, null, CLICK, null); + static final Sounds LANDING_GEAR = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.75F)); + static final Sounds NOZZLE = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.5F)); + static final Sounds WING = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, null); + } +} From ea6458a47273f82032bcd69cfe9b3a33652a8aa2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:34 +0800 Subject: [PATCH 1228/1461] Add entity/PlaneEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/PlaneEntity.java | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java new file mode 100644 index 00000000..b80171b8 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java @@ -0,0 +1,516 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A fixed wing aircraft, ported from {@code MCP_EntityPlane}. + * + *

Throttle pushes the plane along its nose, tilted ten degrees up, and the vertical part of + * that push is what keeps it in the air: below full throttle it sinks. VTOL capable planes tilt + * their nozzles (and tilt rotors) towards straight down, which turns the push into lift. + * + *

The original applied its attitude rules once per rendered frame with the partial tick + * filtered to about 0.5. Here they run three times per tick with that value, which is what a + * 60 FPS client did, so the handling is the same without depending on the frame rate. + */ +public class PlaneEntity extends AircraftEntity implements EngineSoundSource { + private static final float LOOK_SCALE = 0.03F; + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private @Nullable PartState nozzle; + private @Nullable PartState wing; + + private double currentSpeed = MIN_SPEED; + private float keyRoll; + private float rotorAngle; + public float rotorAngleO; + private float soundLevel; + + public PlaneEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable PlaneDefinition lookup(String name) { + return MCHeli.content().plane(name); + } + + @Override + public @Nullable PlaneDefinition definition() { + return (PlaneDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + PlaneDefinition def = (PlaneDefinition) definition; + nozzle = def.hasNozzleState() ? addPart(PART_NOZZLE, 1.5F, PartState.Sounds.NOZZLE) : null; + wing = def.wings.isEmpty() ? null : addPart(PART_WING, 2.5F, PartState.Sounds.WING); + if (nozzle != null && def.defaultVtol && !nozzle.status()) { + nozzle.force(true); + } + } + + @Override + public boolean steersWithMouse() { + return true; + } + + public @Nullable PartState nozzle() { + return nozzle; + } + + public @Nullable PartState wing() { + return wing; + } + + public float nozzleAngle() { + return nozzle != null ? nozzle.angle() : 0.0F; + } + + @Override + public int vtolMode() { + if (nozzle == null) { + return 0; + } + if (!nozzle.status()) { + return nozzle.angle() <= 0.005F ? 0 : 1; + } + return nozzle.angle() >= 89.995F ? 2 : 1; + } + + public float rotorAngle(float partialTicks) { + return rotorAngleO + (rotorAngle - rotorAngleO) * partialTicks; + } + + // Pilot actions, server side + + public boolean canSwitchVtol() { + PlaneDefinition def = definition(); + if (def == null || !def.vtol || nozzle == null || modeSwitchCooldown > 0 || vtolMode() == 1) { + return false; + } + return Math.abs(roll) <= 30.0F && !(onGround() && def.defaultVtol); + } + + public void toggleVtol() { + if (canSwitchVtol()) { + modeSwitchCooldown = 20; + setVtol(!nozzle.status()); + } + } + + private void setVtol(boolean on) { + PlaneDefinition def = definition(); + if (nozzle == null || def == null || (def.defaultVtol && onGround() && !on)) { + return; + } + nozzle.set(on, true); + } + + public boolean canFoldWing() { + PlaneDefinition def = definition(); + if (wing == null || def == null || modeSwitchCooldown > 0) { + return false; + } + if (def.variableSweepWing) { + // Sweep back only when fast and well clear of the ground, spread again only when slow. + if (!onGround() && !hasBlockBelow(3, 20, true)) { + if (throttle < 0.7F) { + return false; + } + } else if (throttle > 0.1F) { + return false; + } + } else { + if (!onGround() && !hasBlockBelow(3, 3, true)) { + return false; + } + if (throttle > 0.01F) { + return false; + } + } + return wing.isOff(); + } + + public boolean canUnfoldWing() { + return wing != null && modeSwitchCooldown <= 0 && wing.isOn(); + } + + @Override + public void toggleFold() { + PlaneDefinition def = definition(); + if (def == null) { + return; + } + if (!def.hatches.isEmpty()) { + super.toggleFold(); + } else if (canFoldWing()) { + wing.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldWing()) { + wing.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Folded wings ground the plane; swept wings only below 20% throttle. */ + private boolean canUseWing(PlaneDefinition def) { + if (wing == null) { + return true; + } + if (def.variableSweepWing) { + return throttle >= 0.2 || wing.isOff(); + } + return wing.isOff(); + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void updatePartsServer(AircraftDefinition definition) { + super.updatePartsServer(definition); + PlaneDefinition def = (PlaneDefinition) definition; + // Swept wings spread out again when the plane slows down or gets close to the ground. + if (wing != null && def.variableSweepWing && wing.isOn() && throttle >= 0.2F + && (throttle < 0.5 || hasBlockBelow(1, 10, true))) { + wing.set(false, true); + } + if (nozzle != null && def.defaultVtol && onGround() && vtolMode() == 0) { + nozzle.set(true, true); + } + } + + // Mouse + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + PlaneDefinition def = definition(); + if (def == null || ticksAlive() < 30 || isPassenger() || hasBlockBelow(3, 2, true)) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + boolean vtol = vtolMode() > 0; + float yawFactor = (vtol ? def.vtolYaw : 1.0F) * 0.8F; + float pitchFactor = (vtol ? def.vtolPitch : 1.0F) * 0.8F; + float rollInput = vtolMode() == 0 ? x * 0.5F : x; + + float yaw = clamp(x, 40.0F * def.mobilityYaw) * yawFactor * LOOK_SCALE; + float pitch = clamp(y, 40.0F * def.mobilityPitch) * pitchFactor * LOOK_SCALE; + float rollDelta = clamp(rollInput, 40.0F * def.mobilityRoll) * yawFactor * LOOK_SCALE; + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + // Simulation + + @Override + protected void simulate() { + PlaneDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + if (def.targetDrone && !piloted) { + input.throttleUp = true; + } + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + } + updateMotion(def); + } + + /** + * Gunner mode needs a plane in level flight with throttle up and nothing solid within five + * blocks below, so the autopilot has room to circle. + */ + @Override + public boolean canSwitchGunnerMode() { + if (!super.canSwitchGunnerMode()) { + return false; + } + float r = Math.abs(Mth.wrapDegrees(roll())); + float p = Math.abs(Mth.wrapDegrees(getXRot())); + return r <= 40.0F && p <= 40.0F && visibleThrottle() > 0.6F && !hasBlockBelow(3, 5, false); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlNotHovering}. */ + private void updateThrottle(PlaneDefinition def, boolean piloted) { + reverse *= 0.8F; + boolean canFly = piloted && isCanopyClosed() && canUseWing(def); + if (canFly && isGunnerMode()) { + // The autopilot holds the throttle the pilot left it at. + return; + } + if (canFly || def.targetDrone) { + float upDown = def.throttleUpDown; + boolean turning = input.left != input.right; + boolean up = input.throttleUp; + if (turning && throttle < def.pivotTurnThrottle && !up && !input.throttleDown) { + up = true; + upDown *= 2.0F; + } + if (up) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * def.throttleUpDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}. In gunner mode the autopilot levels out and circles by {@code AutoPilotRot}. */ + private void updateAttitude(PlaneDefinition def, float pt) { + boolean gunner = isGunnerMode(); + if (gunner) { + setXRot(getXRot() * 0.95F); + setYRot(getYRot() + def.autoPilotRot * 0.2F); + if (Math.abs(roll) > 20.0F) { + roll *= 0.95F; + } + } + boolean flying = !hasBlockBelow(3, 3, true); + if (!flying || input.freeLook || gunner || (def.floats && waterDepth() > 0.0)) { + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.6F * yawRate * pt); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.6F * yawRate * pt); + } + } else { + // A/D bank the plane; in VTOL mode the keys do nothing in the air. + float rate = vtolMode() != 0 ? 0.0F : 0.2F; + if (input.left && !input.right) { + keyRoll -= rate * pt; + } else if (input.right && !input.left) { + keyRoll += rate * pt; + } + roll += keyRoll * 0.5F * def.mobilityRoll; + } + keyRoll *= 1.0F - 0.1F * pt; + if (!flying && Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.97F); + } + if (nozzleAngle() > 0.001F) { + setXRot(getXRot() * (1.0F - 0.03F * pt)); + roll *= 1.0F - 0.1F * pt; + } + roll = AircraftRotation.wrap(roll); + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), def.minRotationPitch, def.maxRotationPitch)); + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + private float maxSpeed(PlaneDefinition def) { + float extra = 0.0F; + if (def.variableSweepWing) { + if (wing != null) { + extra = (def.sweepWingSpeed - def.speed) * wing.factor(); + } else if (hatch != null) { + extra = (def.sweepWingSpeed - def.speed) * hatch.factor(); + } + } + return def.speed + extra; + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(PlaneDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + // The gunner mode autopilot holds its altitude. + boolean levelOff = isGunnerMode(); + + if (depth == 0.0) { + if (def.targetDrone) { + levelOff = targetDroneAutopilot(def) || levelOff; + } + if (!levelOff) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else { + my *= 0.8; + } + } else { + setXRot(getXRot() * 0.8F); + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + } + + double push = throttle / 10.0; + float nozzleAngle = nozzleAngle(); + Vec3 v; + if (nozzleAngle > 0.001F) { + setXRot(getXRot() * 0.95F); + v = direction(getYRot(), getXRot() - nozzleAngle); + if (nozzleAngle >= 90.0F) { + v = new Vec3(v.x * 0.8, v.y, v.z * 0.8); + } + } else { + v = direction(getYRot(), getXRot() - 10.0F); + } + if (!levelOff) { + my += v.y * push / (nozzleAngle <= 0.01F ? 2.0 : 8.0); + } + + boolean canMove = def.canMoveOnGround || !isOnSolidGround(); + if (canMove) { + if (def.enableBack && reverse > 0.0F) { + mx -= v.x * reverse; + mz -= v.z * reverse; + } else { + mx += v.x * push; + mz += v.z * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = maxSpeed(def); + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + if (Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.8F); + } + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** Target drones circle at a safe height on their own. Returns whether to hold altitude. */ + private boolean targetDroneAutopilot(PlaneDefinition def) { + if (hasBlockBelow(3, 40, true)) { + if (!hasBlockBelow(3, 5, true)) { + setYRot(getYRot() + def.autoPilotRot * 2.0F); + if (getXRot() > -20.0F) { + setXRot(getXRot() - 0.5F); + } + } + return false; + } + setYRot(getYRot() + def.autoPilotRot); + setXRot(getXRot() * 0.95F); + if (!level().isClientSide() && canRaiseLandingGear() && modeSwitchCooldown <= 0) { + toggleLandingGear(); + } + return true; + } + + /** {@code MCH_Lib.Rot2Vec3}: unit vector along a yaw and pitch, Minecraft conventions. */ + private static Vec3 direction(float yaw, float pitch) { + double y = Math.toRadians(yaw); + double p = Math.toRadians(pitch); + return new Vec3(-Math.sin(y) * Math.cos(p), -Math.sin(p), Math.cos(y) * Math.cos(p)); + } + + // Animation and sound + + @Override + protected void tickAnimation() { + PlaneDefinition def = definition(); + float throttle = visibleThrottle(); + rotorAngleO = rotorAngle; + rotorAngle += throttle * def.rotorSpeed; + if (rotorAngle > 360.0F) { + rotorAngle -= 360.0F; + rotorAngleO -= 360.0F; + } else if (rotorAngle < 0.0F) { + rotorAngle += 360.0F; + rotorAngleO += 360.0F; + } + + float target = throttle; + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + PlaneDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return 0.6F + visibleThrottle() * 0.4F; + } +} From 42febb8a2f52cbbafb134bd557296a9f323a19b0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:35 +0800 Subject: [PATCH 1229/1461] Add entity/RappelEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/RappelEntity.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java new file mode 100644 index 00000000..3023c3ae --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/RappelEntity.java @@ -0,0 +1,96 @@ +package com.jnhprojects.mcheli.entity; + +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Carries someone down a helicopter's rope, {@code MCH_EntityHide}. It stays under its hook while + * the helicopter drifts, slides down at a steady pace and lets its rider off at the ground or at + * the end of the rope. Pulling the ropes in drops whoever is still on them. + */ +public class RappelEntity extends Entity { + private static final EntityDataAccessor DATA_VEHICLE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_ROPE = SynchedEntityData.defineId(RappelEntity.class, EntityDataSerializers.INT); + + public RappelEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_VEHICLE, 0); + builder.define(DATA_ROPE, -1); + } + + public void attach(AircraftEntity vehicle, int rope) { + entityData.set(DATA_VEHICLE, vehicle.getId()); + entityData.set(DATA_ROPE, rope); + } + + private @Nullable AircraftEntity vehicle() { + return level().getEntity(entityData.get(DATA_VEHICLE)) instanceof AircraftEntity a && a.isAlive() ? a : null; + } + + @Override + public void tick() { + super.tick(); + AircraftEntity vehicle = vehicle(); + Entity rider = getFirstPassenger(); + if (rider != null) { + rider.resetFallDistance(); + } + if (level().isClientSide()) { + return; + } + if (vehicle == null || !vehicle.isRepelling() || rider == null) { + arrive(); + return; + } + Vec3 hook = vehicle.ropePosition(entityData.get(DATA_ROPE)); + Vec3 m = getDeltaMovement(); + double fall = (m.y - (onGround() ? 0.01 : 0.03)) * 0.9; + setPos(hook.x, getY(), hook.z); + if (onGround()) { + arrive(); + return; + } + move(MoverType.SELF, new Vec3(0.0, fall, 0.0)); + setDeltaMovement(0.0, fall, 0.0); + if (Math.abs(getY() - hook.y) > Math.abs(vehicle.ropeLength()) + 5.0F) { + arrive(); + } + } + + private void arrive() { + ejectPassengers(); + discard(); + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + return new Vec3(0.0, -0.3, 0.0); + } + + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + @Override + protected void readAdditionalSaveData(CompoundTag input) { + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + } +} From a95503d5e9c7aaa0fe800a5f81206554034ffcb5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:35 +0800 Subject: [PATCH 1230/1461] Add entity/TankEntity.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/entity/TankEntity.java | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java new file mode 100644 index 00000000..e729c7f5 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java @@ -0,0 +1,267 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.physics.GroundImpact; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Tanks and cars, ported from {@code MCH_EntityTank}. + * + *

The drive model is the plane's minus lift: throttle pushes along the hull, A/D turn it, + * S brakes and then reverses when {@code EnableBack} is set, and Space is a hand brake. The hull + * does not follow the mouse. Instead a {@link TerrainFollower} reads the terrain under the + * wheels and the hull eases towards that pitch and roll, while the crew look around freely and + * the turret follows their view. + */ +public class TankEntity extends AircraftEntity implements EngineSoundSource { + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private final TerrainFollower terrain = new TerrainFollower(this); + private double currentSpeed = MIN_SPEED; + /** + * Squared horizontal distance covered by last tick's move. The original read + * {@code posX - prevPosX} here, but vanilla resets {@code xo} before {@link #simulate()} runs, + * which left vehicles with {@code PivotTurnThrottle} unable to steer. + */ + private double lastStepSq; + private float soundLevel; + private float turnSound; + + public TankEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable TankDefinition lookup(String name) { + return MCHeli.content().tank(name); + } + + @Override + public @Nullable TankDefinition definition() { + return (TankDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + TankDefinition def = (TankDefinition) definition; + terrain.create(def.groundWheels, new Vector3(0.0, -0.35, def.weightedCenterZ)); + } + + /** Walls and long drops hurt tanks and cars, scaled by speed and height. */ + @Override + protected boolean takesGroundImpacts() { + return true; + } + + /** Tanks climb and tilt on slopes all the time; only rolling over damages them. */ + @Override + protected float crashTilt() { + return 91.0F; + } + + @Override + protected void simulate() { + TankDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), -90.0F, 90.0F)); + roll = Mth.clamp(roll, -90.0F, 90.0F); + } + } + updateMotion(def, piloted); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlSub}. */ + private void updateThrottle(TankDefinition def, boolean piloted) { + reverse *= 0.8F; + float upDown = def.throttleUpDown; + if (input.brake) { + reverse *= 0.5F; + throttle = Math.max(throttle - 0.02 * upDown, 0.0); + } + if ((piloted && isCanopyClosed()) || def.targetDrone) { + if (input.throttleUp || (def.targetDrone && !piloted)) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * upDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}: ease onto the terrain, steer with A/D. */ + private void updateAttitude(TankDefinition def, float pt) { + setXRot(getXRot() + (terrain.targetPitch - getXRot()) * pt); + roll += (terrain.targetRoll - roll) * pt; + + boolean flying = !hasBlockBelow(3, 3, true); + if (flying && !(def.floats && waterDepth() > 0.0)) { + return; + } + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + float pivot = def.pivotTurnThrottle; + double dist = lastStepSq; + // With PivotTurnThrottle set the vehicle has to be rolling before it can turn, like a car. + if (pivot <= 0.0F || throttle >= pivot || reverse >= pivot / 10.0F || dist > reverse * 0.01) { + float speedScale = pivot <= 0.0F ? 1.0F : (float) Math.sqrt(Math.min(dist, 1.0)); + float direction = !input.throttleUp && input.throttleDown && throttle < pivot + 0.05 ? -1.0F : 1.0F; + float step = 0.6F * yawRate * pt * direction * speedScale; + if (input.left && !input.right) { + setYRot(getYRot() - step); + } else if (input.right && !input.left) { + setYRot(getYRot() + step); + } + } + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(TankDefinition def, boolean piloted) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + + if (depth == 0.0) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double push = throttle / 10.0; + double yawRad = Math.toRadians(getYRot()); + double pitchRad = Math.toRadians(getXRot() - 10.0F); + double vx = -Math.sin(yawRad) * Math.cos(pitchRad); + double vy = -Math.sin(pitchRad); + double vz = Math.cos(yawRad) * Math.cos(pitchRad); + my += vy * push / 8.0; + + if (def.canMoveOnGround || !isOnSolidGround()) { + if (def.enableBack && reverse > 0.0F) { + mx -= vx * reverse; + mz -= vz * reverse; + } else { + mx += vx * push; + mz += vz * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + } + + if (!terrain.isEmpty()) { + Vec3 slide = terrain.move(mx, mz); + mx += slide.x; + mz += slide.z; + if (!piloted) { + // Nobody is easing the hull in per frame, so take the attitude straight away. + setXRot(terrain.targetPitch); + roll = terrain.targetRoll; + } + } + + double startX = getX(); + double startZ = getZ(); + move(MoverType.SELF, new Vec3(mx, my, mz)); + double stepX = getX() - startX; + double stepZ = getZ() - startZ; + lastStepSq = stepX * stepX + stepZ * stepZ; + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** The original's {@code applyOnGroundPitch} is a no-op for tanks; the terrain decides. */ + @Override + protected void applyOnGroundPitch(float factor) { + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void tickAnimation() { + float target = visibleThrottle(); + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + // Turning on the spot still revs the engine. + if (!input.left && !input.right && !input.throttleDown) { + turnSound *= 0.8F; + } else { + turnSound = Math.min(turnSound + 0.1F, 0.75F); + } + target = Math.max(target, turnSound); + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + TankDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return Math.max(0.5F + visibleThrottle() * 0.5F, 0.5F + turnSound * 0.5F); + } +} From a15334577e0a24ecaa71cda8d15999fc9dad8037 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:36 +0800 Subject: [PATCH 1231/1461] Add entity/TerrainFollower.java to the 1.20.6 shared sources --- .../mcheli/entity/TerrainFollower.java | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java new file mode 100644 index 00000000..92e8e59e --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java @@ -0,0 +1,225 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * Works out the pitch and roll of a ground vehicle from where its wheels touch the terrain. + * Ported from {@code MCH_WheelManager}. + * + *

The original spawned an invisible entity per wheel and let each one fall onto the ground + * under its mount point. Here every wheel is a one block collision box moved with vanilla's + * collision code, which gives the same result without extra entities to sync. Each left/right + * pair spans a plane with the centre of gravity; the weighted average of their normals is the + * ground normal the hull leans towards. + */ +final class TerrainFollower { + private static final double STEP_HEIGHT = 1.5; + private static final double MAX_DRIFT = 2.0; + private static final double SNAP_DISTANCE = 8.0; + + private final AircraftEntity vehicle; + private Probe[] probes = new Probe[0]; + private Vector3 centre = Vector3.ZERO; + private double wheelBase = 1.0; + float targetPitch; + float targetRoll; + + TerrainFollower(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + /** + * @param wheels positive X wheel positions, front to back; the mirrored wheel is added for each + * @param centre centre of gravity in hull space + */ + void create(List wheels, Vector3 centre) { + this.centre = centre; + probes = new Probe[wheels.size() * 2]; + double minZ = Double.MAX_VALUE; + double maxZ = -Double.MAX_VALUE; + for (int i = 0; i < probes.length; i++) { + Vector3 w = wheels.get(i / 2); + Vector3 local = new Vector3(i % 2 == 0 ? w.x() : -w.x(), w.y(), w.z()); + Probe probe = new Probe(local, local.z() >= centre.z()); + Vec3 at = vehicle.toWorld(local); + probe.setPos(at.x, at.y + 1.0, at.z); + probes[i] = probe; + minZ = Math.min(minZ, local.z()); + maxZ = Math.max(maxZ, local.z()); + } + wheelBase = Math.max(maxZ - minZ, 1.0E-3); + } + + boolean isEmpty() { + return probes.length == 0; + } + + /** + * Drags the wheels along with the hull, lets them settle on the terrain and derives the target + * attitude from them. + * + * @return extra horizontal motion from sliding down steep slopes + */ + Vec3 move(double mx, double mz) { + for (Probe probe : probes) { + Vec3 target = vehicle.toWorld(probe.local); + if (target.distanceToSqr(probe.x, probe.y, probe.z) > SNAP_DISTANCE * SNAP_DISTANCE) { + // Teleported, or created before the vehicle was placed: start over under the hull. + probe.setPos(target.x, target.y + 1.0, target.z); + } + double dx = target.x - probe.x + mx; + double dy = (target.y - probe.y) * 0.15; + double dz = target.z - probe.z + mz; + probe.move(dx, dy, dz); + probe.move(0.0, -0.1, 0.0); + } + + int pairs = probes.length / 2; + // If neither half touches the ground, the outermost pair of that half carries the hull, + // which makes the vehicle tip over a crest instead of floating level. + int rear = pairs - 1; + for (int i = 0; i < pairs; i++) { + if (!probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + rear = -1; + break; + } + } + if (rear >= 0) { + probes[rear * 2].onGround = true; + probes[rear * 2 + 1].onGround = true; + } + int front = 0; + for (int i = pairs - 1; i >= 0; i--) { + if (probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + front = -1; + break; + } + } + if (front >= 0 && pairs > 0) { + probes[front * 2].onGround = true; + probes[front * 2 + 1].onGround = true; + } + + Vec3 wc = vehicle.toWorld(centre).subtract(vehicle.position()); + double cx = vehicle.getX() + wc.x; + double cy = vehicle.getY() + centre.y(); + double cz = vehicle.getZ() + wc.z; + Vec3 normal = Vec3.ZERO; + for (int i = 0; i < pairs; i++) { + Probe w1 = probes[i * 2]; + Probe w2 = probes[i * 2 + 1]; + Vec3 v1 = new Vec3(w1.x - cx, w1.y - cy, w1.z - cz); + Vec3 v2 = new Vec3(w2.x - cx, w2.y - cy, w2.z - cz); + Vec3 n = (w1.local.z() >= 0.0 ? v2.cross(v1) : v1.cross(v2)).normalize(); + double weight = w1.onGround || w2.onGround ? Math.abs(w1.local.z() / wheelBase) : 0.0; + normal = normal.add(n.scale(weight)); + } + normal = normal.normalize(); + + Vec3 slide = Vec3.ZERO; + if (normal.y > 0.01 && normal.y < 0.7) { + slide = new Vec3(normal.x / 50.0, 0.0, normal.z / 50.0); + } + + // Into the hull's frame, then read pitch and roll off the normal. + double yaw = Math.toRadians(vehicle.getYRot()); + double c = Math.cos(yaw); + double s = Math.sin(yaw); + double lx = normal.x * c + normal.z * s; + double lz = normal.z * c - normal.x * s; + float pitch = (float) (90.0 - Math.toDegrees(Math.atan2(normal.y, lz))); + float roll = (float) -(90.0 - Math.toDegrees(Math.atan2(normal.y, lx))); + if (normal.lengthSqr() < 1.0E-6) { + pitch = vehicle.getXRot(); + roll = vehicle.roll; + } + + var def = vehicle.definition(); + float pitchStep = def != null ? def.onGroundPitchFactor : 0.0F; + float rollStep = def != null ? def.onGroundRollFactor : 0.0F; + targetPitch = Mth.clamp(pitch, vehicle.getXRot() - pitchStep, vehicle.getXRot() + pitchStep); + targetRoll = Mth.clamp(roll, vehicle.roll - rollStep, vehicle.roll + rollStep); + + // Wheels that fell behind (stuck on a ledge) are pulled back under the hull. + for (Probe probe : probes) { + Vec3 v = vehicle.position().add(new Vec3(AircraftRotation.toWorld(vehicle.getYRot(), targetPitch, targetRoll, + probe.local.x(), probe.local.y(), probe.local.z()))); + boolean pulled = false; + double x = probe.x; + double z = probe.z; + if (x > v.x + MAX_DRIFT || x < v.x - MAX_DRIFT) { + x = Mth.clamp(x, v.x - MAX_DRIFT, v.x + MAX_DRIFT); + pulled = true; + } + if (z > v.z + MAX_DRIFT || z < v.z - MAX_DRIFT) { + z = Mth.clamp(z, v.z - MAX_DRIFT, v.z + MAX_DRIFT); + pulled = true; + } + if (pulled) { + probe.setPos(x, v.y + STEP_HEIGHT / 2.0, z); + } + } + return slide; + } + + /** One wheel: a one block box that falls onto the terrain and climbs steps up to 1.5 high. */ + private final class Probe { + final Vector3 local; + /** In front of the centre of gravity. */ + final boolean front; + double x; + double y; + double z; + boolean onGround; + + Probe(Vector3 local, boolean front) { + this.local = local; + this.front = front; + } + + void setPos(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + private AABB box() { + return new AABB(x - 0.5, y, z - 0.5, x + 0.5, y + 1.0, z + 0.5); + } + + void move(double dx, double dy, double dz) { + AABB box = box(); + Vec3 wanted = new Vec3(dx, dy, dz); + Vec3 moved = collide(wanted, box); + boolean blockedSideways = moved.x != dx || moved.z != dz; + boolean wasGrounded = onGround || (moved.y != dy && dy < 0.0); + if (blockedSideways && wasGrounded) { + // Same step-up rule as the original wheel entity: climb, move, settle back down. + Vec3 up = collide(new Vec3(0.0, STEP_HEIGHT, 0.0), box); + AABB raised = box.move(up); + Vec3 across = collide(new Vec3(dx, 0.0, dz), raised); + AABB shifted = raised.move(across); + Vec3 down = collide(new Vec3(0.0, -STEP_HEIGHT, 0.0), shifted); + Vec3 stepped = up.add(across).add(down); + if (stepped.horizontalDistanceSqr() > moved.horizontalDistanceSqr()) { + moved = stepped; + } + } + x += moved.x; + y += moved.y; + z += moved.z; + onGround = dy < 0.0 && moved.y != dy; + } + + private Vec3 collide(Vec3 movement, AABB box) { + return Entity.collideBoundingBox(vehicle, movement, box, vehicle.level(), List.of()); + } + } +} From b2736508d3e08473d6aa236c2a3903f0e991d255 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:36 +0800 Subject: [PATCH 1232/1461] Add entity/UavStationEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/UavStationEntity.java | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java new file mode 100644 index 00000000..52da80bc --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/UavStationEntity.java @@ -0,0 +1,330 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; + +/** + * The UAV station and the portable UAV controller, {@code MCH_EntityUavStation}. A player sits at + * it and puts a UAV in its slot: the drone appears at the station's launch offset and the player + * flies it from there. The portable controller only launches small UAVs and its lid opens and + * closes with Shift and right-click. + * + *

The original kept the player at the station and had the drone read its pilot from there. + * Here the pilot takes the drone's reserved seat for as long as they fly it, hidden and unhurt, + * so the drone uses the same controls, cameras, HUD and weapons as every other vehicle. Leaving + * the drone, or losing it, puts the pilot back beside the station. + */ +public class UavStationEntity extends Entity { + public static final int STATION = 1; + public static final int PORTABLE = 2; + /** Further out than this, measured flat, the link breaks and the drone is lost, as in the original. */ + public static final double RANGE = 123.0; + /** The launch offset goes up to this far from the station along each axis. */ + public static final int MAX_OFFSET = 50; + + private static final EntityDataAccessor DATA_KIND = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_OPEN = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_OFFSET = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BLOCK_POS); + private static final EntityDataAccessor DATA_LINKED = SynchedEntityData.defineId(UavStationEntity.class, EntityDataSerializers.BOOLEAN); + + private final SimpleContainer slot = new SimpleContainer(1); + private @Nullable UUID lastUav; + /** The lid of the portable controller, 0 shut to 1 open. */ + private float cover; + private float previousCover; + + public UavStationEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_KIND, (byte) STATION); + builder.define(DATA_OPEN, true); + builder.define(DATA_OFFSET, new BlockPos(12, 2, 12)); + builder.define(DATA_LINKED, false); + } + + public int kind() { + return entityData.get(DATA_KIND); + } + + public void setKind(int kind) { + entityData.set(DATA_KIND, (byte) (kind == PORTABLE ? PORTABLE : STATION)); + } + + public boolean isOpen() { + return entityData.get(DATA_OPEN); + } + + /** Where a launched drone appears, relative to the station. */ + public BlockPos offset() { + return entityData.get(DATA_OFFSET); + } + + public void setOffset(BlockPos offset) { + entityData.set(DATA_OFFSET, new BlockPos(Mth.clamp(offset.getX(), -MAX_OFFSET, MAX_OFFSET), + Mth.clamp(offset.getY(), -MAX_OFFSET, MAX_OFFSET), Mth.clamp(offset.getZ(), -MAX_OFFSET, MAX_OFFSET))); + } + + /** Whether a drone is flown from here right now; the screen lights up. */ + public boolean isLinked() { + return entityData.get(DATA_LINKED); + } + + public void setLinked(boolean linked) { + entityData.set(DATA_LINKED, linked); + } + + public SimpleContainer slot() { + return slot; + } + + public float cover(float partialTicks) { + return Mth.lerp(partialTicks, previousCover, cover); + } + + /** + * {@code initUavPostion}: the first launch offset is twelve blocks out on the diagonal the + * station faces, two up. + */ + public void initOffset() { + int quarter = (int) (Mth.positiveModulo(getYRot() + 45.0F, 360.0F) / 90.0F) % 4; + int x = quarter == 0 || quarter == 3 ? 12 : -12; + int z = quarter == 0 || quarter == 1 ? 12 : -12; + setOffset(new BlockPos(x, 2, z)); + } + + @Override + public void tick() { + super.tick(); + previousCover = cover; + cover = Mth.clamp(cover + (isOpen() ? 0.1F : -0.1F), 0.0F, 1.0F); + if (level().isClientSide()) { + return; + } + Vec3 m = getDeltaMovement().add(0.0, -0.03, 0.0); + move(MoverType.SELF, new Vec3(0.0, m.y, 0.0)); + setDeltaMovement(0.0, onGround() ? 0.0 : m.y * 0.96, 0.0); + + Entity user = getFirstPassenger(); + if (user instanceof ServerPlayer player && !slot.getItem(0).isEmpty()) { + launch(player, slot.getItem(0)); + } + } + + /** What the slot holds, if it is a vehicle at all. */ + public static @Nullable AircraftDefinition definitionOf(ItemStack stack) { + String name = AircraftItem.vehicleOf(stack); + return name != null ? MCHeli.content().any(name) : null; + } + + /** Whether this station can put up the vehicle in {@code stack}. */ + public boolean accepts(@Nullable AircraftDefinition def) { + return def != null && def.uav && (kind() != PORTABLE || def.smallUav); + } + + /** {@code handleItem}: puts up the drone in the slot and hands it to {@code player}. */ + private void launch(ServerPlayer player, ItemStack stack) { + AircraftDefinition def = definitionOf(stack); + if (!accepts(def) || !(level() instanceof ServerLevel server)) { + return; + } + if (ServerConfig.isDisabled(def.name())) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName()), true); + return; + } + AircraftEntity uav = ModEntities.typeFor(def).create(server); + if (uav == null) { + return; + } + BlockPos o = offset(); + double y = Math.max(getY() + o.getY(), server.getMinBuildHeight() + 2.0); + uav.setType(def); + uav.moveTo(getX() + o.getX(), y, getZ() + o.getZ(), getYRot() - 180.0F, 0.0F); + if (!server.noCollision(uav, uav.getBoundingBox().deflate(0.1))) { + player.displayClientMessage(Component.translatable("message.mcheli.uav.blocked"), true); + slot.setItem(0, ItemStack.EMPTY); + Containers.dropItemStack(server, player.getX(), player.getY(), player.getZ(), stack); + return; + } + slot.setItem(0, ItemStack.EMPTY); + uav.setFuelLevel(def.targetDrone ? uav.maxFuel() : (int) (uav.maxFuel() * 0.05F)); + server.addFreshEntity(uav); + if (!def.targetDrone) { + player.closeContainer(); + control(player, uav); + } + } + + /** {@code controlLastAircraft}: takes the last drone flown from here again. */ + public boolean continueControl(ServerPlayer player) { + AircraftEntity uav = lastUav(); + if (uav == null || getFirstPassenger() != player || !uav.canTakeRemotePilot()) { + return false; + } + player.closeContainer(); + return control(player, uav); + } + + public @Nullable AircraftEntity lastUav() { + if (lastUav == null || !(level() instanceof ServerLevel server)) { + return null; + } + Entity e = server.getEntity(lastUav); + return e instanceof AircraftEntity uav && uav.isAlive() && !uav.isDestroyed() && inRange(uav) ? uav : null; + } + + public boolean inRange(Entity uav) { + double dx = uav.getX() - getX(); + double dz = uav.getZ() - getZ(); + return dx * dx + dz * dz <= RANGE * RANGE; + } + + private boolean control(ServerPlayer player, AircraftEntity uav) { + player.stopRiding(); + if (!uav.takeRemotePilot(player, this)) { + return false; + } + lastUav = uav.getUUID(); + setLinked(true); + player.setYRot(uav.getYRot()); + return true; + } + + /** Where a pilot comes back to when the drone is left or lost: the seat in front of the station. */ + public Vec3 returnPosition() { + float yaw = getYRot() * Mth.DEG_TO_RAD; + return new Vec3(getX() - Mth.sin(yaw) * 0.9, getY(), getZ() + Mth.cos(yaw) * 0.9); + } + + @Override + public InteractionResult interact(Player player, InteractionHand hand) { + if (isVehicle()) { + return InteractionResult.PASS; + } + if (kind() == PORTABLE && player.isSecondaryUseActive()) { + if (!level().isClientSide()) { + entityData.set(DATA_OPEN, !isOpen()); + } + return InteractionResult.SUCCESS; + } + if (player.isSecondaryUseActive() || kind() == PORTABLE && !isOpen()) { + return InteractionResult.PASS; + } + if (player instanceof ServerPlayer server) { + player.setYRot(getYRot() - 180.0F); + if (player.startRiding(this)) { + server.openMenu(new SimpleMenuProvider((id, inventory, p) -> new UavStationMenu(id, inventory, this), + Component.translatable(kind() == PORTABLE ? "item.mcheli.uav_station2" : "item.mcheli.uav_station"))); + } + } + return InteractionResult.SUCCESS; + } + + /** The original sat the player down in front of the console, lower for the portable one. */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + double y = kind() == PORTABLE ? -0.6 : 0.35; + return new Vec3(0.0, y, 0.9).yRot(-getYRot() * Mth.DEG_TO_RAD); + } + + @Override + public boolean hurt(DamageSource source, float damage) { + if (!(level() instanceof ServerLevel level)) { + return false; + } + if (isInvulnerableTo(source) || isRemoved() || damage <= 0.0F) { + return false; + } + boolean creative = source.getEntity() instanceof Player player && player.getAbilities().instabuild; + ejectPassengers(); + AircraftEntity uav = lastUav(); + if (uav != null) { + uav.releaseRemotePilot(); + } + Containers.dropContents(level, blockPosition(), slot); + if (!creative) { + spawnAtLocation(item()); + } + ModSounds.playAt(level, getX(), getY(), getZ(), source.getEntity() instanceof Player ? "hit" : "helidmg", 1.0F, 1.0F); + discard(); + return true; + } + + private Item item() { + return kind() == PORTABLE ? ModItems.UAV_CONTROLLER.get() : ModItems.UAV_STATION.get(); + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith() { + return true; + } + + @Override + public @Nullable ItemStack getPickResult() { + return new ItemStack(item()); + } + @Override + protected void readAdditionalSaveData(CompoundTag input) { + setKind((input.contains("Kind") ? input.getInt("Kind") : STATION)); + entityData.set(DATA_OPEN, (input.contains("Open") ? input.getBoolean("Open") : true)); + setOffset(new BlockPos((input.contains("UavX") ? input.getInt("UavX") : 12), (input.contains("UavY") ? input.getInt("UavY") : 2), (input.contains("UavZ") ? input.getInt("UavZ") : 12))); + lastUav = input.hasUUID("LastUav") ? input.getUUID("LastUav") : null; + slot.setItem(0, ItemStack.parseOptional(registryAccess(), input.getCompound("Item"))); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + output.putInt("Kind", kind()); + output.putBoolean("Open", isOpen()); + BlockPos o = offset(); + output.putInt("UavX", o.getX()); + output.putInt("UavY", o.getY()); + output.putInt("UavZ", o.getZ()); + if (lastUav != null) { + output.putUUID("LastUav", lastUav); + } + if (!slot.getItem(0).isEmpty()) { + output.put("Item", slot.getItem(0).save(registryAccess())); + } + } +} From b1ed706959fe9b3a4ea56d8cbde723bc20d42915 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:37 +0800 Subject: [PATCH 1233/1461] Add entity/VehicleInventory.java to the 1.20.6 shared sources --- .../mcheli/entity/VehicleInventory.java | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java new file mode 100644 index 00000000..5ea8dcd2 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/VehicleInventory.java @@ -0,0 +1,144 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.supply.SupplyRules; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.Containers; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; + +/** + * What a vehicle carries besides its crew, {@code MCH_AircraftInventory} and the container half of + * {@code W_EntityContainer}: the equipment slots of the vehicle screen (three fuel cans and a + * parachute per ejection seat) and the cargo chest of vehicles with an {@code InventorySize}. + * + *

The cargo chest always has room for the largest size, 54 slots, and the screen shows as many + * rows as the definition asks for. Items in rows a content reload took away are kept rather than + * lost, and still drop when the vehicle is destroyed. + */ +public final class VehicleInventory { + public static final int FUEL_SLOTS = 3; + public static final int PARACHUTE_SLOTS = 2; + public static final int EQUIPMENT_SLOTS = FUEL_SLOTS + PARACHUTE_SLOTS; + public static final int CARGO_SLOTS = 54; + /** How far from the vehicle's centre a player outside may keep a screen open, past its width. */ + private static final double REACH = 12.0; + + private final AircraftEntity vehicle; + private final SimpleContainer equipment = new GuardedContainer(EQUIPMENT_SLOTS); + private final SimpleContainer cargo = new GuardedContainer(CARGO_SLOTS); + + VehicleInventory(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + public SimpleContainer equipment() { + return equipment; + } + + public SimpleContainer cargo() { + return cargo; + } + + /** {@code isUseableByPlayer}: the crew, or someone standing next to the vehicle. */ + public boolean stillValid(Player player) { + if (vehicle.isRemoved() || vehicle.definition() == null) { + return false; + } + if (player.getVehicle() == vehicle) { + return true; + } + double reach = REACH + vehicle.definition().bodyWidth / 2.0; + return player.distanceToSqr(vehicle) <= reach * reach; + } + + // Fuel + + /** + * {@code updateFuel}: every half second, while the vehicle stands on something, each fuel can + * in a fuel slot pours up to 100 units into the tank. Server only. + * + * @return whether any fuel went in + */ + boolean pourFuelCans(int ticks) { + if (ticks % SupplyRules.FUEL_SLOT_INTERVAL != 0 || !vehicle.canSupply()) { + return false; + } + boolean poured = false; + for (int slot = 0; slot < FUEL_SLOTS; slot++) { + ItemStack can = equipment.getItem(slot); + if (!(can.getItem() instanceof FuelItem)) { + continue; + } + int amount = SupplyRules.fuelFromCan(vehicle.maxFuel() - vehicle.fuel(), FuelItem.contents(can)); + if (amount > 0) { + vehicle.refuel(amount); + can.setDamageValue(can.getDamageValue() + amount); + poured = true; + } + } + if (poured) { + equipment.setChanged(); + } + return poured; + } + + // Parachutes + + /** Parachute slots the vehicle screen offers: one per ejection seat. */ + public static int parachuteSlots(AircraftDefinition def) { + return Math.min(def.ejectionSeats(), PARACHUTE_SLOTS); + } + + /** + * {@code consumeParachute}: takes one parachute out of the parachute slots for a crew member + * who bails out. + * + * @return false when there is none left + */ + boolean takeParachute() { + for (int slot = FUEL_SLOTS; slot < EQUIPMENT_SLOTS; slot++) { + if (equipment.getItem(slot).getItem() instanceof ParachuteItem) { + equipment.removeItem(slot, 1); + return true; + } + } + return false; + } + + // Saving and dropping + + void save(CompoundTag output) { + output.put("Equipment", ContainerHelper.saveAllItems(new CompoundTag(), equipment.getItems(), false, vehicle.registryAccess())); + output.put("Cargo", ContainerHelper.saveAllItems(new CompoundTag(), cargo.getItems(), false, vehicle.registryAccess())); + } + + void load(CompoundTag input) { + equipment.clearContent(); + cargo.clearContent(); + ContainerHelper.loadAllItems(input.getCompound("Equipment"), equipment.getItems(), vehicle.registryAccess()); + ContainerHelper.loadAllItems(input.getCompound("Cargo"), cargo.getItems(), vehicle.registryAccess()); + } + + /** {@code setDead}: everything aboard falls out where the vehicle was. Server only. */ + void dropAll() { + Containers.dropContents(vehicle.level(), vehicle, equipment); + Containers.dropContents(vehicle.level(), vehicle, cargo); + } + + /** A container that only stays open for players {@link #stillValid(Player)} allows. */ + private final class GuardedContainer extends SimpleContainer { + GuardedContainer(int size) { + super(size); + } + + @Override + public boolean stillValid(Player player) { + return VehicleInventory.this.stillValid(player); + } + } +} From 4d26c86aff808da69f835e984a20d2e4e7661458 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:37 +0800 Subject: [PATCH 1234/1461] Add entity/VehicleParticles.java to the 1.20.6 shared sources --- .../mcheli/entity/VehicleParticles.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java new file mode 100644 index 00000000..e441d2bd --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/VehicleParticles.java @@ -0,0 +1,180 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.ParticleSplash; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; + +/** + * The spray, dust and exhaust a vehicle throws up, all client side: + *

    + *
  • {@code onUpdate_ParticleSandCloud}: a helicopter's downwash raises a ring of dust off the + * ground or water below it; boats and amphibians with {@code EnableSeaSurfaceParticle} raise + * one off the sea while under way.
  • + *
  • {@code onUpdate_ParticleSplash}: each {@code AddParticleSplash} point sprays while it moves + * through water.
  • + *
  • {@code onUpdate_ParticleNozzle}: jet nozzles smoke with the throttle, more when the blast + * hits the ground, as on a Harrier in the hover.
  • + *
  • {@code onUpdate_ParticleLandingGear}: a plane rolling fast kicks up bits of the ground.
  • + *
+ */ +final class VehicleParticles { + private static final int WHITE = 0xFFFFFF; + + private VehicleParticles() { + } + + static void tick(AircraftEntity vehicle, AircraftDefinition def) { + if (vehicle.isDestroyed()) { + return; + } + switch (vehicle) { + case HelicopterEntity heli -> sandCloud(heli, def, false); + case PlaneEntity plane -> { + if (def instanceof PlaneDefinition planeDef) { + nozzles(plane, planeDef); + } + landingGear(plane); + splashes(plane, def); + sandCloud(plane, def, true); + } + case TankEntity tank -> { + splashes(tank, def); + sandCloud(tank, def, true); + } + default -> { + } + } + } + + private static void sandCloud(AircraftEntity vehicle, AircraftDefinition def, boolean seaOnly) { + if (seaOnly && !def.seaSurfaceParticles || def.particlesScale <= 0.01F) { + return; + } + Level level = vehicle.level(); + RandomSource random = vehicle.level().getRandom(); + float scale = def.particlesScale * 3.0F * (seaOnly ? 2.0F : 1.0F); + double throttle = Math.min(vehicle.visibleThrottle() * 2.0, 1.0); + // A boat needs some water under it before it sprays; the original counted water blocks. + int count = seaOnly ? (int) (scale * 7.0F) : 0; + int range = (int) (scale * 10.0F) + 1; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + int bx = Mth.floor(vehicle.getX() + 0.5); + int by = Mth.floor(vehicle.getY() + 0.5); + int bz = Mth.floor(vehicle.getZ() + 0.5); + int y = 0; + double groundY = 0.0; + boolean found = false; + for (; y < range && !found; y++) { + for (int x = -1; x <= 1 && !found; x++) { + for (int z = -1; z <= 1 && !found; z++) { + BlockState state = level.getBlockState(pos.set(bx + x, by - y, bz + z)); + if (state.isAir()) { + continue; + } + if (seaOnly && state.getFluidState().is(FluidTags.WATER)) { + count--; + } + if (count <= 0) { + groundY = vehicle.getY() + 1.0 + scale / 5.0F - y; + found = true; + } + } + } + } + if (!found) { + return; + } + double density = (range - y + 1) / (5.0 * scale) / 2.0; + for (int k = 0; k < (int) (throttle * 6.0 * density); k++) { + float angle = random.nextFloat() * Mth.TWO_PI; + double dx = Mth.cos(angle); + double dz = Mth.sin(angle); + level.addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, scale * 5.0F, (int) (10.0F * scale) + 1), true, + vehicle.getX() + dx * scale * 3.0, groundY + (random.nextDouble() - 0.5) * scale, vehicle.getZ() + dz * scale * 3.0, + scale * dx * 0.3, scale * -0.02, scale * dz * 0.3); + } + } + + private static void splashes(AircraftEntity vehicle, AircraftDefinition def) { + if (def.splashes.isEmpty()) { + return; + } + double mx = vehicle.getX() - vehicle.xo; + double mz = vehicle.getZ() - vehicle.zo; + double dist = Math.min(mx * mx + mz * mz, 1.0); + RandomSource random = vehicle.level().getRandom(); + for (ParticleSplash splash : def.splashes) { + for (int i = 0; i < splash.count(); i++) { + if (dist > 0.03 + random.nextFloat() * 0.1) { + splash(vehicle, splash, -mx * splash.acceleration(), -mz * splash.acceleration(), splash.size() * (0.5 + dist * 0.5)); + } + } + } + } + + private static void splash(AircraftEntity vehicle, ParticleSplash splash, double mx, double mz, double size) { + RandomSource random = vehicle.level().getRandom(); + Vec3 at = vehicle.toWorld(splash.position()).add(random.nextDouble() - 0.5, (random.nextDouble() - 0.5) * 0.5, random.nextDouble() - 0.5); + BlockPos block = BlockPos.containing(at.x + 0.5, at.y, at.z + 0.5); + if (!vehicle.level().getFluidState(block).is(FluidTags.WATER)) { + return; + } + int age = splash.age() + (int) (random.nextFloat() * 0.5F * splash.age()); + vehicle.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, (float) size * (random.nextFloat() * 0.2F + 0.8F), age), true, + at.x, at.y, at.z, mx + (random.nextFloat() - 0.5) * 0.7, splash.motionY(), mz + (random.nextFloat() - 0.5) * 0.7); + } + + private static void nozzles(PlaneEntity plane, PlaneDefinition def) { + float throttle = plane.visibleThrottle(); + if (def.nozzles.isEmpty() || throttle <= 0.1F) { + return; + } + RandomSource random = plane.level().getRandom(); + PartState nozzle = plane.nozzle(); + float tilt = nozzle != null ? nozzle.angle() * Mth.DEG_TO_RAD : 0.0F; + // Out of the back, turned down as the nozzles swing towards the hover. + Vector3f blast = plane.toWorldOffset(new Vector3(0.0, -Mth.sin(tilt), -Mth.cos(tilt))); + for (MovingPart part : def.nozzles) { + if (random.nextFloat() > throttle * 1.5F) { + continue; + } + Vec3 at = plane.toWorld(part.position()).add(blast.x, blast.y, blast.z); + BlockPos ahead = BlockPos.containing(at.x + blast.x * 3.0, at.y + blast.y * 3.0, at.z + blast.z * 3.0); + float spread = plane.level().getBlockState(ahead).isAir() ? 0.7F : 2.0F; + plane.level().addAlwaysVisibleParticle(new SmokeParticleOptions(WHITE, 5.0F * def.particlesScale, 30), true, at.x, at.y, at.z, + blast.x + (random.nextFloat() - 0.5F) * spread, blast.y, blast.z + (random.nextFloat() - 0.5F) * spread); + } + } + + private static void landingGear(PlaneEntity plane) { + double dx = plane.getX() - plane.xo; + double dz = plane.getZ() - plane.zo; + if (!plane.onGround() || dx * dx + dz * dz <= 0.01) { + return; + } + BlockPos below = BlockPos.containing(plane.getX(), plane.getY() - 0.5, plane.getZ()); + BlockState state = plane.level().getBlockState(below); + if (state.getRenderShape() == RenderShape.INVISIBLE) { + return; + } + RandomSource random = plane.level().getRandom(); + float width = plane.getBbWidth(); + plane.level().addParticle(new BlockParticleOption(ParticleTypes.BLOCK, state), + plane.getX() + (random.nextFloat() - 0.5) * width, plane.getBoundingBox().minY + 0.1, plane.getZ() + (random.nextFloat() - 0.5) * width, + -dx * 4.0, 1.5, -dz * 4.0); + } +} From 56794e819f2dcd5fd33c314227221fa6fda8b12a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:37 +0800 Subject: [PATCH 1235/1461] Add entity/projectile/A10Entity.java to the 1.20.6 shared sources --- .../mcheli/entity/projectile/A10Entity.java | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java new file mode 100644 index 00000000..926c6783 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java @@ -0,0 +1,168 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * The A-10 that answers a close air support call, {@code MCH_EntityA10}. It dives in for a second, + * strafes the target with forty GAU-8 rounds and climbs away. + */ +public class A10Entity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_DIRECTION = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.INT); + private static final int LIFETIME = 70; + + private int despawnCount; + private int shotCount; + public int power = 32; + public int explosionPower = 1; + public float acceleration = 4.0F; + private @Nullable Entity shooter; + private @Nullable Entity user; + + public A10Entity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_DIRECTION, 0); + } + + /** + * @param direction 0 heading south (+Z), then clockwise in steps of 90 degrees + */ + public void setup(String weapon, int direction, double mx, double mz, @Nullable Entity shooter, @Nullable Entity user) { + entityData.set(DATA_WEAPON, weapon); + entityData.set(DATA_DIRECTION, direction); + setYRot(90 * direction); + yRotO = getYRot(); + setDeltaMovement(mx, 0.0, mz); + this.shooter = shooter; + this.user = user; + } + + public int direction() { + return entityData.get(DATA_DIRECTION); + } + + /** Hidden during the first second, while it is still out of sight. */ + public boolean isVisible() { + return despawnCount > 20; + } + + @Override + public void tick() { + super.tick(); + despawnCount++; + if (!level().isClientSide()) { + if (despawnCount > LIFETIME) { + discard(); + return; + } + if (shotCount < 40) { + for (int i = 0; i < 2; i++) { + fireGau8(shotCount); + shotCount++; + } + if (shotCount == 38) { + ModSounds.playAt(this, "gau-8_snd", 150.0F, 1.0F); + } + } + } + if (despawnCount <= 20) { + setDeltaMovement(getDeltaMovement().x, -0.3, getDeltaMovement().z); + } else { + setPos(getX() + getDeltaMovement().x, getY() + getDeltaMovement().y, getZ() + getDeltaMovement().z); + setDeltaMovement(getDeltaMovement().add(0.0, 0.02, 0.0)); + } + } + + private void fireGau8(int count) { + int dir = direction(); + double x = getX(); + double z = getZ(); + double tx = random.nextDouble() - 0.5; + double ty = -2.6; + double tz = random.nextDouble() - 0.5; + switch (dir) { + case 0 -> { + tz += 10.0; + z += count * 0.6; + } + case 1 -> { + tx -= 10.0; + x -= count * 0.6; + } + case 2 -> { + tz -= 10.0; + z -= count * 0.6; + } + default -> { + tx += 10.0; + x += count * 0.6; + } + } + BulletEntity round = ModEntities.BULLET.get().create(level()); + if (round == null) { + return; + } + round.launch(x, getY(), z, tx, ty, tz, 90 * dir, 30.0F, acceleration); + round.setWeapon(entityData.get(DATA_WEAPON)); + round.explosionPower = shotCount % 4 == 0 ? explosionPower : 0; + round.power = power; + round.setOwners(shooter, user); + level().addFreshEntity(round); + } + + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 512.0 * 512.0; + } + + @Override + protected void readAdditionalSaveData(CompoundTag tag) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag tag) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } + + /** The yaw for this frame, as 26.x's {@code Entity#getYRot(float)} gives it. */ + public float getYRot(float partialTicks) { + return partialTicks == 1.0F ? getYRot() : Mth.rotLerp(partialTicks, yRotO, getYRot()); + } + + /** The pitch for this frame, as 26.x's {@code Entity#getXRot(float)} gives it. */ + public float getXRot(float partialTicks) { + return partialTicks == 1.0F ? getXRot() : Mth.lerp(partialTicks, xRotO, getXRot()); + } +} From e670aeaa8e6adccfb6a3bff1729f4dfd81954208 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:38 +0800 Subject: [PATCH 1236/1461] Add entity/projectile/AAMissileEntity.java to the 1.20.6 shared sources --- .../entity/projectile/AAMissileEntity.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java new file mode 100644 index 00000000..a9d41520 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** An air to air missile homing on its locked target, {@code MCH_EntityAAMissile}. */ +public class AAMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 3422500.0; + + public AAMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (age() > 4 && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize * 0.5F); + } + if (level().isClientSide() || w == null || isRemoved()) { + return; + } + Entity t = target(); + if (user() == null || t == null || t.isRemoved()) { + discard(); + return; + } + double d = distanceToSqr(t); + if (d > MAX_DISTANCE_SQ) { + discard(); + } else if (age() > w.rigidityTime) { + // The original compared the squared distance with the plain fuse distance. + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + setPos((t.getX() + getX()) / 2.0, (t.getY() + getY()) / 2.0, (t.getZ() + getZ()) / 2.0); + impact(new Impact(position(), t, null, Direction.UP), 1.0F); + } else { + guideTo(t.getX(), t.getY(), t.getZ()); + } + } + } + + @Override + public String defaultModel() { + return "aamissile"; + } +} From ebc82d21a0fc66a4ada5051cf6f93a0af96d300e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:38 +0800 Subject: [PATCH 1237/1461] Add entity/projectile/ASMissileEntity.java to the 1.20.6 shared sources --- .../entity/projectile/ASMissileEntity.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java new file mode 100644 index 00000000..e5f46c96 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java @@ -0,0 +1,91 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * An air to surface missile flying to the block the gunner aimed at, {@code MCH_EntityASMissile}. + * Powered ones climb for half a second first; gliding bombs only steer sideways. + */ +public class ASMissileEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + + public ASMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public float gravity() { + return isBomblet() ? -0.03F : super.gravity(); + } + + @Override + public float gravityInWater() { + return isBomblet() ? -0.03F : super.gravityInWater(); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && !isBomblet()) { + trail(w.trajectoryParticle, 3, 10.0F * w.smokeSize * 0.5F); + } + if (w != null && !level().isClientSide() && !isBomblet() && !isRemoved()) { + BlockPos aim = BlockPos.containing(targetX, targetY, targetZ); + if (!level().getBlockState(aim).isAir()) { + double dist = Math.sqrt(distanceToSqr(targetX, targetY, targetZ)); + if (dist < w.proximityFuseDist) { + if (w.bomblet > 0) { + for (int i = 0; i < w.bomblet; i++) { + sprinkleBomblet(); + } + } else { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + } + discard(); + } else if (gravity() == 0.0F) { + double up = age() < 10 ? 20.0 : 0.0; + aimAlong(targetX - getX(), targetY + up - getY(), targetZ - getZ()); + } else { + double dx = targetX - getX(); + double dy = (targetY - getY()) * 0.3; + double dz = targetZ - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + Vec3 m = getDeltaMovement(); + setDeltaMovement(dx * acceleration / d, m.y, dz * acceleration / d); + } + } + } + faceMotion(); + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + ASMissileEntity child = spawnChild(ModEntities.AS_MISSILE.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x * 0.5 + (random.nextFloat() - 0.5F) * spread, + m.y * 0.5 / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z * 0.5 + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From 3be26d079844ea24d75b5c5146c35cdd52bad3da Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:39 +0800 Subject: [PATCH 1238/1461] Add entity/projectile/ATMissileEntity.java to the 1.20.6 shared sources --- .../entity/projectile/ATMissileEntity.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java new file mode 100644 index 00000000..d7031ecf --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java @@ -0,0 +1,84 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** + * An anti tank missile, {@code MCH_EntityATMissile}. In the default mode it flies straight at the + * locked target; in top attack mode it climbs 150 blocks above the shooter and dives. + */ +public class ATMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 2250000.0; + + /** 0 direct, 1 top attack. */ + public int guidanceType; + + public ATMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && tickCount >= w.trajectoryParticleStartTick) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + if (!level().isClientSide() && !isRemoved()) { + Entity t = target(); + if (user() != null && t != null && !t.isRemoved() && w != null) { + steer(w, t); + } else { + discard(); + } + } + faceMotion(); + } + + private void steer(WeaponDefinition w, Entity t) { + double x = t.getX() - getX(); + double y = t.getY() - getY(); + double z = t.getZ() - getZ(); + double d = x * x + y * y + z * z; + if (d > MAX_DISTANCE_SQ) { + discard(); + return; + } + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + return; + } + int rigidity = w.rigidityTime; + if (age() <= rigidity) { + return; + } + float af = age() < rigidity + w.trajectoryParticleStartTick ? 0.5F : 1.0F; + if (guidanceType == 1) { + Entity shooter = user(); + if (age() <= rigidity + 20) { + guideTo(t.getX(), shooter.getY() + 150.0, t.getZ()); + } else if (age() <= rigidity + 30) { + guideTo(t.getX(), shooter.getY(), t.getZ()); + } else { + if (age() == rigidity + 35) { + power = (int) (power * 1.2F); + if (explosionPower > 0) { + explosionPower++; + } + } + guideTo(t.getX(), t.getY(), t.getZ()); + } + } else { + d = Math.sqrt(d); + setDeltaMovement(x * acceleration / d * af, y * acceleration / d * af, z * acceleration / d * af); + } + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From 20455f5ed65fb815e14ea54e1d4af1bae1ac1b3d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:39 +0800 Subject: [PATCH 1239/1461] Add entity/projectile/BombEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/projectile/BombEntity.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java new file mode 100644 index 00000000..c758d75b --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** A free fall bomb, {@code MCH_EntityBomb}. Cluster bombs split into bomblets. */ +public class BombEntity extends ProjectileEntity { + public BombEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (!level().isClientSide() && w != null && !isRemoved()) { + Vec3 m = getDeltaMovement(); + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + float fuse = w.proximityFuseDist; + if (fuse > 0.1F && age() % 10 == 0) { + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(fuse), this::canHit)) { + if (entity instanceof LivingEntity) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + BombEntity child = spawnChild(ModEntities.BOMB.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + /** A cluster bomb that has not split yet is not drawn when it splits right away. */ + public boolean isVisible() { + WeaponDefinition w = weapon(); + return w == null || isBomblet() || w.bomblet <= 0 || w.bombletSTime > 0; + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From 36686f4392b3b954856e2c6ac9262a4b95e0e18a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:40 +0800 Subject: [PATCH 1240/1461] Add entity/projectile/BulletEntity.java to the 1.20.6 shared sources --- .../entity/projectile/BulletEntity.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java new file mode 100644 index 00000000..b2b418ac --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** A gun round, {@code MCH_EntityBullet}. High explosive rounds may carry a proximity fuse. */ +public class BulletEntity extends ProjectileEntity { + public BulletEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return true; + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (isRemoved() || level().isClientSide() || age() <= 1 || w == null || explosionPower <= 0) { + return; + } + float fuse = w.proximityFuseDist; + if (fuse > 0.1F) { + fuse++; + float range = fuse + Math.abs(w.acceleration); + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(range), this::canHit)) { + if (entity.distanceToSqr(this) < fuse * fuse) { + setPos((entity.getX() + getX()) / 2.0, (entity.getY() + getY()) / 2.0, (entity.getZ() + getZ()) / 2.0); + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + + @Override + public String defaultModel() { + return "bullet"; + } +} From 7de40de9a0abfd93a929c3f7818196ee3ca43424 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:40 +0800 Subject: [PATCH 1241/1461] Add entity/projectile/DispensedItemEntity.java to the 1.20.6 shared sources --- .../projectile/DispensedItemEntity.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java new file mode 100644 index 00000000..2fe1f43a --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java @@ -0,0 +1,121 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseFireBlock; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A canister that uses an item on every block around where it lands, {@code MCH_EntityDispensedItem}: + * water bombers put out fires, crop dusters spread bone meal or seeds, and so on. + */ +public class DispensedItemEntity extends ProjectileEntity { + public DispensedItemEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize); + } + if (!level().isClientSide() && w != null) { + Vec3 m = getDeltaMovement(); + if (acceleration < 1.0E-4) { + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + } + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + } + tickBomblets(); + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + return; + } + WeaponDefinition w = weapon(); + Item item = w != null ? LegacyItems.item(w.dispenseItem, w.dispenseDamage) : null; + if (w != null && item != null && user() instanceof ServerPlayer player && !player.isRemoved()) { + BlockPos center = hit.block() != null ? hit.block() : blockPosition(); + int range = w.dispenseRange - 1; + for (int x = -range; x <= range; x++) { + for (int y = -range; y <= range; y++) { + for (int z = -range; z <= range; z++) { + int dist = x * x + y * y + z * z; + if (dist <= range * range && (dist <= 0.5 * range * range || random.nextInt(2) == 0)) { + useOn(server, center.offset(x, y, z), item); + } + } + } + } + } + discard(); + } + + private void useOn(ServerLevel server, BlockPos pos, Item item) { + if (pos.getY() < server.getMinBuildHeight() || pos.getY() >= server.getMaxBuildHeight() - 1) { + return; + } + BlockState state = server.getBlockState(pos); + if (state.isAir()) { + return; + } + if (item == Items.WATER_BUCKET) { + // A water drop douses fire and quenches lava instead of placing water. + FluidState fluid = server.getFluidState(pos); + if (state.getBlock() instanceof BaseFireBlock) { + server.removeBlock(pos, false); + } else if (fluid.is(FluidTags.LAVA)) { + server.setBlockAndUpdate(pos, fluid.isSource() ? Blocks.OBSIDIAN.defaultBlockState() : Blocks.COBBLESTONE.defaultBlockState()); + } + return; + } + // Used from above with nobody holding it, the way a dispenser block would. + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(pos).add(0.0, 0.5, 0.0), Direction.UP, pos, false); + item.useOn(new UseOnContext(server, null, InteractionHand.MAIN_HAND, new ItemStack(item), hit)); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + DispensedItemEntity child = spawnChild(ModEntities.DISPENSED_ITEM.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From c7f89a742f55c1d5895a64ef37fdbeea969866ea Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:41 +0800 Subject: [PATCH 1242/1461] Add entity/projectile/FlareEntity.java to the 1.20.6 shared sources --- .../mcheli/entity/projectile/FlareEntity.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java new file mode 100644 index 00000000..c8a9905f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java @@ -0,0 +1,159 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A burning flare, or with a fuse a smoke grenade that bursts into a cloud. Port of + * {@code MCH_EntityFlare}. Flares only look the part: decoying happens in the dispenser. + */ +public class FlareEntity extends Entity { + private static final EntityDataAccessor DATA_FUSE = SynchedEntityData.defineId(FlareEntity.class, EntityDataSerializers.INT); + + private double gravity = -0.013; + private double airResistance = 0.992; + private float size = 6.0F; + + public FlareEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_FUSE, 0); + } + + /** + * @param fuse ticks until a smoke grenade bursts, 0 for a flare + * @param slow the long hanging type 4 flare + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float size, int fuse, boolean slow) { + moveTo(x, y, z, random.nextFloat() * 360.0F, random.nextFloat() * 360.0F); + setDeltaMovement(mx, my, mz); + this.size = size; + entityData.set(DATA_FUSE, fuse); + if (slow) { + gravity *= 0.6; + airResistance = 0.995; + } + } + + private int fuse() { + return entityData.get(DATA_FUSE); + } + + /** A smoke grenade rather than a burning flare. */ + public boolean isSmoke() { + return fuse() > 0; + } + + @Override + public void tick() { + if (fuse() > 0 && tickCount >= fuse()) { + // Removal bursts the grenade on the client. + discard(); + return; + } + boolean server = !level().isClientSide(); + if (server && (!level().isLoaded(blockPosition()) || tickCount > 300)) { + discard(); + return; + } + super.tick(); + Vec3 m = getDeltaMovement(); + if (server) { + HitResult hit = level().clip(new ClipContext(position(), position().add(m), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.MISS) { + discard(); + return; + } + } else { + trail(); + } + setPos(getX() + m.x, getY() + m.y, getZ() + m.z); + setDeltaMovement(m.x * airResistance, m.y + gravity, m.z * airResistance); + if (server && (isInWater() || onGround())) { + discard(); + } + } + + private void trail() { + double x = (getX() - xo) / 2.0; + double y = (getY() - yo) / 2.0; + double z = (getZ() - zo) / 2.0; + for (int i = 0; i < 2; i++) { + if (size < 5.0F && random.nextInt(2) == 0) { + continue; + } + level().addAlwaysVisibleParticle(fuse() > 0 ? ParticleTypes.CLOUD : ParticleTypes.LARGE_SMOKE, true, + xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + if (fuse() == 0) { + level().addAlwaysVisibleParticle(ParticleTypes.FLAME, true, xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + } + } + } + + /** A smoke grenade bursting into a cloud that hangs around for ten seconds or more. */ + private void burst() { + if (!level().isClientSide()) { + return; + } + for (int i = 0; i < 20; i++) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + getX() + (random.nextDouble() - 0.5) * 10.0, + getY() + (random.nextDouble() - 0.5) * 10.0, + getZ() + (random.nextDouble() - 0.5) * 10.0, + (random.nextDouble() - 0.5) * 0.045, 0.005, (random.nextDouble() - 0.5) * 0.045); + } + } + + @Override + public void remove(RemovalReason reason) { + if (level().isClientSide() && fuse() > 0) { + burst(); + entityData.set(DATA_FUSE, 0); + } + super.remove(reason); + } + + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public boolean isInvulnerable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(CompoundTag tag) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag tag) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From 485777d7ce8e934ed46b31778735fdb542c5fb26 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:41 +0800 Subject: [PATCH 1243/1461] Add entity/projectile/MarkerRocketEntity.java to the 1.20.6 shared sources --- .../entity/projectile/MarkerRocketEntity.java | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java new file mode 100644 index 00000000..7ccac6f0 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java @@ -0,0 +1,138 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.Vec3; + +/** + * A marking rocket, {@code MCH_EntityMarkerRocket}. Where it lands under open sky it pops red + * smoke, and five seconds later a stick of bombs falls on the mark. + */ +public class MarkerRocketEntity extends ProjectileEntity { + private static final EntityDataAccessor DATA_STATUS = SynchedEntityData.defineId(MarkerRocketEntity.class, EntityDataSerializers.BYTE); + + /** 0 inert, 1 flying, 2 marking. */ + private int countDown; + + public MarkerRocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_STATUS, (byte) 0); + } + + public void setStatus(int status) { + if (!level().isClientSide()) { + entityData.set(DATA_STATUS, (byte) status); + } + } + + public int status() { + return entityData.get(DATA_STATUS); + } + + @Override + public void tick() { + int status = status(); + if (level().isClientSide()) { + if (weapon() == null) { + super.tick(); + } + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && status != 0) { + if (status == 1) { + super.tick(); + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } else { + float gb = random.nextFloat() * 0.3F; + int rgb = (int) ((random.nextFloat() * 0.2F + 0.8F) * 255) << 16 | (int) (gb * 255) << 8 | (int) (gb * 255); + for (int i = 0; i < 5; i++) { + level().addAlwaysVisibleParticle(new DustParticleOptions(Vec3.fromRGB24(rgb).toVector3f(), 4.0F), true, getX(), getY(), getZ(), + (random.nextFloat() - 0.5F) * 0.7F, 0.3F + random.nextFloat() * 0.3F, (random.nextFloat() - 0.5F) * 0.7F); + } + } + } + } else if (status == 0 || isInWater()) { + discard(); + } else if (status == 1) { + super.tick(); + } else if (countDown > 0) { + countDown--; + if (countDown == 40) { + dropBombs(); + } + } else { + discard(); + } + } + + private void dropBombs() { + int count = 6 + random.nextInt(2); + for (int i = 0; i < count; i++) { + BombEntity bomb = ModEntities.BOMB.get().create(level()); + if (bomb == null) { + continue; + } + bomb.launch(getX() + (random.nextFloat() - 0.5F) * 15.0F, 260.0F + random.nextFloat() * 10.0F + i * 30, + getZ() + (random.nextFloat() - 0.5F) * 15.0F, 0.0, -0.5, 0.0, 0.0F, 90.0F, 4.0); + bomb.setWeapon(weaponName()); + bomb.explosionPower = 3 + random.nextInt(2); + bomb.explosionPowerInWater = 0; + bomb.power = 30; + bomb.piercing = 0; + bomb.setOwners(shooter, user); + level().addFreshEntity(bomb); + } + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (level().isClientSide() || hit.entity() != null || hit.block() == null) { + return; + } + BlockPos mark = hit.block().relative(hit.side()); + if (!level().getBlockState(mark).isAir()) { + discard(); + return; + } + level().setBlockAndUpdate(mark, Blocks.FIRE.defaultBlockState()); + int solid = 0; + for (int y = mark.getY() + 1; y < level().getMaxBuildHeight() - 1; y++) { + if (!level().getBlockState(new BlockPos(mark.getX(), y, mark.getZ())).isAir() && ++solid >= 5) { + break; + } + } + if (solid < 5) { + setStatus(2); + setPos(mark.getX() + 0.5, mark.getY() + 1.1, mark.getZ() + 0.5); + xo = getX(); + yo = getY(); + zo = getZ(); + countDown = 100; + } else { + discard(); + } + } + + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From 6f134b55798442cdec64b34f493c6b57adbbdd95 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:42 +0800 Subject: [PATCH 1244/1461] Add entity/projectile/ProjectileEntity.java to the 1.20.6 shared sources --- .../entity/projectile/ProjectileEntity.java | 799 ++++++++++++++++++ 1 file changed, 799 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java new file mode 100644 index 00000000..a572358b --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java @@ -0,0 +1,799 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Combat; +import com.jnhprojects.mcheli.weapon.Explosions; +import com.jnhprojects.mcheli.weapon.ModDamageTypes; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Everything a weapon fires: rounds, rockets, missiles, bombs and torpedoes. A port of + * {@code MCH_EntityBaseBullet}. + * + *

Projectiles fly on both sides, the client only for looks; hits, fuses and guidance are decided + * on the server. They are never saved: like the original, one found in a loaded chunk vanishes. + */ +public abstract class ProjectileEntity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TARGET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BOMBLET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.BOOLEAN); + /** Who fired, for clients: their rounds must not stop on their own vehicle. */ + private static final EntityDataAccessor DATA_SHOOTER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_USER = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + + /** Beyond 5820 blocks from whoever fired it, a projectile is dropped. */ + private static final double MAX_RANGE_SQ = 3.38724E7; + + protected @Nullable Entity shooter; + protected @Nullable Entity user; + protected @Nullable Entity target; + private @Nullable WeaponDefinition weapon; + + public int explosionPower; + public int explosionPowerInWater; + public int power = 1; + public int piercing; + public int delayFuse; + public int sprinkleTime; + public double acceleration = 1.0; + public double accelerationFactor = 1.0; + private int age; + /** Client only: the round hit something here and waits, hidden, for the server to remove it. */ + private boolean spent; + + public Vec3 prevMotion = Vec3.ZERO; + /** Position two ticks ago, used to spread trail particles over the distance covered. */ + protected double xo2; + protected double yo2; + protected double zo2; + + protected ProjectileEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_TARGET, 0); + builder.define(DATA_BOMBLET, false); + builder.define(DATA_SHOOTER, 0); + builder.define(DATA_USER, 0); + } + + /** + * The second constructor of the original: place the projectile and point it along + * {@code (mx, my, mz)} at {@code acceleration} blocks per tick, capped at 3.9. + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float yaw, float pitch, double acceleration) { + moveTo(x, y, z, yaw, pitch); + xo2 = x; + yo2 = y; + zo2 = z; + double a = Math.min(acceleration, 3.9); + double d = Math.sqrt(mx * mx + my * my + mz * mz); + Vec3 motion = d > 0.0 ? new Vec3(mx * a / d, my * a / d, mz * a / d) : Vec3.ZERO; + setDeltaMovement(motion); + prevMotion = motion; + this.acceleration = a; + } + + // Weapon + + public void setWeapon(String name) { + WeaponDefinition def = MCHeli.content().weapon(name); + if (def == null) { + return; + } + weapon = def; + if (!level().isClientSide()) { + entityData.set(DATA_WEAPON, name); + } + if (def.bomblet > 0) { + sprinkleTime = def.bombletSTime; + } + piercing = def.piercing; + if (boostsAcceleration() && def.acceleration > 4.0F) { + accelerationFactor = def.acceleration / 4.0F; + } + } + + /** Rounds and rockets faster than 4 blocks a tick move several steps per tick instead. */ + protected boolean boostsAcceleration() { + return false; + } + + public @Nullable WeaponDefinition weapon() { + return weapon; + } + + /** Model drawn when the weapon names none, one of the original's {@code MCH_DefaultBulletModels}. */ + public abstract String defaultModel(); + + /** The model to draw: {@code ModelBomblet} for bomblets, else {@code ModelBullet}, else the default. */ + public String model() { + if (weapon == null) { + return defaultModel(); + } + String name = isBomblet() ? weapon.bombletModel : weapon.bulletModel; + return name.isEmpty() ? defaultModel() : name; + } + + public String weaponName() { + return entityData.get(DATA_WEAPON); + } + + /** {@code setParameterFromWeapon}: power and charges from the weapon file, and who to credit. */ + public void setParameters(WeaponDefinition def, @Nullable Entity shooter, @Nullable Entity user) { + explosionPower = def.explosion; + explosionPowerInWater = def.explosionInWater; + power = def.power; + piercing = def.piercing; + setOwners(shooter, user); + } + + /** The same, inherited from a parent projectile, for bomblets. */ + public void setParameters(ProjectileEntity parent) { + explosionPower = parent.explosionPower; + explosionPowerInWater = parent.explosionPowerInWater; + power = parent.power; + piercing = parent.piercing; + setOwners(parent.shooter, parent.user); + } + + public void setOwners(@Nullable Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + entityData.set(DATA_SHOOTER, shooter != null ? shooter.getId() : 0); + entityData.set(DATA_USER, user != null ? user.getId() : 0); + } + + public @Nullable Entity user() { + return user; + } + + public @Nullable Entity shooter() { + return shooter; + } + + public boolean isBomblet() { + return entityData.get(DATA_BOMBLET); + } + + public void setBomblet() { + sprinkleTime = 0; + entityData.set(DATA_BOMBLET, true); + } + + public void setTarget(@Nullable Entity entity) { + target = entity; + if (!level().isClientSide()) { + entityData.set(DATA_TARGET, entity != null ? entity.getId() : 0); + } + } + + public @Nullable Entity target() { + return target; + } + + /** A flare seduced this missile: it loses its target and is gone. */ + public void decoy() { + target = null; + discard(); + } + + public int age() { + return age; + } + + public float gravity() { + return weapon != null ? weapon.gravity : 0.0F; + } + + public float gravityInWater() { + return weapon != null ? weapon.gravityInWater : 0.0F; + } + + // Tick + + @Override + public void tick() { + if (spent) { + return; + } + if (level().isClientSide() && age == 0) { + int targetId = entityData.get(DATA_TARGET); + if (targetId > 0) { + target = level().getEntity(targetId); + } + } + Vec3 motion = getDeltaMovement(); + if (!motion.equals(prevMotion)) { + faceMotion(); + } + prevMotion = motion; + age++; + xo2 = xo; + yo2 = yo; + zo2 = zo; + super.tick(); + + if (weapon == null) { + if (age >= 2) { + discard(); + return; + } + setWeapon(weaponName()); + if (weapon == null) { + return; + } + } + + if (!level().isClientSide()) { + if (getY() <= level().getMaxBuildHeight() - 1 && !level().isLoaded(blockPosition())) { + if (weapon.delayFuse <= 0) { + discard(); + return; + } + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + if (delayFuse > 0) { + delayFuse--; + if (delayFuse == 0) { + onTimeout(); + discard(); + return; + } + } + if (!isValid()) { + discard(); + return; + } + if (weapon.timeFuse > 0 && age > weapon.timeFuse) { + onTimeout(); + discard(); + return; + } + if (weapon.explosionAltitude > 0 && isGroundWithin(weapon.explosionAltitude)) { + impact(new Impact(position(), null, null, Direction.DOWN), 1.0F); + } + } + + motion = getDeltaMovement(); + setDeltaMovement(motion.x, motion.y + (isInWater() ? gravityInWater() : gravity()), motion.z); + if (!isRemoved()) { + checkCollisions(); + } + motion = getDeltaMovement().scale(accelerationFactor); + double nx = getX() + motion.x; + double ny = getY() + motion.y; + double nz = getZ() + motion.z; + if (level().isClientSide()) { + splash(nx, ny, nz); + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + level().addParticle(ParticleTypes.BUBBLE, nx - m.x * 0.25, ny - m.y * 0.25, nz - m.z * 0.25, m.x, m.y, m.z); + } + setPos(nx, ny, nz); + } + + /** Turns the nose along the flight path, the way every projectile class did after moving. */ + protected void faceMotion() { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + + /** {@code setMotion}: flies straight along {@code (dx, dy, dz)} at full speed. */ + protected void aimAlong(double dx, double dy, double dz) { + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d > 0.0) { + setDeltaMovement(dx * acceleration / d, dy * acceleration / d, dz * acceleration / d); + } + } + + /** {@code guidanceToTarget}: turns a seventh of the way towards the target each tick. */ + protected void guideTo(double tx, double ty, double tz) { + double dx = tx - getX(); + double dy = ty - getY(); + double dz = tz - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d <= 0.0) { + return; + } + Vec3 m = getDeltaMovement(); + setDeltaMovement((m.x * 6.0 + dx * acceleration / d) / 7.0, + (m.y * 6.0 + dy * acceleration / d) / 7.0, + (m.z * 6.0 + dz * acceleration / d) / 7.0); + faceMotion(); + } + + /** {@code checkValid}: whoever fired it still exists and is not too far away. */ + private boolean isValid() { + if (shooter == null && user == null) { + return false; + } + if (user != null && user.isRemoved()) { + return false; + } + Entity origin = user != null ? user : shooter; + double dx = getX() - origin.getX(); + double dz = getZ() - origin.getZ(); + return dx * dx + dz * dz < MAX_RANGE_SQ; + } + + private boolean isGroundWithin(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(Direction.DOWN); + if (!level().getBlockState(pos).isAir()) { + return true; + } + } + return false; + } + + /** Spawns {@code count} trail particles spread over the last two ticks of flight. */ + protected void trail(String name, int count, float size) { + if (!level().isClientSide() || name.isEmpty() || count < 1 || count > 50) { + return; + } + ParticleOptions particle = trailParticle(name); + double x = (getX() - xo) / count; + double y = (getY() - yo) / count; + double z = (getZ() - zo) / count; + double x2 = (xo - xo2) / count; + double y2 = (yo - yo2) / count; + double z2 = (zo - zo2) / count; + for (int i = 0; i < count; i++) { + double px = (xo + x * i + xo2 + x2 * i) / 2.0; + double py = (yo + y * i + yo2 + y2 * i) / 2.0; + double pz = (zo + z * i + zo2 + z2 * i) / 2.0; + level().addAlwaysVisibleParticle(particle, true, px, py, pz, 0.0, 0.0, 0.0); + } + } + + /** + * The original's own smoke particle ({@code explode}) was a large white puff; the cloud is the + * closest vanilla one. Other names are 1.7.10 particle names. + */ + private static ParticleOptions trailParticle(String name) { + return switch (name) { + case "explode", "cloud" -> ParticleTypes.CLOUD; + case "flame" -> ParticleTypes.FLAME; + case "largesmoke" -> ParticleTypes.LARGE_SMOKE; + case "heart" -> ParticleTypes.HEART; + case "fireworksspark" -> ParticleTypes.FIREWORK; + default -> ParticleTypes.SMOKE; + }; + } + + private void splash(double nx, double ny, double nz) { + if (weapon == null || weapon.power <= 0) { + return; + } + boolean wasDry = !level().getFluidState(BlockPos.containing(xo + 0.5, yo + 0.5, zo + 0.5)).is(FluidTags.WATER); + if (wasDry && level().getFluidState(BlockPos.containing(nx + 0.5, ny + 0.5, nz + 0.5)).is(FluidTags.WATER)) { + float pwr = Math.min(weapon.power, 20); + int n = random.nextInt(1 + (int) pwr / 3) + (int) pwr / 2 + 1; + for (int i = 0; i < n; i++) { + level().addParticle(ParticleTypes.SPLASH, nx, ny + 0.5, nz, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3, + pwr * 0.03 * (random.nextDouble() * 0.5 + 0.5) * 1.8, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3); + } + } + } + + /** {@code onUpdateTimeout}: the fuse ran out in mid air. */ + protected void onTimeout() { + if (!(level() instanceof ServerLevel server) || weapon == null) { + return; + } + if (isInWater()) { + if (explosionPowerInWater > 0) { + explode(server, getX(), getY(), getZ(), explosionPowerInWater, explosionPowerInWater, true); + } + } else if (explosionPower > 0) { + explode(server, getX(), getY(), getZ(), explosionPower, weapon.explosionBlock, false); + } else if (explosionPower < 0) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } + + /** {@code onUpdateBomblet}: once the sprinkle timer runs out the carrier splits up. */ + protected void tickBomblets() { + if (!level().isClientSide() && sprinkleTime > 0 && !isRemoved() && weapon != null) { + sprinkleTime--; + if (sprinkleTime == 0) { + for (int i = 0; i < weapon.bomblet; i++) { + sprinkleBomblet(); + } + discard(); + } + } + } + + protected void sprinkleBomblet() { + } + + /** Creates a copy of this projectile's kind for a bomblet, flying from here. */ + protected @Nullable T spawnChild(EntityType type, double mx, double my, double mz, float yaw, float pitch) { + T child = type.create(level()); + if (child == null) { + return null; + } + child.launch(getX(), getY(), getZ(), mx, my, mz, yaw, pitch, acceleration); + child.setWeapon(weaponName()); + child.setParameters(this); + return child; + } + + // Collision + + /** Where something was hit. {@code entity} is set for entity hits, {@code block} for block hits. */ + public record Impact(Vec3 location, @Nullable Entity entity, @Nullable BlockPos block, Direction side) { + } + + /** {@code onUpdateCollided}: the swept ray of this tick against blocks, then entities. */ + protected void checkCollisions() { + if (weapon == null) { + return; + } + Vec3 motion = getDeltaMovement().scale(accelerationFactor); + Vec3 from = position(); + Vec3 to = from.add(motion); + BlockHitResult blockHit = null; + for (int i = 0; i < 5; i++) { + blockHit = level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (blockHit.getType() != HitResult.Type.BLOCK) { + blockHit = null; + break; + } + BlockState state = level().getBlockState(blockHit.getBlockPos()); + if (user == null || level().isClientSide() || !Combat.isBulletBreakable(state)) { + break; + } + level().destroyBlock(blockHit.getBlockPos(), true, user); + } + + if (weapon.delayFuse > 0) { + if (blockHit != null) { + bounce(blockHit.getDirection()); + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + return; + } + + if (blockHit != null) { + to = blockHit.getLocation(); + } + Impact entityHit = findEntity(from, to, motion); + if (entityHit != null) { + // A vehicle applies the factor of the box struck itself, when it takes the damage. + impact(entityHit, 1.0F); + } else if (blockHit != null) { + impact(new Impact(blockHit.getLocation(), null, blockHit.getBlockPos(), blockHit.getDirection()), 1.0F); + } + } + + private @Nullable Impact findEntity(Vec3 from, Vec3 to, Vec3 motion) { + AABB area = getBoundingBox().expandTowards(motion).inflate(21.0); + Entity best = null; + Vec3 bestAt = null; + double bestDistance = 0.0; + List clipped = new ArrayList<>(); + for (Entity candidate : level().getEntities(this, area, this::canHit)) { + Optional at; + if (candidate instanceof AircraftEntity vehicle) { + at = vehicle.clipHitBoxes(from, to, 0.3); + clipped.add(vehicle); + } else { + at = candidate.getBoundingBox().inflate(0.3).clip(from, to); + } + if (at.isPresent()) { + double d = from.distanceTo(at.get()); + if (best == null || d < bestDistance) { + best = candidate; + bestAt = at.get(); + bestDistance = d; + } + } + } + // Only the vehicle actually hit keeps the factor of the box the ray struck. + for (AircraftEntity vehicle : clipped) { + if (vehicle != best) { + vehicle.consumeHitBoxFactor(); + } + } + return best != null ? new Impact(bestAt, best, null, Direction.UP) : null; + } + + /** Clients only learn who fired through synched data; look them up once it has arrived. */ + private void resolveOwners() { + if (shooter == null && entityData.get(DATA_SHOOTER) > 0) { + shooter = level().getEntity(entityData.get(DATA_SHOOTER)); + } + if (user == null && entityData.get(DATA_USER) > 0) { + user = level().getEntity(entityData.get(DATA_USER)); + } + } + + /** {@code canBeCollidedEntity}: never the shooter, its crew, or rounds fired by the same side. */ + public boolean canHit(Entity entity) { + if (level().isClientSide()) { + resolveOwners(); + } + if (!entity.isPickable() || entity == user || entity == shooter) { + return false; + } + if (entity instanceof ProjectileEntity other) { + if (level().isClientSide()) { + return false; + } + if (other.shooter != null && other.shooter == shooter || other.user != null && other.user == user) { + return false; + } + } + return !(shooter instanceof AircraftEntity vehicle) || !vehicle.hasPassenger(entity); + } + + /** {@code boundBullet}: a delayed fuse round bounces off what it hits. */ + private void bounce(Direction side) { + Vec3 m = getDeltaMovement(); + float bound = weapon != null ? weapon.bound : 0.0F; + switch (side) { + case DOWN -> { + if (m.y > 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case UP -> { + if (m.y < 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case NORTH -> { + if (m.z > 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case SOUTH -> { + if (m.z < 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case WEST -> { + if (m.x > 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + case EAST -> { + if (m.x < 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + } + } + + /** {@code onImpact}: damage what was hit, then explode or pierce through. */ + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + if (weapon != null && (weapon.explosion == 0 || weapon.modeNum >= 2) && hit.block() != null) { + debris(hit); + } + if (piercing <= 0) { + // The server decides what the hit does; until its removal arrives the round stays + // where it struck instead of flying on through the hull. + spent = true; + setPos(hit.location()); + setDeltaMovement(Vec3.ZERO); + } + return; + } + if (weapon == null) { + discard(); + return; + } + if (hit.entity() != null) { + hitEntity(hit.entity(), damageFactor); + piercing = 0; + } + float expPower = explosionPower * damageFactor; + float expPowerInWater = explosionPowerInWater * damageFactor; + Vec3 at = hit.location(); + if (piercing > 0) { + piercing--; + if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, 1.0F, 1.0F, false); + } + return; + } + if (expPowerInWater == 0.0F) { + if (weapon.fae) { + explodeFae(server, expPower); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } else if (hit.entity() != null) { + if (isInWater()) { + explode(server, at.x, at.y, at.z, expPowerInWater, expPowerInWater, true); + } else { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } + } else if (isInWater() || hit.block() != null && isWaterAround(hit.block())) { + BlockPos b = hit.block() != null ? hit.block() : blockPosition(); + explode(server, b.getX(), b.getY(), b.getZ(), expPowerInWater, expPowerInWater, true); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + discard(); + } + + /** {@code MCH_Lib.isBlockInWater}: water on any side of the block. */ + private boolean isWaterAround(BlockPos pos) { + for (Direction d : Direction.values()) { + if (level().getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return level().getFluidState(pos).is(FluidTags.WATER); + } + + /** Client side chips flying off the block a round struck. */ + private void debris(Impact hit) { + if (weapon == null || hit.block() == null) { + return; + } + BlockState state = level().getBlockState(hit.block()); + if (state.isAir()) { + return; + } + float p = weapon.power; + Vec3 m = getDeltaMovement(); + var particle = new BlockParticleOption(ParticleTypes.BLOCK, state); + for (int i = 0; i < p / 3.0F; i++) { + level().addParticle(particle, + hit.location().x + (random.nextFloat() - 0.5) * p / 10.0, + hit.location().y + 0.1, + hit.location().z + (random.nextFloat() - 0.5) * p / 10.0, + -m.x * p / 2.0, p / 2.0F, -m.z * p / 2.0); + } + } + + /** {@code onImpactEntity}. */ + protected void hitEntity(Entity entity, float damageFactor) { + if (!entity.isRemoved()) { + DamageSource source = ModDamageTypes.bullet(level(), this, user); + Combat.hurt(entity, source, power * damageFactor, weapon); + } + confirmHit(); + } + + /** {@code notifyHitBullet}: the shooter hears that a round connected. */ + protected void confirmHit() { + if (user instanceof ServerPlayer player) { + ModSounds.playTo(player, "hit", 1.0F, 1.0F); + } + } + + protected void explode(ServerLevel server, double x, double y, double z, float size, float blockSize, boolean inWater) { + if (weapon == null) { + return; + } + boolean destroyBlocks = !isBomblet() || random.nextInt(3) == 0; + Explosions.Params p = new Explosions.Params(this, user, size, blockSize, weapon.flaming, destroyBlocks, 0, inWater, weapon); + if (Explosions.explode(server, x, y, z, p)) { + confirmHit(); + } + } + + /** Fuel air explosives burn whatever they catch. */ + private void explodeFae(ServerLevel server, float size) { + Explosions.Params p = new Explosions.Params(this, user, size, weapon.explosionBlock, weapon.flaming, true, 15, false, weapon); + if (Explosions.explode(server, getX(), getY(), getZ(), p)) { + confirmHit(); + } + } + + // Entity plumbing + + @Override + public boolean hurt(DamageSource source, float damage) { + // Something thrown at a projectile sets it off, as in the original. + if (damage > 0.0F && source.is(DamageTypes.THROWN) && !isRemoved()) { + impact(new Impact(position().add(0.5, 0.5, 0.5), null, blockPosition(), Direction.UP), 1.0F); + return true; + } + return false; + } + + /** Client only: whether the round already hit something and should no longer be drawn. */ + public boolean isSpent() { + return spent; + } + + @Override + public boolean isPickable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(CompoundTag tag) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag tag) { + tag.putString("WeaponName", weaponName()); + } + + @Override + public boolean shouldBeSaved() { + return false; + } + + /** The yaw for this frame, as 26.x's {@code Entity#getYRot(float)} gives it. */ + public float getYRot(float partialTicks) { + return partialTicks == 1.0F ? getYRot() : Mth.rotLerp(partialTicks, yRotO, getYRot()); + } + + /** The pitch for this frame, as 26.x's {@code Entity#getXRot(float)} gives it. */ + public float getXRot(float partialTicks) { + return partialTicks == 1.0F ? getXRot() : Mth.lerp(partialTicks, xRotO, getXRot()); + } +} From 7c06dcda2dcd0cf9b75efe7485aa0ee27bb981a4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:42 +0800 Subject: [PATCH 1245/1461] Add entity/projectile/RocketEntity.java to the 1.20.6 shared sources --- .../entity/projectile/RocketEntity.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java new file mode 100644 index 00000000..c39fa6fe --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** An unguided rocket, {@code MCH_EntityRocket}, which may burst into bomblets. */ +public class RocketEntity extends ProjectileEntity { + public RocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return !isBomblet(); + } + + @Override + public void tick() { + super.tick(); + tickBomblets(); + WeaponDefinition w = weapon(); + if (!isBomblet() && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + RocketEntity child = spawnChild(ModEntities.ROCKET.get(), m.x, m.y, m.z, getYRot(), getXRot()); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(child.getDeltaMovement().add( + (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread)); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From d121d9bbd898816a930181fe00082cc90560d0df Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:42 +0800 Subject: [PATCH 1246/1461] Add entity/projectile/ThrowableEntity.java to the 1.20.6 shared sources --- .../entity/projectile/ThrowableEntity.java | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java new file mode 100644 index 00000000..1f198726 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/ThrowableEntity.java @@ -0,0 +1,191 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.Explosions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.FastColor; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A thrown grenade, {@code MCH_EntityThrowable}. It flies under its definition's gravity, bounces + * off blocks losing speed, and when its fuse runs out either explodes or, for a smoke grenade, + * pours out coloured smoke until it disappears. + */ +public class ThrowableEntity extends Entity { + private static final EntityDataAccessor DATA_NAME = SynchedEntityData.defineId(ThrowableEntity.class, EntityDataSerializers.STRING); + + private @Nullable Entity thrower; + private @Nullable ThrowableDefinition definition; + private int definitionGeneration = -1; + + public ThrowableEntity(EntityType type, Level level) { + super(type, level); + } + + /** + * Throws {@code def} from {@code thrower}'s eyes the way it looks. + * + * @param strength 0.2 for a flick up to 1.4 for a full draw, the original's acceleration + */ + public static ThrowableEntity thrown(Level level, Entity thrower, ThrowableDefinition def, float strength) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + Vec3 look = thrower.getLookAngle(); + entity.setPos(thrower.getX() + look.x * 0.3, thrower.getEyeY() - 0.1, thrower.getZ() + look.z * 0.3); + entity.setYRot(thrower.getYRot()); + entity.setDeltaMovement(look.scale(1.5 * strength).add(thrower.getDeltaMovement())); + entity.thrower = thrower; + entity.setDefinition(def); + return entity; + } + + /** One fired from a dispenser: no thrower, straight along {@code direction}. */ + public static ThrowableEntity dispensed(Level level, Vec3 at, Vec3 direction, ThrowableDefinition def) { + ThrowableEntity entity = new ThrowableEntity(ModEntities.THROWABLE.get(), level); + entity.setPos(at); + entity.setDeltaMovement(direction.normalize().scale(def.dispenseAcceleration)); + entity.setDefinition(def); + return entity; + } + + private void setDefinition(ThrowableDefinition def) { + entityData.set(DATA_NAME, def.name()); + definition = def; + definitionGeneration = MCHeli.content().generation(); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_NAME, ""); + } + + /** The definition by name, looked up again after a content reload. */ + public @Nullable ThrowableDefinition definition() { + if (definitionGeneration != MCHeli.content().generation()) { + definition = MCHeli.content().throwable(entityData.get(DATA_NAME)); + definitionGeneration = MCHeli.content().generation(); + } + return definition; + } + + public String definitionName() { + return entityData.get(DATA_NAME); + } + + @Override + public void tick() { + super.tick(); + ThrowableDefinition def = definition(); + if (def == null) { + // The original gave a missing definition ten ticks to arrive. + if (tickCount > 10 && !level().isClientSide()) { + discard(); + } + return; + } + if (level().isClientSide()) { + if (def.smokes() && tickCount >= def.timeFuse) { + smoke(def); + } + return; + } + if (tickCount >= def.timeFuse && !def.smokes()) { + Explosions.explode((ServerLevel) level(), getX(), getY(), getZ(), new Explosions.Params(this, thrower, + def.explosion, def.explosion, false, true, 0, isInWater(), null)); + discard(); + return; + } + if (tickCount >= def.aliveTime) { + discard(); + return; + } + fly(def); + } + + /** {@code onUpdate} and {@code boundBullet}: gravity of its own, and a bounce off whatever it hits. */ + private void fly(ThrowableDefinition def) { + boolean water = isInWater(); + Vec3 m = getDeltaMovement().add(0.0, water ? def.gravityInWater : def.gravity, 0.0); + Vec3 from = position(); + move(MoverType.SELF, m); + // Whichever axis came up short hit something. + Vec3 moved = position().subtract(from); + double mx = blocked(moved.x, m.x) ? -m.x * def.bound : m.x; + double my = m.y; + double mz = blocked(moved.z, m.z) ? -m.z * def.bound : m.z; + if (blocked(moved.y, m.y)) { + mx *= 0.9; + mz *= 0.9; + // Off the floor or the ceiling it keeps a share of its speed, unless that would be a crawl. + my = Math.abs(my) > 0.05 ? -my * def.bound : 0.0; + } + double drag = water ? 0.8 : 0.99; + setDeltaMovement(mx * drag, my * drag, mz * drag); + } + + private static boolean blocked(double moved, double wanted) { + return Math.abs(moved) < Math.abs(wanted) - 1.0E-6; + } + + private void smoke(ThrowableDefinition def) { + int color = def.smokeColor; + for (int i = 0; i < def.smokeNum; i++) { + // A little noise per puff, keeping greys grey. + float r = (FastColor.ARGB32.red(color) / 255.0F) * 0.9F + random.nextFloat() * 0.1F; + float g = (FastColor.ARGB32.green(color) / 255.0F) * 0.9F + random.nextFloat() * 0.1F; + float b = (FastColor.ARGB32.blue(color) / 255.0F) * 0.9F + random.nextFloat() * 0.1F; + if (FastColor.ARGB32.red(color) == FastColor.ARGB32.green(color)) { + g = r; + } + if (FastColor.ARGB32.red(color) == FastColor.ARGB32.blue(color)) { + b = r; + } + if (FastColor.ARGB32.green(color) == FastColor.ARGB32.blue(color)) { + b = g; + } + float size = def.smokeSize + random.nextFloat() * def.smokeSize / 3.0F; + level().addAlwaysVisibleParticle(new SmokeParticleOptions(FastColor.ARGB32.colorFromFloat(1.0F, r, g, b) & 0xFFFFFF, size), true, + getX(), getY() + 0.5, getZ(), + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2, + def.smokeVelocityVertical * random.nextFloat() * 0.2, + def.smokeVelocityHorizontal * (random.nextFloat() - 0.5F) * 0.2); + } + } + + /** The server moves it; clients ease towards each position it sends. */ + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 128.0 * 128.0; + } + + @Override + protected void readAdditionalSaveData(CompoundTag input) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag output) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From f77922476b39397491a22bf9d4728f2d2e4c2cf0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:43 +0800 Subject: [PATCH 1247/1461] Add entity/projectile/TorpedoEntity.java to the 1.20.6 shared sources --- .../entity/projectile/TorpedoEntity.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java new file mode 100644 index 00000000..a89f7c01 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java @@ -0,0 +1,56 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A torpedo, {@code MCH_EntityTorpedo}. Once in the water it settles on its running speed and + * either holds course or, when guided, runs to the point the gunner aimed at. + */ +public class TorpedoEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + public double accelerationInWater = 2.0; + + public TorpedoEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + boolean guided = w != null && w.guidedTorpedo; + if (!level().isClientSide() && isInWater()) { + if (acceleration < accelerationInWater) { + acceleration += 0.1; + } else if (acceleration > accelerationInWater + 0.2F) { + acceleration -= 0.1; + } + if (guided) { + aimAlong(targetX - getX(), targetY - getY(), targetZ - getZ()); + } else { + Vec3 m = getDeltaMovement(); + aimAlong(m.x, m.y * 0.8F, m.z); + } + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + if (guided) { + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + } + + @Override + public String defaultModel() { + return "torpedo"; + } +} From 6870c6a33f00eb1a824950f078206d0bdd3f54ae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:43 +0800 Subject: [PATCH 1248/1461] Add entity/projectile/TvMissileEntity.java to the 1.20.6 shared sources --- .../entity/projectile/TvMissileEntity.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java new file mode 100644 index 00000000..a06fef1a --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A wire guided missile flown by the gunner's view, {@code MCH_EntityTvMissile}. It steers while + * it is the vehicle's active TV missile; once another is fired it carries on straight. + */ +public class TvMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 1440000.0; + + public TvMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + Entity aimer = user(); + if (aimer != null) { + if (distanceToSqr(aimer) > MAX_DISTANCE_SQ) { + discard(); + } + if (!level().isClientSide() && !isRemoved()) { + steer(aimer); + } + } else if (!level().isClientSide()) { + discard(); + } + } + + private void steer(Entity aimer) { + if (aimer.isRemoved() || !(shooter() instanceof AircraftEntity vehicle) || vehicle.tvMissile() != this) { + return; + } + Vec3 dir = Vec3.directionFromRotation(aimer.getXRot(), aimer.getYRot()); + aimAlong(dir.x, dir.y, dir.z); + setYRot(aimer.getYRot()); + setXRot(aimer.getXRot()); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From 63afddac2727a339bf9a92bf34b1e4316e3e1fc7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:44 +0800 Subject: [PATCH 1249/1461] Add item/AircraftItem.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/item/AircraftItem.java | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java new file mode 100644 index 00000000..370301b4 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java @@ -0,0 +1,146 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackManifest; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.ChatFormatting; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.component.CustomModelData; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * Places a vehicle, the same way a boat is placed. + * + *

There is one item per vehicle kind ({@code mcheli:helicopter}, {@code mcheli:plane} and so + * on). Which vehicle a stack places is stored in its {@link ModDataComponents#VEHICLE} component, + * and its look comes from the {@code item_model} component pointing at the model the import wrote + * for that vehicle. Items are registered once at startup, so this is what lets definitions be + * added or reloaded while the game runs. + */ +public class AircraftItem extends Item { + private final String kind; + + public AircraftItem(String kind, Properties properties) { + super(properties); + this.kind = kind; + } + + /** The vehicle kind this item holds, such as {@code helicopter}. */ + public String kind() { + return kind; + } + + /** + * A stack that places {@code def}. Vehicles from the import show the item model the import + * wrote for them, picked by the kind model's {@code custom_model_data} overrides (1.20.6 has no + * {@code item_model} component); item models only load with the resources, so pack vehicles keep + * their kind's own model. + */ + public static ItemStack stackOf(AircraftDefinition def) { + ItemStack stack = new ItemStack(ModItems.forKind(def.kind())); + stack.set(ModDataComponents.VEHICLE.get(), def.name()); + if (def.pack().equals(ContentSource.ORIGINAL)) { + MCHeli.legacyContent().iconIndex(def.name()) + .ifPresent(index -> stack.set(DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(index))); + } + return stack; + } + + /** The definition name a stack places, or {@code null} if it is not a vehicle item or has none. */ + public static @Nullable String vehicleOf(ItemStack stack) { + return stack.getItem() instanceof AircraftItem item ? item.definitionName(stack) : null; + } + + protected @Nullable String definitionName(ItemStack stack) { + return stack.get(ModDataComponents.VEHICLE.get()); + } + + @Override + public Component getName(ItemStack stack) { + String name = definitionName(stack); + if (name == null) { + return super.getName(stack); + } + AircraftDefinition def = MCHeli.content().any(name); + // Vehicles added at runtime may bring no lang entry; their DisplayName stands in. + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + /** Who made a vehicle from a content pack. The import's vehicles have their credits in the docs. */ + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, List out, TooltipFlag flag) { + super.appendHoverText(stack, context, out, flag); + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + PackManifest credit = def != null ? def.credit() : null; + if (credit != null) { + out.add((credit.author().isEmpty() + ? Component.translatable("item.mcheli.vehicle.from_pack", credit.name()) + : Component.translatable("item.mcheli.vehicle.from_pack_by", credit.name(), credit.author())) + .withStyle(ChatFormatting.GRAY)); + } + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + HitResult hit = getPlayerPOVHitResult(level, player, ClipContext.Fluid.ANY); + if (!(hit instanceof BlockHitResult blockHit) || hit.getType() != HitResult.Type.BLOCK) { + return InteractionResultHolder.pass(stack); + } + + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + if (!level.isClientSide()) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.unknown", String.valueOf(name)), true); + } + return InteractionResultHolder.fail(stack); + } + if (!level.isClientSide() && ServerConfig.isDisabled(def.name())) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName()), true); + return InteractionResultHolder.fail(stack); + } + + AircraftEntity vehicle = ModEntities.typeFor(def).create(level); + if (vehicle == null) { + return InteractionResultHolder.fail(stack); + } + Vec3 at = blockHit.getLocation(); + vehicle.setType(def); + vehicle.moveTo(at.x, at.y, at.z, player.getYRot(), 0.0F); + if (!level.noCollision(vehicle, vehicle.getBoundingBox())) { + return InteractionResultHolder.fail(stack); + } + + if (!level.isClientSide()) { + level.addFreshEntity(vehicle); + level.gameEvent(player, GameEvent.ENTITY_PLACE, at); + stack.consume(1, player); + } + player.awardStat(Stats.ITEM_USED.get(this)); + return InteractionResultHolder.sidedSuccess(stack, level.isClientSide()); + } +} From b072810dc695394bd7e6f30e59b98e99870e48e3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:44 +0800 Subject: [PATCH 1250/1461] Add item/ChainItem.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/item/ChainItem.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java new file mode 100644 index 00000000..0f89341b --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ChainItem.java @@ -0,0 +1,85 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.ChainEntity; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; + +import java.util.Map; +import java.util.UUID; +import java.util.WeakHashMap; + +/** + * The tow chain, {@code MCH_ItemChain}. Use it on both ends, in either order, two to sixteen + * blocks apart: the vehicle pulls, so one end must be a vehicle. Use it on either end of a chain + * to take the chain off, or on the hooked end again to let go of it. Vehicles and containers + * hand the click over here, and every step is told to the player above the hotbar. + */ +public class ChainItem extends Item { + private static final double MIN_DISTANCE = 2.0; + private static final double MAX_DISTANCE = 16.0; + + /** The first end each player has hooked, until they hook the second. Server only. */ + private static final Map HOOKED = new WeakHashMap<>(); + + public ChainItem(Properties properties) { + super(properties.stacksTo(1)); + } + + /** A click with the chain on {@code target}. */ + public static InteractionResult useOn(Entity target, Player player) { + if (!(player.level() instanceof ServerLevel level)) { + return InteractionResult.SUCCESS; + } + ChainEntity existing = ChainEntity.on(target); + if (existing != null) { + MCHeli.LOGGER.debug("Chain: {} takes the chain off {}", player.getName().getString(), target); + existing.disconnect(); + HOOKED.remove(player); + player.displayClientMessage(Component.translatable("message.mcheli.chain.removed"), true); + return InteractionResult.SUCCESS; + } + UUID firstId = HOOKED.get(player); + Entity first = firstId != null ? level.getEntity(firstId) : null; + // An end hooked far away was most likely forgotten: start over from this one. + if (first == null || first.isRemoved() || target.distanceTo(first) > MAX_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} hooks {}", player.getName().getString(), target); + HOOKED.put(player, target.getUUID()); + ModSounds.playAt(target, "chain", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.hooked", target.getDisplayName()), true); + return InteractionResult.SUCCESS; + } + if (first == target) { + MCHeli.LOGGER.debug("Chain: {} lets go of {}", player.getName().getString(), target); + HOOKED.remove(player); + player.displayClientMessage(Component.translatable("message.mcheli.chain.released"), true); + return InteractionResult.SUCCESS; + } + double distance = target.distanceTo(first); + if (distance < MIN_DISTANCE) { + MCHeli.LOGGER.debug("Chain: {} and {} are only {} blocks apart", first, target, distance); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.too_close", String.format("%.1f", distance)), true); + return InteractionResult.FAIL; + } + if (!ChainEntity.canTow(first) && !ChainEntity.canTow(target)) { + MCHeli.LOGGER.debug("Chain: neither {} nor {} can tow", first, target); + ModSounds.playAt(player, "ng", 1.0F, 1.0F); + player.displayClientMessage(Component.translatable("message.mcheli.chain.no_vehicle"), true); + return InteractionResult.FAIL; + } + MCHeli.LOGGER.debug("Chain: {} chains {} and {}", player.getName().getString(), first, target); + ChainEntity chain = ChainEntity.connect(level, first, target); + HOOKED.remove(player); + Entity towed = chain.towed(); + Entity tow = chain.tow(); + player.displayClientMessage(Component.translatable("message.mcheli.chain.connected", + towed != null ? towed.getDisplayName() : first.getDisplayName(), tow != null ? tow.getDisplayName() : target.getDisplayName()), true); + return InteractionResult.SUCCESS; + } +} From 5524d59b5543054264c2a6b8efed10ced7138f5b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:45 +0800 Subject: [PATCH 1251/1461] Add item/FuelItem.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/item/FuelItem.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java new file mode 100644 index 00000000..71b3ed0a --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/FuelItem.java @@ -0,0 +1,76 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +/** + * The fuel can, {@code MCH_ItemFuel}. It holds 600 units; the durability bar shows how much is + * left. Using it refills it from the coal and charcoal in the inventory, as in the original. + * + *

A can in a fuel slot of the vehicle screen empties into the tank while the vehicle stands on + * something, as in the original. Not in the original: right-clicking a vehicle with the can pours + * up to 100 units at once, which saves opening the screen. + */ +public class FuelItem extends Item { + /** Units a full can holds, the original's maximum damage. */ + public static final int CAPACITY = 600; + /** Units poured into a vehicle per click: what the original moved per fuel slot per update. */ + private static final int POUR = 100; + private static final int PER_COAL = 100; + private static final int PER_CHARCOAL = 75; + + public FuelItem(Properties properties) { + super(properties.durability(CAPACITY)); + } + + public static int contents(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + /** Fills a vehicle's tank from the can. Server only. */ + public static InteractionResult pour(ItemStack stack, AircraftEntity vehicle, Player player) { + int added = vehicle.refuel(player.getAbilities().instabuild ? POUR : Math.min(contents(stack), POUR)); + if (added <= 0) { + return InteractionResult.FAIL; + } + if (!player.getAbilities().instabuild) { + stack.setDamageValue(stack.getDamageValue() + added); + } + vehicle.level().playSound(null, vehicle.getX(), vehicle.getY(), vehicle.getZ(), SoundEvents.BUCKET_EMPTY, SoundSource.NEUTRAL, 1.0F, 1.0F); + return InteractionResult.SUCCESS; + } + + /** Charcoal first, then coal, each one worth part of the can, until it is full. */ + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + if (!stack.isDamaged() || player.getAbilities().instabuild) { + return InteractionResultHolder.pass(player.getItemInHand(hand)); + } + if (!level.isClientSide()) { + refill(stack, player.getInventory(), Items.CHARCOAL, PER_CHARCOAL); + refill(stack, player.getInventory(), Items.COAL, PER_COAL); + } + return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide()); + } + + private static void refill(ItemStack can, Inventory inventory, Item coal, int worth) { + for (int slot = 0; slot < inventory.getContainerSize() && can.isDamaged(); slot++) { + ItemStack item = inventory.getItem(slot); + while (item.is(coal) && !item.isEmpty() && can.isDamaged()) { + can.setDamageValue(Math.max(can.getDamageValue() - worth, 0)); + item.shrink(1); + } + } + } +} From 9b58cb361cbe2a6c2204f40720e220e984b9adeb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:45 +0800 Subject: [PATCH 1252/1461] Add item/LauncherItem.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/item/LauncherItem.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java new file mode 100644 index 00000000..8dce2b68 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/LauncherItem.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.UseAnim; +import net.minecraft.world.level.Level; + +import java.util.function.Supplier; + +/** + * A shoulder launched missile, {@code MCH_ItemLightWeaponBase}: the FIM-92 Stinger and the + * FGM-148 Javelin. Held up it is a sight that locks onto targets; the attack key fires once + * locked. It holds one missile, and its bar shows whether it is loaded: an empty launcher reloads + * from a missile in the inventory after two seconds held but not raised. + */ +public class LauncherItem extends Item { + /** The original's durability: 0 is loaded, the maximum is empty. */ + private static final int EMPTY = 10; + + private final String weapon; + private final Supplier missile; + + /** + * @param weapon the weapon definition it fires, such as {@code fim92} + * @param missile the item it reloads from + */ + public LauncherItem(String weapon, Supplier missile, Properties properties) { + super(properties.durability(EMPTY)); + this.weapon = weapon; + this.missile = missile; + } + + public String weapon() { + return weapon; + } + + public Item missile() { + return missile.get(); + } + + public static boolean isLoaded(ItemStack stack) { + return stack.getDamageValue() < stack.getMaxDamage(); + } + + public static void setLoaded(ItemStack stack, boolean loaded) { + stack.setDamageValue(loaded ? 0 : stack.getMaxDamage()); + } + + /** Not from a vehicle seat or the GLTD, as in the original. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity) && !(player.getVehicle() instanceof GltdEntity); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResultHolder.pass(player.getItemInHand(hand)); + } + player.startUsingItem(hand); + return InteractionResultHolder.consume(player.getItemInHand(hand)); + } + + @Override + public int getUseDuration(ItemStack stack) { + return 72000; + } + + @Override + public UseAnim getUseAnimation(ItemStack stack) { + return UseAnim.BOW; + } +} From bc0295c3e7da7651064136c555b853f584543da2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:46 +0800 Subject: [PATCH 1253/1461] Add item/LegacyAircraftItem.java to the 1.20.6 shared sources --- .../mcheli/item/LegacyAircraftItem.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java new file mode 100644 index 00000000..14ca5640 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * A vehicle item as 1.0.0 saved it: one item per vehicle, such as {@code mcheli:ah-64}. + * + *

These are still registered for the vehicles imported at startup, so that stacks in 1.0.0 + * worlds load instead of vanishing. They work as before, are not listed anywhere, and turn into + * the generic item of their kind ({@link AircraftItem#stackOf}) as soon as they sit in a player's + * inventory. Stacks in chests convert once a player takes them out. + */ +public final class LegacyAircraftItem extends AircraftItem { + private final String definitionName; + + public LegacyAircraftItem(String kind, String definitionName, Properties properties) { + super(kind, properties); + this.definitionName = definitionName; + } + + @Override + protected @Nullable String definitionName(ItemStack stack) { + return definitionName; + } + + @Override + public void inventoryTick(ItemStack stack, Level level, Entity owner, int slot, boolean selected) { + if (level.isClientSide() || !(owner instanceof Player player)) { + return; + } + AircraftDefinition def = MCHeli.content().any(definitionName); + if (def == null) { + return; + } + Inventory inventory = player.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + if (inventory.getItem(i) == stack) { + inventory.setItem(i, convert(stack, def)); + return; + } + } + } + + /** The generic stack for {@code def}, keeping count, custom name and anything else set on the old one. */ + static ItemStack convert(ItemStack legacy, AircraftDefinition def) { + ItemStack converted = AircraftItem.stackOf(def); + converted.applyComponents(legacy.getComponentsPatch()); + converted.set(ModDataComponents.VEHICLE.get(), def.name()); + converted.setCount(legacy.getCount()); + return converted; + } +} From ceb69d0fa7cdab7f859a3ed45e18576549ae94e4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:46 +0800 Subject: [PATCH 1254/1461] Add item/ParachuteItem.java to the 1.20.6 shared sources --- .../mcheli/item/ParachuteItem.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java new file mode 100644 index 00000000..9f19eb4f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ParachuteItem.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +/** The parachute, {@code MCH_ItemParachute}: used in mid-air, it opens above the player. */ +public class ParachuteItem extends Item { + public ParachuteItem(Properties properties) { + super(properties.stacksTo(1)); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + if (player.isPassenger() || player.onGround()) { + return InteractionResultHolder.pass(player.getItemInHand(hand)); + } + if (!level.isClientSide()) { + ParachuteEntity.open(level, player, ParachuteEntity.Kind.ITEM); + player.getItemInHand(hand).consume(1, player); + } + return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide()); + } +} From d455fa0b3c8702163d2ec2bb9dafd57e2f85054a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:46 +0800 Subject: [PATCH 1255/1461] Add item/PlacedEntityItem.java to the 1.20.6 shared sources --- .../mcheli/item/PlacedEntityItem.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java new file mode 100644 index 00000000..3b68bcca --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/PlacedEntityItem.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; + +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Sets up a piece of equipment that lives in the world as an entity: the GLTD, the container, the + * UAV station. Like the originals' items it goes on top of the block clicked, facing the way the + * player looks, and only where it fits. + */ +public class PlacedEntityItem extends Item { + private final Supplier> type; + + public PlacedEntityItem(Supplier> type, Properties properties) { + super(properties); + this.type = type; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + Player player = context.getPlayer(); + BlockPos above = context.getClickedPos().above(); + Entity entity = type.get().create(level); + if (entity == null) { + return InteractionResult.FAIL; + } + entity.moveTo(above.getX() + 0.5, above.getY(), above.getZ() + 0.5, player != null ? player.getYRot() : 0.0F, 0.0F); + prepare(entity, player); + if (!level.noCollision(entity, entity.getBoundingBox().deflate(0.1)) || !level.getEntities(entity, entity.getBoundingBox()).isEmpty()) { + return InteractionResult.FAIL; + } + if (!level.isClientSide()) { + level.addFreshEntity(entity); + level.gameEvent(player, GameEvent.ENTITY_PLACE, entity.position()); + } + ItemStack stack = context.getItemInHand(); + if (player != null) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, player); + return InteractionResult.SUCCESS; + } + + /** Sets up the new entity before it is checked for room and spawned. */ + protected void prepare(Entity entity, @Nullable Player player) { + } +} From aad15588c48020a3f16844f3e40ad606a2a74409 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:47 +0800 Subject: [PATCH 1256/1461] Add item/RangefinderItem.java to the 1.20.6 shared sources --- .../mcheli/item/RangefinderItem.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java new file mode 100644 index 00000000..ffc75bf7 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/RangefinderItem.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.UseAnim; +import net.minecraft.world.level.Level; + +/** + * The laser rangefinder, {@code MCH_ItemRangeFinder}. Held up to the eye it is a scope that reads + * the range to whatever it points at; the attack key spots targets for the team, using one of + * its ten charges. Crafting it with redstone charges it again. + */ +public class RangefinderItem extends Item { + public static final int CHARGES = 10; + + public RangefinderItem(Properties properties) { + super(properties.durability(CHARGES)); + } + + /** Not from a vehicle seat: the vehicle's own sight does that job. */ + public static boolean canUse(Player player) { + return !(player.getVehicle() instanceof AircraftEntity); + } + + /** Charges left, from none to {@link #CHARGES}. */ + public static int charges(ItemStack stack) { + return stack.getMaxDamage() - stack.getDamageValue(); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + if (!canUse(player)) { + return InteractionResultHolder.pass(player.getItemInHand(hand)); + } + player.startUsingItem(hand); + return InteractionResultHolder.consume(player.getItemInHand(hand)); + } + + @Override + public int getUseDuration(ItemStack stack) { + return 72000; + } + + @Override + public UseAnim getUseAnimation(ItemStack stack) { + return UseAnim.SPYGLASS; + } +} From cf39a9d18ee6549fa23bbd25cd34ec6b1a9f2a35 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:47 +0800 Subject: [PATCH 1257/1461] Add item/ThrowableItem.java to the 1.20.6 shared sources --- .../mcheli/item/ThrowableItem.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java new file mode 100644 index 00000000..ace8ccc8 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/ThrowableItem.java @@ -0,0 +1,124 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.throwable.ThrowableDefinition; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.Direction; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.dispenser.BlockSource; +import net.minecraft.core.dispenser.DefaultDispenseItemBehavior; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.UseAnim; +import net.minecraft.world.item.component.CustomModelData; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Hand grenades, {@code MCH_ItemThrowable}. Like the vehicles there is one item, and the + * {@link ModDataComponents#THROWABLE} component says which definition a stack throws, so pack + * throwables need no item of their own. Held like a bow: the longer the draw, the further it goes. + */ +public class ThrowableItem extends Item { + private static final int FULL_DRAW = 35; + + public ThrowableItem(Properties properties) { + super(properties); + } + + /** A stack of {@code def}, showing the item model the import wrote for it. */ + public static ItemStack stackOf(ThrowableDefinition def) { + ItemStack stack = new ItemStack(ModItems.THROWABLE.get()); + stack.set(ModDataComponents.THROWABLE.get(), def.name()); + stack.set(DataComponents.MAX_STACK_SIZE, def.stackSize); + if (def.pack().equals(ContentSource.ORIGINAL)) { + MCHeli.legacyContent().iconIndex(def.name()) + .ifPresent(index -> stack.set(DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(index))); + } + return stack; + } + + private static @Nullable ThrowableDefinition definition(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + return name != null ? MCHeli.content().throwable(name) : null; + } + + @Override + public Component getName(ItemStack stack) { + String name = stack.get(ModDataComponents.THROWABLE.get()); + if (name == null) { + return super.getName(stack); + } + ThrowableDefinition def = MCHeli.content().throwable(name); + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + if (definition(player.getItemInHand(hand)) == null) { + return InteractionResultHolder.fail(player.getItemInHand(hand)); + } + player.startUsingItem(hand); + return InteractionResultHolder.consume(player.getItemInHand(hand)); + } + + @Override + public int getUseDuration(ItemStack stack) { + return 72000; + } + + @Override + public UseAnim getUseAnimation(ItemStack stack) { + return UseAnim.BOW; + } + + @Override + public void releaseUsing(ItemStack stack, Level level, LivingEntity entity, int remainingTime) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return; + } + int drawn = Math.max(getUseDuration(stack) - remainingTime, 5); + float strength = drawn <= FULL_DRAW ? drawn / 25.0F : 1.0F; + level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), SoundEvents.ARROW_SHOOT, SoundSource.PLAYERS, + 0.5F, 0.4F / (level.getRandom().nextFloat() * 0.4F + 0.8F)); + if (!level.isClientSide()) { + level.addFreshEntity(ThrowableEntity.thrown(level, entity, def, strength)); + } + if (entity instanceof Player player) { + player.awardStat(Stats.ITEM_USED.get(this)); + } + stack.consume(1, entity); + } + + /** Dispensers throw them straight out, {@code MCH_ItemThrowableDispenseBehavior}. */ + public static class DispenseBehavior extends DefaultDispenseItemBehavior { + @Override + protected ItemStack execute(BlockSource source, ItemStack stack) { + ThrowableDefinition def = definition(stack); + if (def == null) { + return super.execute(source, stack); + } + Direction facing = source.state().getValue(DispenserBlock.FACING); + Vec3 at = source.center().add(facing.getStepX() * 0.7, facing.getStepY() * 0.7, facing.getStepZ() * 0.7); + Vec3 direction = new Vec3(facing.getStepX(), facing.getStepY() + 0.1, facing.getStepZ()); + source.level().addFreshEntity(ThrowableEntity.dispensed(source.level(), at, direction, def)); + stack.shrink(1); + return stack; + } + } +} From 4f4ee36b6fe4c0d19214f8a5a13e4d91ed53cde1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:48 +0800 Subject: [PATCH 1258/1461] Add item/UavStationItem.java to the 1.20.6 shared sources --- .../mcheli/item/UavStationItem.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java new file mode 100644 index 00000000..7e3bc1c8 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/UavStationItem.java @@ -0,0 +1,33 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station and the portable UAV controller, {@code MCH_ItemUavStation}. Both place the same + * entity; the kind decides its model and which drones it launches. + */ +public class UavStationItem extends PlacedEntityItem { + private final int kind; + + public UavStationItem(int kind, Properties properties) { + super(ModEntities.UAV_STATION, properties); + this.kind = kind; + } + + /** Squared to the nearest quarter turn, facing the player, as the original placed it. */ + @Override + protected void prepare(Entity entity, @Nullable Player player) { + if (entity instanceof UavStationEntity station) { + station.setKind(kind); + float yaw = player != null ? player.getYRot() : 0.0F; + int quarter = Math.floorMod(Math.round(Mth.wrapDegrees(yaw) / 90.0F), 4); + station.setYRot(quarter * 90.0F - 180.0F); + station.initOffset(); + } + } +} From b9efd788d46377d979a01632e8d558e34602c07d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:48 +0800 Subject: [PATCH 1259/1461] Add item/VehicleIngredient.java to the 1.20.6 shared sources --- .../mcheli/item/VehicleIngredient.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java new file mode 100644 index 00000000..f1a42eab --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Matches one vehicle, such as an AH-6 in the recipe that turns it into an AH-6X. Every vehicle of + * a kind shares an item, so a plain item ingredient would accept any of them. In JSON: + *

{"type": "mcheli:vehicle", "vehicle": "ah-6"}
+ * with the type also under {@code neoforge:ingredient_type} and {@code fabric:type}, since each + * loader names custom ingredients its own way. + * + *

This is the loader-independent half; each loader module wraps it in its own ingredient class. + */ +public final class VehicleIngredient { + public static final ResourceLocation ID = new ResourceLocation(MCHeli.MOD_ID, "vehicle"); + + private VehicleIngredient() { + } + + /** The JSON fields, given a way to build the loader's ingredient from a vehicle name. */ + public static MapCodec codec(Function create, Function vehicle) { + return Codec.STRING.fieldOf("vehicle").xmap(create, vehicle); + } + + public static StreamCodec streamCodec(Function create, Function vehicle) { + return ByteBufCodecs.STRING_UTF8.map(create, vehicle).cast(); + } + + /** The items a vehicle can be: the generic item of each kind. */ + @SuppressWarnings("deprecation") + public static HolderSet kindItems() { + return HolderSet.direct(Item::builtInRegistryHolder, + ModItems.HELICOPTER.get(), ModItems.PLANE.get(), ModItems.TANK.get(), ModItems.VEHICLE.get()); + } + + public static Stream> items() { + return kindItems().stream(); + } + + public static boolean matches(String vehicle, @Nullable ItemStack stack) { + return stack != null && vehicle.equals(AircraftItem.vehicleOf(stack)); + } + + /** What recipe books and JEI show: the vehicle itself, or nothing while it is not loaded. */ + public static Stream displayStacks(String vehicle) { + AircraftDefinition def = MCHeli.content().any(vehicle); + return def != null ? Stream.of(AircraftItem.stackOf(def)) : Stream.empty(); + } +} From 4a4c7a1031f16a263ae79f4defb392b3c233d7ef Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:49 +0800 Subject: [PATCH 1260/1461] Add item/WrenchItem.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/item/WrenchItem.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java new file mode 100644 index 00000000..b02eca5d --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/item/WrenchItem.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +/** + * The wrench, {@code MCH_ItemWrench}. Right-clicking a damaged vehicle repairs 10 hit points, once + * a second, at one point of the wrench's durability. Sneaking switches the vehicle to its next + * paint scheme. + * + *

The original repaired while the use key was held; a click per repair works the same with + * today's item use, which stops as soon as the cursor leaves the hull. + */ +public class WrenchItem extends Item { + /** An iron tool's durability, the original's tool material. */ + private static final int DURABILITY = 250; + private static final int REPAIR = 10; + private static final int COOLDOWN = 20; + + public WrenchItem(Properties properties) { + super(properties.durability(DURABILITY)); + } + + /** Server only. */ + public static InteractionResult useOn(ItemStack stack, AircraftEntity vehicle, Player player, InteractionHand hand) { + if (player.isSecondaryUseActive()) { + return vehicle.nextTexture() ? InteractionResult.SUCCESS : InteractionResult.FAIL; + } + if (player.getCooldowns().isOnCooldown(stack.getItem()) || !vehicle.repair(REPAIR)) { + return InteractionResult.FAIL; + } + player.getCooldowns().addCooldown(stack.getItem(), COOLDOWN); + stack.hurtAndBreak(1, player, LivingEntity.getSlotForHand(hand)); + ModSounds.playAt(vehicle, "wrench", 1.0F, 0.9F + vehicle.level().getRandom().nextFloat() * 0.2F); + return InteractionResult.SUCCESS; + } +} From 172ab230d219d5f506e1d4561ba8139929286ed2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:49 +0800 Subject: [PATCH 1261/1461] Take the drafting table's result from the recipe on 1.20.6 --- .../mcheli/menu/DraftingTableMenu.java | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java new file mode 100644 index 00000000..13a96f96 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/DraftingTableMenu.java @@ -0,0 +1,154 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.registry.ModBlocks; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.ResultContainer; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; + +import java.util.List; + +/** + * The drafting table, {@code MCH_DraftingTableGuiContainer}: the player's inventory and one output + * slot. The screen browses the MCHeli recipes and asks for one by its id; the server finds the + * recipe, takes the ingredients from the inventory and puts the result in the output slot. + */ +public class DraftingTableMenu extends AbstractContainerMenu { + public static final int OUTPUT_X = 178; + public static final int OUTPUT_Y = 90; + + private final ContainerLevelAccess access; + private final ResultContainer output = new ResultContainer(); + private final int outputSlot; + + /** The client side, from the open screen packet. */ + public DraftingTableMenu(int containerId, Inventory inventory) { + this(containerId, inventory, ContainerLevelAccess.NULL); + } + + public DraftingTableMenu(int containerId, Inventory inventory, ContainerLevelAccess access) { + super(ModMenus.DRAFTING_TABLE.get(), containerId); + this.access = access; + addPlayerSlots(inventory, 30, 140); + outputSlot = slots.size(); + addSlot(new Slot(output, 0, OUTPUT_X, OUTPUT_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + } + + /** + * {@code createRecipeItem}: makes one of the recipe behind {@code id}, if it is ours, the output + * slot is free and the player carries the ingredients. Creative players need neither. + */ + public void craft(ServerPlayer player, ResourceLocation id) { + boolean creative = player.getAbilities().instabuild; + Slot slot = slots.get(outputSlot); + if (slot.hasItem() && !creative) { + return; + } + RecipeHolder holder = player.level().getServer().getRecipeManager().byKey(id).orElse(null); + if (holder == null || !id.getNamespace().equals(MCHeli.MOD_ID) || !(holder.value() instanceof CraftingRecipe recipe)) { + return; + } + // A shaped recipe lists its empty grid slots too. + List ingredients = recipe.getIngredients().stream().filter(ingredient -> !ingredient.isEmpty()).toList(); + if (!creative && !take(player.getInventory(), ingredients, true)) { + return; + } + if (!creative) { + take(player.getInventory(), ingredients, false); + } + slot.set(recipe.getResultItem(player.registryAccess()).copy()); + broadcastChanges(); + } + + /** + * Finds, and unless {@code dryRun} removes, one item per ingredient. A stack serves several + * ingredients as far as its count goes. + */ + private static boolean take(Inventory inventory, List ingredients, boolean dryRun) { + int[] used = new int[inventory.getContainerSize()]; + for (Ingredient ingredient : ingredients) { + int found = -1; + for (int i = 0; i < inventory.getContainerSize() && found < 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && stack.getCount() > used[i] && ingredient.test(stack)) { + found = i; + } + } + if (found < 0) { + return false; + } + used[found]++; + } + if (!dryRun) { + for (int i = 0; i < used.length; i++) { + if (used[i] > 0) { + inventory.removeItem(i, used[i]); + } + } + } + return true; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(access, player, ModBlocks.DRAFTING_TABLE.get()); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + if (index != outputSlot) { + return ItemStack.EMPTY; + } + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (!moveItemStackTo(stack, 0, outputSlot, true)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** What is left in the output goes back to the player, as in the original. */ + @Override + public void removed(Player player) { + super.removed(player); + if (!player.level().isClientSide()) { + clearContainer(player, output); + } + } + + /** The player's inventory and, 58 below its top, the hotbar. */ + private void addPlayerSlots(Inventory inventory, int x, int y) { + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column + row * 9 + 9, x + column * 18, y + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, x + column * 18, y + 58)); + } + } +} From da2cec878de2d2eda0084066e5da2ab74143731b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:50 +0800 Subject: [PATCH 1262/1461] Add menu/UavStationMenu.java to the 1.20.6 shared sources --- .../mcheli/menu/UavStationMenu.java | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java new file mode 100644 index 00000000..6e4b7a0c --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/UavStationMenu.java @@ -0,0 +1,166 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The UAV station screen, {@code MCH_ContainerUavStation}: one slot for the drone to put up, the + * launch offset with its -10, -1, +1 and +10 buttons per axis, and Continue to take the last drone + * again. The player sits at the station while it is open, so the client finds it as their vehicle. + */ +public class UavStationMenu extends AbstractContainerMenu { + /** Button ids: axis times four plus the step, then Continue. */ + public static final int[] STEPS = {-10, -1, 1, 10}; + public static final int BUTTON_CONTINUE = 12; + + private static final int DATA_SIZE = 4; + private static final int DATA_CAN_CONTINUE = 3; + + private final @Nullable UavStationEntity station; + private final ContainerData data; + + /** The client side, from the open screen packet. */ + public UavStationMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof UavStationEntity s ? s : null, + new SimpleContainer(1), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public UavStationMenu(int containerId, Inventory inventory, UavStationEntity station) { + this(containerId, inventory, station, station.slot(), new ServerData(station)); + } + + private UavStationMenu(int containerId, Inventory inventory, @Nullable UavStationEntity station, Container slot, ContainerData data) { + super(ModMenus.UAV_STATION.get(), containerId); + this.station = station; + this.data = data; + checkContainerSize(slot, 1); + addSlot(new Slot(slot, 0, 20, 20) { + @Override + public boolean mayPlace(ItemStack stack) { + return AircraftItem.vehicleOf(stack) != null; + } + + @Override + public int getMaxStackSize() { + return 1; + } + }); + addPlayerSlots(inventory, 8, 84); + addDataSlots(data); + } + + public @Nullable UavStationEntity station() { + return station; + } + + /** The launch offset along {@code axis} (0 x, 1 y, 2 z), as the server last reported it. */ + public int offset(int axis) { + return (short) data.get(axis); + } + + public boolean canContinue() { + return data.get(DATA_CAN_CONTINUE) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (station == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CONTINUE) { + return station.continueControl(server); + } + if (id < 0 || id >= 12) { + return false; + } + int[] pos = {station.offset().getX(), station.offset().getY(), station.offset().getZ()}; + pos[id / 4] += STEPS[id % 4]; + station.setOffset(new BlockPos(pos[0], pos[1], pos[2])); + return true; + } + + @Override + public boolean stillValid(Player player) { + return station != null && station.isAlive() && player.getVehicle() == station; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + if (index == 0) { + if (!moveItemStackTo(stack, 1, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (AircraftItem.vehicleOf(stack) == null || !moveItemStackTo(stack, 0, 1, false)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** Whatever is left in the slot goes back to the player, as with a crafting table. */ + @Override + public void removed(Player player) { + super.removed(player); + if (station != null && !player.level().isClientSide()) { + clearContainer(player, station.slot()); + } + } + + private record ServerData(UavStationEntity station) implements ContainerData { + @Override + public int get(int index) { + BlockPos o = station.offset(); + return switch (index) { + case 0 -> o.getX(); + case 1 -> o.getY(); + case 2 -> o.getZ(); + default -> station.lastUav() != null ? 1 : 0; + }; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } + + /** The player's inventory and, 58 below its top, the hotbar. */ + private void addPlayerSlots(Inventory inventory, int x, int y) { + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column + row * 9 + 9, x + column * 18, y + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, x + column * 18, y + 58)); + } + } +} From 906c0ea9c5a197cf8a59961a91654f6ad991720b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:50 +0800 Subject: [PATCH 1263/1461] Add menu/VehicleMenu.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/menu/VehicleMenu.java | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java new file mode 100644 index 00000000..d69c63ec --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/menu/VehicleMenu.java @@ -0,0 +1,229 @@ +package com.jnhprojects.mcheli.menu; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.VehicleInventory; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.registry.ModMenus; +import com.jnhprojects.mcheli.weapon.Resupply; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +/** + * The vehicle screen, {@code MCH_AircraftGuiContainer}: fuel slots, a parachute slot per ejection + * seat, reloading the weapons from the crew member's own items and the way into the cargo chest. + * + *

Only crew open it, so the client finds the vehicle as the one its player rides. The weapon + * rounds and which weapons can be reloaded right now come from the server through data slots, + * since a client only hears about the ammunition of the seat it sits in. + */ +public class VehicleMenu extends AbstractContainerMenu { + /** Weapons the screen can show; more than any vehicle in the original has. */ + public static final int MAX_WEAPONS = 32; + /** Button ids from zero up reload that weapon; this one opens the cargo chest. */ + public static final int BUTTON_CARGO = 1000; + + public static final int FUEL_X = 8; + public static final int FUEL_Y = 30; + public static final int PARACHUTE_X = 8; + public static final int PARACHUTE_Y = 66; + public static final int INVENTORY_Y = 102; + + private static final int DATA_RELOADABLE = MAX_WEAPONS; + private static final int DATA_SIZE = MAX_WEAPONS + 2; + + private final @Nullable AircraftEntity vehicle; + private final Container equipment; + private final ContainerData data; + private final int parachuteSlots; + private boolean supplied; + + /** The client side, from the open screen packet. */ + public VehicleMenu(int containerId, Inventory inventory) { + this(containerId, inventory, inventory.player.getVehicle() instanceof AircraftEntity v ? v : null, + new SimpleContainer(VehicleInventory.EQUIPMENT_SLOTS), new SimpleContainerData(DATA_SIZE)); + } + + /** The server side. */ + public VehicleMenu(int containerId, Inventory inventory, AircraftEntity vehicle) { + this(containerId, inventory, vehicle, vehicle.inventory().equipment(), new ServerData(vehicle, inventory.player)); + } + + private VehicleMenu(int containerId, Inventory inventory, @Nullable AircraftEntity vehicle, Container equipment, ContainerData data) { + super(ModMenus.VEHICLE.get(), containerId); + this.vehicle = vehicle; + this.equipment = equipment; + this.data = data; + AircraftDefinition def = vehicle != null ? vehicle.definition() : null; + parachuteSlots = def != null ? VehicleInventory.parachuteSlots(def) : 0; + checkContainerSize(equipment, VehicleInventory.EQUIPMENT_SLOTS); + + for (int i = 0; i < VehicleInventory.FUEL_SLOTS; i++) { + addSlot(new Slot(equipment, i, FUEL_X + i * 18, FUEL_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() instanceof FuelItem; + } + }); + } + for (int i = 0; i < VehicleInventory.PARACHUTE_SLOTS; i++) { + boolean active = i < parachuteSlots; + addSlot(new Slot(equipment, VehicleInventory.FUEL_SLOTS + i, PARACHUTE_X + i * 18, PARACHUTE_Y) { + @Override + public boolean mayPlace(ItemStack stack) { + return active && stack.getItem() instanceof ParachuteItem; + } + + @Override + public boolean isActive() { + return active; + } + }); + } + addPlayerSlots(inventory, 8, INVENTORY_Y); + addDataSlots(data); + } + + public @Nullable AircraftEntity vehicle() { + return vehicle; + } + + /** Parachute slots in use: one per ejection seat. */ + public int parachuteSlots() { + return parachuteSlots; + } + + /** Magazine and reserve of a weapon together, as the server last reported them. */ + public int rounds(int weapon) { + return weapon >= 0 && weapon < MAX_WEAPONS ? data.get(weapon) : 0; + } + + /** Whether the server would reload this weapon now: the vehicle is down and the items are there. */ + public boolean canReload(int weapon) { + if (weapon < 0 || weapon >= MAX_WEAPONS) { + return false; + } + int bits = (data.get(DATA_RELOADABLE) & 0xFFFF) | (data.get(DATA_RELOADABLE + 1) & 0xFFFF) << 16; + return (bits & 1 << weapon) != 0; + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (vehicle == null || !(player instanceof ServerPlayer server)) { + return false; + } + if (id == BUTTON_CARGO) { + return vehicle.openCargo(server); + } + if (id >= 0 && id < MAX_WEAPONS && Resupply.supply(server, vehicle, id)) { + supplied = true; + return true; + } + return false; + } + + @Override + public boolean stillValid(Player player) { + return vehicle != null && equipment.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack stack = slot.getItem(); + ItemStack moved = stack.copy(); + int equipmentEnd = VehicleInventory.EQUIPMENT_SLOTS; + if (index < equipmentEnd) { + if (!moveItemStackTo(stack, equipmentEnd, slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof FuelItem) { + if (!moveItemStackTo(stack, 0, VehicleInventory.FUEL_SLOTS, false)) { + return ItemStack.EMPTY; + } + } else if (stack.getItem() instanceof ParachuteItem && parachuteSlots > 0) { + int first = VehicleInventory.FUEL_SLOTS; + if (!moveItemStackTo(stack, first, first + parachuteSlots, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + return moved; + } + + /** As in the original, closing the screen after a resupply loads every magazine from its reserve. */ + @Override + public void removed(Player player) { + super.removed(player); + if (supplied && vehicle != null && vehicle.weapons() != null && !player.level().isClientSide()) { + vehicle.weapons().reloadAll(); + } + } + + /** Rounds per weapon and the reload bits, worked out on the server whenever the menu syncs. */ + private record ServerData(AircraftEntity vehicle, Player player) implements ContainerData { + @Override + public int get(int index) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons == null) { + return 0; + } + if (index < MAX_WEAPONS) { + WeaponSet set = weapons.set(index); + // Data slots travel as shorts. + return set != null ? Math.min(set.ammo() + set.restAmmo(), Short.MAX_VALUE) : 0; + } + int bits = 0; + if (player instanceof ServerPlayer server) { + for (int i = 0; i < Math.min(weapons.sets().size(), MAX_WEAPONS); i++) { + if (Resupply.canSupply(server, vehicle, i)) { + bits |= 1 << i; + } + } + } + return index == DATA_RELOADABLE ? bits & 0xFFFF : bits >>> 16; + } + + @Override + public void set(int index, int value) { + } + + @Override + public int getCount() { + return DATA_SIZE; + } + } + + /** The player's inventory and, 58 below its top, the hotbar. */ + private void addPlayerSlots(Inventory inventory, int x, int y) { + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column + row * 9 + 9, x + column * 18, y + row * 18)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, x + column * 18, y + 58)); + } + } +} From 073620f9cc1db9d59e83032fe1fdc9ac1c499348 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:50 +0800 Subject: [PATCH 1264/1461] Add network/BoardVehiclePacket.java to the 1.20.6 shared sources --- .../mcheli/network/BoardVehiclePacket.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java new file mode 100644 index 00000000..0f7ccf72 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.Vec3; + +/** + * Client to server: the player right-clicked a vehicle's hull outside its main hit box. The + * server repeats the reach check against the hull boxes, then treats it as an ordinary click. + * + * @param location where on the hull the click landed, in world coordinates + */ +public record BoardVehiclePacket(int entityId, Vec3 location) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("board_vehicle"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(BoardVehiclePacket::encode, BoardVehiclePacket::decode); + + /** The same slack vanilla allows on top of the player's reach for a click on an entity. */ + private static final double REACH_SLACK = 3.0; + + public static void encode(BoardVehiclePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeDouble(msg.location.x); + buf.writeDouble(msg.location.y); + buf.writeDouble(msg.location.z); + } + + public static BoardVehiclePacket decode(FriendlyByteBuf buf) { + return new BoardVehiclePacket(buf.readVarInt(), new Vec3(buf.readDouble(), buf.readDouble(), buf.readDouble())); + } + + public static void handle(BoardVehiclePacket msg, ServerPlayer sender) { + if (sender == null || !(sender.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) || vehicle.isRemoved()) { + return; + } + Vec3 eye = sender.getEyePosition(); + double reach = sender.entityInteractionRange() + REACH_SLACK; + // The click must land on the hull, and the hull must be within reach. + if (eye.distanceToSqr(msg.location) > reach * reach) { + return; + } + Vec3 through = eye.add(msg.location.subtract(eye).scale(1.05)); + boolean onHull = vehicle.clipHitBoxes(eye, through, 0.5).isPresent(); + vehicle.consumeHitBoxFactor(); + if (onHull) { + vehicle.interactAt(sender, msg.location.subtract(vehicle.position()), InteractionHand.MAIN_HAND); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 534aea6ecf1aa7beefe49fbbff8e8331b9cbd442 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:51 +0800 Subject: [PATCH 1265/1461] Add network/CrewActionPacket.java to the 1.20.6 shared sources --- .../mcheli/network/CrewActionPacket.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java new file mode 100644 index 00000000..1b565e21 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java @@ -0,0 +1,164 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A one-off request from a crew member: lower the gear, fold the wings, change seats and so on. + * + *

The client only asks; the server checks that the sender sits in that vehicle, in a seat + * allowed to do it, and then applies the same conditions the original checked before switching. + * The result reaches every client through synched data. + */ +public record CrewActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("crew_action"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(CrewActionPacket::encode, CrewActionPacket::decode); + + public enum Action { + /** Raise or lower the landing gear. Pilot only. */ + LANDING_GEAR, + /** Fold or unfold hatches, wings or swept wings. Pilot only. */ + FOLD, + /** Tilt the nozzles for vertical flight. Pilot only. */ + VTOL, + /** Eject from seat zero or one. */ + EJECT, + /** Move to the next free crew seat. */ + SWITCH_SEAT, + /** Load nearby cargo onto the racks, or drive onto a carrier's rack. Pilot only. */ + RACK_LOAD, + /** Drop the last cargo, or leave the rack this vehicle rides on. Pilot only. */ + RACK_UNLOAD, + /** Select the next weapon this seat may use. */ + NEXT_WEAPON, + /** Select the previous weapon this seat may use. */ + PREVIOUS_WEAPON, + /** Cycle the fire mode of the selected weapon. */ + WEAPON_MODE, + /** Release flares. Pilot and first crew seat. */ + FLARE, + /** Open the vehicle screen: fuel and parachute slots, reloading, the way to the cargo chest. */ + VEHICLE_SCREEN, + /** Hand the controls to the autopilot and man the weapons, or take them back. Pilot only. */ + GUNNER_MODE, + /** Turn night vision on for the sender, in vehicles with {@code EnableNightVision}. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF, + /** Let the troops down: ropes from a hover, a parachute drop, or out on the ground. Pilot only. */ + DROP_CREW, + /** Slide down a rope or jump with a parachute. Crew other than the pilot. */ + JUMP_OUT, + /** Turn the search light on or off. Pilot and first crew seat. */ + SEARCH_LIGHT + } + + public static void encode(CrewActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static CrewActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown crew action " + ordinal); + } + return new CrewActionPacket(entityId, actions[ordinal]); + } + + public static void handle(CrewActionPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + boolean pilot = vehicle.getControllingPassenger() == sender; + switch (msg.action) { + case LANDING_GEAR -> { + if (pilot) { + vehicle.toggleLandingGear(); + } + } + case FOLD -> { + if (pilot) { + vehicle.toggleFold(); + } + } + case VTOL -> { + if (pilot && vehicle instanceof PlaneEntity plane) { + plane.toggleVtol(); + } + } + case GUNNER_MODE -> { + if (pilot) { + vehicle.setGunnerMode(!vehicle.isGunnerMode()); + } + } + case EJECT -> vehicle.eject(sender); + case SWITCH_SEAT -> vehicle.switchSeat(sender); + case RACK_LOAD -> { + if (pilot) { + if (!vehicle.boardNearbyRack()) { + vehicle.loadNearbyCargo(); + } + } + } + case RACK_UNLOAD -> { + if (pilot) { + if (vehicle.isPassenger()) { + vehicle.leaveRack(); + } else { + vehicle.unloadCargo(); + } + } + } + case NEXT_WEAPON, PREVIOUS_WEAPON -> { + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons != null && !vehicle.isDestroyed()) { + int next = weapons.next(seat, msg.action == Action.NEXT_WEAPON ? 1 : -1); + if (next >= 0) { + weapons.switchTo(sender, seat, next); + } + } + } + case WEAPON_MODE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.switchMode(vehicle.seatOf(sender)); + } + } + case FLARE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null && vehicle.seatOf(sender) <= 1) { + weapons.flares.use(); + } + } + case VEHICLE_SCREEN -> vehicle.openScreen(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> vehicle.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + case DROP_CREW -> { + if (pilot && !vehicle.dropCrew()) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + case SEARCH_LIGHT -> vehicle.toggleSearchLight(sender); + case JUMP_OUT -> { + if (!pilot && !vehicle.jumpOut(sender)) { + ModSounds.playTo(sender, "ng", 1.0F, 1.0F); + } + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 3817eba99ada4bc484c69d4e01c9d0135b66d28f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:51 +0800 Subject: [PATCH 1266/1461] Add network/DraftingTablePacket.java to the 1.20.6 shared sources --- .../mcheli/network/DraftingTablePacket.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java new file mode 100644 index 00000000..02c394df --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/DraftingTablePacket.java @@ -0,0 +1,37 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +/** + * The Create button of the drafting table, {@code MCH_DraftingTableCreatePacket}: make one of the + * recipe with this id. + */ +public record DraftingTablePacket(ResourceLocation recipe) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("drafting_table"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(DraftingTablePacket::encode, DraftingTablePacket::decode); + + public static void encode(DraftingTablePacket msg, FriendlyByteBuf buf) { + buf.writeResourceLocation(msg.recipe); + } + + public static DraftingTablePacket decode(FriendlyByteBuf buf) { + return new DraftingTablePacket(buf.readResourceLocation()); + } + + public static void handle(DraftingTablePacket msg, ServerPlayer sender) { + if (sender != null && sender.containerMenu instanceof DraftingTableMenu menu && menu.stillValid(sender)) { + menu.craft(sender, msg.recipe); + } + } + + @Override + public Type type() { + return TYPE; + } +} From b908e7a57c4b053c7a0fff657c773b5efc50d166 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:52 +0800 Subject: [PATCH 1267/1461] Add network/EquipmentActionPacket.java to the 1.20.6 shared sources --- .../mcheli/network/EquipmentActionPacket.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java new file mode 100644 index 00000000..76090f2e --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/EquipmentActionPacket.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.GltdEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A request from a player using a piece of equipment they sit at, such as the GLTD. Like + * {@link CrewActionPacket} the client only asks; the equipment checks that the sender is the one + * using it. + */ +public record EquipmentActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("equipment_action"); + public static final StreamCodec STREAM_CODEC = + StreamCodec.ofMember(EquipmentActionPacket::encode, EquipmentActionPacket::decode); + + public enum Action { + /** Mark the spot under the sight and call in the A-10. */ + CALL_STRIKE, + /** Turn night vision on for the sender. */ + NIGHT_VISION_ON, + /** Turn the sender's night vision off again. */ + NIGHT_VISION_OFF + } + + public static void encode(EquipmentActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static EquipmentActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown equipment action " + ordinal); + } + return new EquipmentActionPacket(entityId, actions[ordinal]); + } + + public static void handle(EquipmentActionPacket msg, ServerPlayer sender) { + if (sender == null || sender.getVehicle() == null || sender.getVehicle().getId() != msg.entityId) { + return; + } + if (sender.getVehicle() instanceof GltdEntity gltd) { + switch (msg.action) { + case CALL_STRIKE -> gltd.callStrike(sender); + case NIGHT_VISION_ON, NIGHT_VISION_OFF -> gltd.setNightVision(sender, msg.action == Action.NIGHT_VISION_ON); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From c43426c732964c2a21d9cfbe666a1717d9c81015 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:52 +0800 Subject: [PATCH 1268/1461] Add network/ExplosionEffectPacket.java to the 1.20.6 shared sources --- .../mcheli/network/ExplosionEffectPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java new file mode 100644 index 00000000..279b1511 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.ExplosionEffect; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to nearby players: draw an MCHeli explosion here. The original's + * {@code MCH_PacketEffectExplosion}, plus a flag for the bigger fireball of a crashing vehicle. + */ +public record ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater, boolean crash) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("explosion_effect"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(ExplosionEffectPacket::encode, ExplosionEffectPacket::decode); + + public ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater) { + this(x, y, z, size, inWater, false); + } + + public static void encode(ExplosionEffectPacket msg, FriendlyByteBuf buf) { + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.size); + buf.writeBoolean(msg.inWater); + buf.writeBoolean(msg.crash); + } + + public static ExplosionEffectPacket decode(FriendlyByteBuf buf) { + return new ExplosionEffectPacket(buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readBoolean(), + buf.readBoolean()); + } + + public static void handle(ExplosionEffectPacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player == null || !Float.isFinite(msg.size)) { + return; + } + float size = Math.min(msg.size, 50.0F); + if (msg.crash) { + ExplosionEffect.crash(player.level(), msg.x, msg.y, msg.z, Math.min(size, ExplosionEffect.MAX_CRASH_SIZE)); + } else { + ExplosionEffect.play(player.level(), msg.x, msg.y, msg.z, size, msg.inWater); + } + } + + @Override + public Type type() { + return TYPE; + } +} From ab160f646e4a0d670e81dca12cf9bf07245373a5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:53 +0800 Subject: [PATCH 1269/1461] Add network/FireWeaponPacket.java to the 1.20.6 shared sources --- .../mcheli/network/FireWeaponPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java new file mode 100644 index 00000000..0056dcc0 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.ServerFiring; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Client to server: the crew member's selected weapon fired on their side, so fire it for real. + * The client already checked readiness, ammunition and lock; the server checks again with its own + * timers and only trusts the two option values, a locked entity and a fire mode. + * + * @param weapon the weapon the client fired, ignored unless it is still the seat's selection + */ +public record FireWeaponPacket(int entityId, int weapon, int option1, int option2) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("fire_weapon"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(FireWeaponPacket::encode, FireWeaponPacket::decode); + + public static void encode(FireWeaponPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.option1); + buf.writeVarInt(msg.option2); + } + + public static FireWeaponPacket decode(FriendlyByteBuf buf) { + return new FireWeaponPacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(FireWeaponPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons == null || weapons.selected(seat) != msg.weapon || weapons.operatorSeat(msg.weapon) != seat) { + return; + } + Shot shot = new Shot(vehicle, sender); + shot.option1 = msg.option1; + shot.option2 = msg.option2; + weapons.use(sender, seat, shot, ServerFiring.INSTANCE); + } + + @Override + public Type type() { + return TYPE; + } +} From 41161bc8b9d3ea91a11c8f7209aa42be07c2113f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:53 +0800 Subject: [PATCH 1270/1461] Add network/LauncherPacket.java to the 1.20.6 shared sources --- .../mcheli/network/LauncherPacket.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java new file mode 100644 index 00000000..4306edfa --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/LauncherPacket.java @@ -0,0 +1,118 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +/** + * The player with a launcher in hand, {@code MCH_PacketLightWeaponPlayerControl}: fire at the + * locked target, or finish a reload. The client locks and times the reload; the server checks + * the launcher, the target and the missile in the inventory. + * + * @param target the locked entity, for {@link Action#FIRE} + * @param mode the Javelin's attack mode: 0 direct, 1 top attack + */ +public record LauncherPacket(Action action, int target, int mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("launcher"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LauncherPacket::encode, LauncherPacket::decode); + + /** Locks further than this are refused, with some slack over the lock range for lag. */ + private static final double MAX_TARGET_DISTANCE = 260.0; + + public enum Action { + FIRE, + RELOAD + } + + public static void encode(LauncherPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeVarInt(msg.target); + buf.writeVarInt(msg.mode); + } + + public static LauncherPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown launcher action " + ordinal); + } + return new LauncherPacket(actions[ordinal], buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(LauncherPacket msg, ServerPlayer sender) { + ItemStack stack = sender == null ? ItemStack.EMPTY : sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof LauncherItem launcher) || !LauncherItem.canUse(sender)) { + return; + } + switch (msg.action) { + case FIRE -> fire(sender, stack, launcher, msg.target, msg.mode); + case RELOAD -> reload(sender, stack, launcher); + } + } + + private static void fire(ServerPlayer player, ItemStack stack, LauncherItem launcher, int targetId, int mode) { + WeaponDefinition def = MCHeli.content().weapon(launcher.weapon()); + ServerLevel level = player.serverLevel(); + Entity target = level.getEntity(targetId); + if (def == null || !LauncherItem.isLoaded(stack) || target == null || target.isRemoved() + || target.distanceToSqr(player) > MAX_TARGET_DISTANCE * MAX_TARGET_DISTANCE) { + return; + } + ProjectileEntity missile = def.type == WeaponType.AT_MISSILE + ? ModEntities.AT_MISSILE.get().create(level) + : ModEntities.AA_MISSILE.get().create(level); + if (missile == null) { + return; + } + float yaw = player.getYRot(); + float pitch = player.getXRot(); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + Vec3 at = player.getEyePosition().add(dir); + missile.launch(at.x, at.y, at.z, dir.x, dir.y, dir.z, yaw, pitch, def.acceleration); + missile.setWeapon(launcher.weapon()); + missile.setParameters(def, player, player); + missile.setTarget(target); + if (missile instanceof ATMissileEntity atMissile) { + atMissile.guidanceType = Math.clamp(mode, 0, Math.max(def.modeNum - 1, 0)); + } + level.addFreshEntity(missile); + if (def.sound != null) { + ModSounds.playAt(player, def.sound, def.soundVolume, def.soundPitch); + } + LauncherItem.setLoaded(stack, false); + } + + private static void reload(ServerPlayer player, ItemStack stack, LauncherItem launcher) { + if (LauncherItem.isLoaded(stack)) { + return; + } + if (!player.getAbilities().instabuild) { + int slot = player.getInventory().findSlotMatchingItem(new ItemStack(launcher.missile())); + if (slot < 0) { + return; + } + player.getInventory().removeItem(slot, 1); + } + LauncherItem.setLoaded(stack, true); + } + + @Override + public Type type() { + return TYPE; + } +} From bd6523e46cb90d0cf812421ce33676d3f4108e1b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:54 +0800 Subject: [PATCH 1271/1461] Add network/LockWarningPacket.java to the 1.20.6 shared sources --- .../mcheli/network/LockWarningPacket.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java new file mode 100644 index 00000000..97d2cbf4 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java @@ -0,0 +1,44 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +/** + * Client to server: this player is locking on to an entity. If it is a vehicle with flares, or a + * crew member of one, its crew hears the warning. The original's {@code MCH_PacketNotifyLock}. + */ +public record LockWarningPacket(int targetId) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("lock_warning"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LockWarningPacket::encode, LockWarningPacket::decode); + + public static void encode(LockWarningPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.targetId); + } + + public static LockWarningPacket decode(FriendlyByteBuf buf) { + return new LockWarningPacket(buf.readVarInt()); + } + + public static void handle(LockWarningPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity)) { + return; + } + Entity target = sender.level().getEntity(msg.targetId); + if (target != null && target.distanceToSqr(sender) < 400.0 * 400.0) { + AircraftEntity vehicle = target instanceof AircraftEntity a ? a : target.getVehicle() instanceof AircraftEntity a ? a : null; + if (vehicle != null) { + vehicle.warnLocked(); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 944c1659742ba9e2a141ffecb548bdf97e35ba32 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:54 +0800 Subject: [PATCH 1272/1461] Add network/MarkerPacket.java to the 1.20.6 shared sources --- .../mcheli/network/MarkerPacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java new file mode 100644 index 00000000..1de0bfc3 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/MarkerPacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.Markers; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to client: the team spotted these entities for {@code ticks}, or marked {@code point}, + * {@code MCH_PacketNotifySpotedEntity} and {@code MCH_PacketNotifyMarkPoint}. Only the spotter and + * their teammates get it. + */ +public record MarkerPacket(int[] entities, int ticks, BlockPos point) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("marker"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(MarkerPacket::encode, MarkerPacket::decode); + /** Spotting sends no point; this stands in for none. */ + private static final BlockPos NO_POINT = new BlockPos(0, Integer.MIN_VALUE, 0); + private static final int MAX_ENTITIES = 256; + + public static MarkerPacket spotted(int[] entities, int ticks) { + return new MarkerPacket(entities, ticks, NO_POINT); + } + + public static MarkerPacket point(BlockPos point) { + return new MarkerPacket(new int[0], 0, point); + } + + public static void encode(MarkerPacket msg, FriendlyByteBuf buf) { + buf.writeVarIntArray(msg.entities); + buf.writeVarInt(msg.ticks); + buf.writeBlockPos(msg.point); + } + + public static MarkerPacket decode(FriendlyByteBuf buf) { + return new MarkerPacket(buf.readVarIntArray(MAX_ENTITIES), buf.readVarInt(), buf.readBlockPos()); + } + + public static void handle(MarkerPacket msg) { + if (msg.entities.length > 0) { + Markers.spotted(msg.entities, msg.ticks); + } + if (!msg.point.equals(NO_POINT)) { + Markers.point(msg.point); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 52ce0d3e7003227f8e8129e94cd0c92e2e86064e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:54 +0800 Subject: [PATCH 1273/1461] Add network/ModNetwork.java to the 1.20.6 shared sources --- .../mcheli/network/ModNetwork.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java new file mode 100644 index 00000000..fa0fbea9 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java @@ -0,0 +1,90 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Our packets. Each one is a custom payload with its own id and the same encoding on every loader, + * so a client on one loader can talk to a server on another. The loader module registers the + * lists below with its own networking API; handlers always run on the main thread. + */ +public final class ModNetwork { + /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ + public static final int PROTOCOL_VERSION = 11; + + /** Client to server. The handler gets the sending player. */ + public static final List> SERVERBOUND = List.of( + new Serverbound<>(PilotStatePacket.TYPE, PilotStatePacket.STREAM_CODEC, PilotStatePacket::handle), + new Serverbound<>(CrewActionPacket.TYPE, CrewActionPacket.STREAM_CODEC, CrewActionPacket::handle), + new Serverbound<>(EquipmentActionPacket.TYPE, EquipmentActionPacket.STREAM_CODEC, EquipmentActionPacket::handle), + new Serverbound<>(SpotPacket.TYPE, SpotPacket.STREAM_CODEC, SpotPacket::handle), + new Serverbound<>(LauncherPacket.TYPE, LauncherPacket.STREAM_CODEC, LauncherPacket::handle), + new Serverbound<>(DraftingTablePacket.TYPE, DraftingTablePacket.STREAM_CODEC, DraftingTablePacket::handle), + new Serverbound<>(TeamPacket.TYPE, TeamPacket.STREAM_CODEC, TeamPacket::handle), + new Serverbound<>(TestFlightPacket.TYPE, TestFlightPacket.STREAM_CODEC, TestFlightPacket::handle), + new Serverbound<>(FireWeaponPacket.TYPE, FireWeaponPacket.STREAM_CODEC, FireWeaponPacket::handle), + new Serverbound<>(BoardVehiclePacket.TYPE, BoardVehiclePacket.STREAM_CODEC, BoardVehiclePacket::handle), + new Serverbound<>(LockWarningPacket.TYPE, LockWarningPacket.STREAM_CODEC, LockWarningPacket::handle), + new Serverbound<>(PackRequestPacket.TYPE, PackRequestPacket.STREAM_CODEC, PackRequestPacket::handle), + new Serverbound<>(PackSubmitPacket.TYPE, PackSubmitPacket.STREAM_CODEC, PackSubmitPacket::handle)); + + /** Server to client. Handlers touch client classes, so they only ever run on a client. */ + public static final List> CLIENTBOUND = List.of( + new Clientbound<>(WeaponStatePacket.TYPE, WeaponStatePacket.STREAM_CODEC, WeaponStatePacket::handle), + new Clientbound<>(ExplosionEffectPacket.TYPE, ExplosionEffectPacket.STREAM_CODEC, ExplosionEffectPacket::handle), + new Clientbound<>(PackListPacket.TYPE, PackListPacket.STREAM_CODEC, PackListPacket::handle), + new Clientbound<>(PackDataPacket.TYPE, PackDataPacket.STREAM_CODEC, PackDataPacket::handle), + new Clientbound<>(PackSoundPacket.TYPE, PackSoundPacket.STREAM_CODEC, PackSoundPacket::handle), + new Clientbound<>(MarkerPacket.TYPE, MarkerPacket.STREAM_CODEC, MarkerPacket::handle)); + + private ModNetwork() { + } + + static CustomPacketPayload.Type type(String name) { + return new CustomPacketPayload.Type<>(new ResourceLocation(MCHeli.MOD_ID, name)); + } + + public static void sendToServer(CustomPacketPayload message) { + Platform.get().sendToServer(message); + } + + public static void sendToPlayer(ServerPlayer player, CustomPacketPayload message) { + Platform.get().sendToPlayer(player, message); + } + + public static void sendToAll(MinecraftServer server, CustomPacketPayload message) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + sendToPlayer(player, message); + } + } + + /** Sends to every player in {@code level} within {@code range} blocks of the point. */ + public static void sendNear(ServerLevel level, double x, double y, double z, double range, CustomPacketPayload message) { + for (ServerPlayer player : level.players()) { + if (player.distanceToSqr(x, y, z) < range * range) { + sendToPlayer(player, message); + } + } + } + + public record Serverbound(CustomPacketPayload.Type type, + StreamCodec codec, + BiConsumer handler) { + } + + public record Clientbound(CustomPacketPayload.Type type, + StreamCodec codec, + Consumer handler) { + } +} From 45abf71f8ad07a836c24e26191368e6214eabf5c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:55 +0800 Subject: [PATCH 1274/1461] Add network/PackDataPacket.java to the 1.20.6 shared sources --- .../mcheli/network/PackDataPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java new file mode 100644 index 00000000..89c1644d --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: one chunk of a pack's zip. Chunks of a pack arrive in order, so + * {@code offset} only lets the client check that nothing went missing. + * + * @param total length of the whole zip, as in the {@link PackListPacket} + */ +public record PackDataPacket(String hash, int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_data"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackDataPacket::encode, PackDataPacket::decode); + + /** Well under the 1 MiB limit of a clientbound custom payload. */ + public static final int CHUNK_SIZE = 128 * 1024; + + public static void encode(PackDataPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.hash, 64); + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackDataPacket decode(FriendlyByteBuf buf) { + return new PackDataPacket(buf.readUtf(64), buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackDataPacket msg) { + ClientAccess.packs().onPackData(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 9a47b1b42cb6e1848bc730eed930364d54ad5047 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:55 +0800 Subject: [PATCH 1275/1461] Add network/PackListPacket.java to the 1.20.6 shared sources --- .../mcheli/network/PackListPacket.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java new file mode 100644 index 00000000..ad7f0c85 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +import java.util.ArrayList; +import java.util.List; + +/** + * Server to player: the content packs the server uses, in load order. Sent on join and after + * {@code /mcheli reload}. The client requests the packs it has not cached ({@link PackRequestPacket}) + * and then rebuilds its content from exactly this list, so both sides fly the same definitions. + * The imported 1.0.3 content is never listed: every player imports their own copy. + */ +public record PackListPacket(List packs) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_list"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackListPacket::encode, PackListPacket::decode); + + public static final int MAX_PACKS = 256; + + /** + * @param size length of the pack's zip in bytes, which is what {@link PackDataPacket} sends + */ + public record Entry(String id, String version, String hash, int size) { + } + + public static void encode(PackListPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.packs.size()); + for (Entry entry : msg.packs) { + buf.writeUtf(entry.id, 64); + buf.writeUtf(entry.version, 32); + buf.writeUtf(entry.hash, 64); + buf.writeVarInt(entry.size); + } + } + + public static PackListPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > MAX_PACKS) { + throw new DecoderException("Too many content packs: " + count); + } + List packs = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + packs.add(new Entry(buf.readUtf(64), buf.readUtf(32), buf.readUtf(64), buf.readVarInt())); + } + return new PackListPacket(List.copyOf(packs)); + } + + public static void handle(PackListPacket msg) { + ClientAccess.packs().onPackList(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 610962bb5cad5a78af2eabe5313c75a4bb14ffb3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:56 +0800 Subject: [PATCH 1276/1461] Add network/PackRequestPacket.java to the 1.20.6 shared sources --- .../mcheli/network/PackRequestPacket.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java new file mode 100644 index 00000000..50f10f74 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.network; + +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Player to server: send me these packs, by content hash. The server only answers for hashes in + * the list it sent, so a client cannot ask for anything else. + */ +public record PackRequestPacket(List hashes) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_request"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackRequestPacket::encode, PackRequestPacket::decode); + + public static void encode(PackRequestPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.hashes.size()); + for (String hash : msg.hashes) { + buf.writeUtf(hash, 64); + } + } + + public static PackRequestPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > PackListPacket.MAX_PACKS) { + throw new DecoderException("Too many content packs requested: " + count); + } + List hashes = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + hashes.add(buf.readUtf(64)); + } + return new PackRequestPacket(List.copyOf(hashes)); + } + + public static void handle(PackRequestPacket msg, ServerPlayer sender) { + if (sender != null) { + PackSync.request(sender, msg.hashes); + } + } + + @Override + public Type type() { + return TYPE; + } +} From aec02947aa103c068736b10800653592f9848a28 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:56 +0800 Subject: [PATCH 1277/1461] Add network/PackSoundPacket.java to the 1.20.6 shared sources --- .../mcheli/network/PackSoundPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java new file mode 100644 index 00000000..8bac5034 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSoundPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: play a sound a content pack brought, once, at a point. The game's own sound + * packet ends at the sound manager, which only knows the sounds in {@code sounds.json}, and a pack + * has none: it would be silent. See {@code ModSounds}. + */ +public record PackSoundPacket(String sound, double x, double y, double z, float volume, float pitch) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_sound"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSoundPacket::encode, PackSoundPacket::decode); + + private static final int MAX_NAME = 256; + + public static void encode(PackSoundPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.sound, MAX_NAME); + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.volume); + buf.writeFloat(msg.pitch); + } + + public static PackSoundPacket decode(FriendlyByteBuf buf) { + return new PackSoundPacket(buf.readUtf(MAX_NAME), buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readFloat()); + } + + public static void handle(PackSoundPacket msg) { + ClientAccess.packs().onPackSound(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From a4d1495ad336939cd021d7599f0c827bbe72d8c5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:57 +0800 Subject: [PATCH 1278/1461] Add network/PackSubmissions.java to the 1.20.6 shared sources --- .../mcheli/network/PackSubmissions.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java new file mode 100644 index 00000000..0a01b93a --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSubmissions.java @@ -0,0 +1,185 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.content.pack.PendingPacks; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.Util; +import org.jspecify.annotations.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Players offering a server the vehicles they built, for its operators to approve. + *

+ *  Submit in the workshop   ── PackSubmitPacket(chunk) ──▶  until the whole zip is here
+ *  checked off the main thread, kept in mcheli/pending, operators told
+ *  /mcheli approve <pack>    moves it to mcheli/packs and reloads, so every player gets it
+ * 
+ * Nothing a client sends is trusted. Submissions are off until the server's owner turns them on, one + * zip can be no larger than the server allows for a pack, a player sends one at a time with a pause + * in between, chunks out of order end the transfer, and a few transfers run at once at most. The + * bytes stay in memory only while they arrive; {@link PendingPacks} checks them as it would any pack. + * + *

Everything here runs on the server thread except the check, which reads the whole pack. + */ +public final class PackSubmissions { + private static final int MAX_UPLOADS = 4; + private static final long COOLDOWN_MILLIS = 30_000L; + + private static final Map UPLOADS = new HashMap<>(); + private static final Map LAST_SUBMISSION = new HashMap<>(); + + private PackSubmissions() { + } + + static void receive(ServerPlayer player, PackSubmitPacket packet) { + UUID id = player.getUUID(); + if (packet.offset() == 0) { + UPLOADS.remove(id); + Component refusal = refusal(player, packet.total()); + if (refusal != null) { + player.sendSystemMessage(refusal); + return; + } + UPLOADS.put(id, new ByteArrayOutputStream()); + } + ByteArrayOutputStream upload = UPLOADS.get(id); + // No upload means it was refused above and the rest of its chunks are still on their way. + if (upload == null) { + return; + } + long end = (long) upload.size() + packet.data().length; + if (packet.offset() != upload.size() || packet.total() > ServerConfig.maxPackBytes() || end > packet.total()) { + UPLOADS.remove(id); + player.sendSystemMessage(Component.translatable("message.mcheli.submit.failed", "chunks arrived out of order")); + return; + } + upload.write(packet.data(), 0, packet.data().length); + if (end == packet.total()) { + UPLOADS.remove(id); + LAST_SUBMISSION.put(id, System.currentTimeMillis()); + check(player, upload.toByteArray()); + } + } + + private static @Nullable Component refusal(ServerPlayer player, int total) { + if (!ServerConfig.allowSubmissions()) { + return Component.translatable("message.mcheli.submit.off"); + } + if (total <= 0 || total > ServerConfig.maxPackBytes()) { + return Component.translatable("message.mcheli.submit.too_large", ServerConfig.maxPackBytes() >> 20); + } + if (UPLOADS.size() >= MAX_UPLOADS) { + return Component.translatable("message.mcheli.submit.busy"); + } + long now = System.currentTimeMillis(); + LAST_SUBMISSION.values().removeIf(time -> now - time >= COOLDOWN_MILLIS); + return LAST_SUBMISSION.containsKey(player.getUUID()) ? Component.translatable("message.mcheli.submit.busy") : null; + } + + private static void check(ServerPlayer player, byte[] zip) { + MinecraftServer server = player.level().getServer(); + UUID id = player.getUUID(); + String name = player.getScoreboardName(); + CompletableFuture.supplyAsync(() -> { + try { + return MCHeli.packs().pending().store(zip, name, id); + } catch (IOException | PackException | RuntimeException e) { + throw new java.util.concurrent.CompletionException(e); + } + }, Util.ioPool()).whenComplete((submission, failure) -> server.execute(() -> { + if (failure != null) { + Throwable cause = failure.getCause() != null ? failure.getCause() : failure; + MCHeli.LOGGER.info("{} sent a pack that was refused: {}", name, cause.toString()); + tell(server, id, Component.translatable("message.mcheli.submit.failed", cause.getMessage() != null ? cause.getMessage() : cause.toString())); + } else { + arrived(server, submission); + } + })); + } + + private static void arrived(MinecraftServer server, PendingPacks.Submission submission) { + MCHeli.LOGGER.info("{} submitted content pack {} {} ({})", submission.submitter(), submission.id(), + submission.manifest().version(), ContentPack.shortHash(submission.hash())); + // Even with approval off, a pack taking over one that is in use waits for a person. + boolean replaces = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (!ServerConfig.requireApproval() && !replaces) { + try { + install(server, submission); + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not install submitted content pack {}", submission.id(), e); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.failed", e.getMessage())); + } + return; + } + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.waiting", submission.id())); + Component note = Component.translatable("message.mcheli.submit.to_operators", submission.submitter(), submission.id(), submission.manifest().version()); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (PackSync.seesProblems(player)) { + player.sendSystemMessage(note); + } + } + } + + /** + * Puts a waiting pack in use: into {@code mcheli/packs}, then a reload that sends every player + * the new list. + * + * @return whether the pack is in use now; the server's allow list or size limits may leave it out + */ + public static boolean install(MinecraftServer server, PendingPacks.Submission submission) throws IOException, PackException { + MCHeli.packs().pending().install(submission, MCHeli.packs().folder()); + ContentReload.reload(server); + MCHeli.LOGGER.info("Content pack {} from {} approved", submission.id(), submission.submitter()); + boolean live = MCHeli.packs().active().stream().anyMatch(pack -> pack.id().equals(submission.id())); + if (live) { + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.approved", submission.id())); + } + return live; + } + + /** Throws a waiting pack away and tells the player who sent it, if they are here. */ + public static void reject(MinecraftServer server, PendingPacks.Submission submission) throws IOException { + MCHeli.packs().pending().remove(submission); + MCHeli.LOGGER.info("Content pack {} from {} rejected", submission.id(), submission.submitter()); + tell(server, submission.submitterId(), Component.translatable("message.mcheli.submit.rejected", submission.id())); + } + + private static void tell(MinecraftServer server, UUID player, Component message) { + ServerPlayer online = server.getPlayerList().getPlayer(player); + if (online != null) { + online.sendSystemMessage(message); + } + } + + /** An operator joining hears what is waiting for them. */ + static void onLogin(ServerPlayer player) { + if (!PackSync.seesProblems(player)) { + return; + } + try { + List waiting = MCHeli.packs().pending().list(); + if (!waiting.isEmpty()) { + player.sendSystemMessage(Component.translatable("message.mcheli.submit.waiting_for_you", waiting.size())); + } + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not list the submitted content packs", e); + } + } + + static void onLogout(ServerPlayer player) { + UPLOADS.remove(player.getUUID()); + } +} From 57ef1a50ee78e1b3a94a5f5ac7d6a5a1ccc85b61 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:57 +0800 Subject: [PATCH 1279/1461] Add network/PackSubmitPacket.java to the 1.20.6 shared sources --- .../mcheli/network/PackSubmitPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java new file mode 100644 index 00000000..d142e700 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSubmitPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Player to server: one chunk of a pack the player made, offered to the server's operators. See + * {@link PackSubmissions}, which treats all of this as untrusted. + * + * @param offset where this chunk goes; 0 starts a new submission + * @param total length of the whole zip + */ +public record PackSubmitPacket(int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_submit"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackSubmitPacket::encode, PackSubmitPacket::decode); + + /** Under the 32 KiB limit of a serverbound custom payload, which is far lower than the other way. */ + public static final int CHUNK_SIZE = 30 * 1024; + + public static void encode(PackSubmitPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackSubmitPacket decode(FriendlyByteBuf buf) { + return new PackSubmitPacket(buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackSubmitPacket msg, ServerPlayer sender) { + PackSubmissions.receive(sender, msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 3554824d373bec95d533ee4b1fda3eb5457012d9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:58 +0800 Subject: [PATCH 1280/1461] Add network/PackSync.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/network/PackSync.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSync.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSync.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSync.java new file mode 100644 index 00000000..e522f525 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PackSync.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * The server half of the content pack sync. + *

+ *  player joins          ── PackListPacket(id, version, hash, size per pack) ──▶ client
+ *                        ◀── PackRequestPacket(hashes it has not cached) ────────
+ *  every tick            ── PackDataPacket(chunk) ──▶  until each pack is sent
+ * 
+ * Each player gets {@link #CHUNKS_PER_TICK} chunks a tick (about 5 MB a second), one pack after + * the other, so a download never crowds out the game's own packets. A pack's bytes are read when + * its turn comes and dropped once sent. Everything here runs on the server thread. + */ +public final class PackSync { + private static final int CHUNKS_PER_TICK = 2; + + private static final Map> TRANSFERS = new HashMap<>(); + + private PackSync() { + } + + /** Sends a joining player the pack list, and operators any pack problems. */ + public static void onLogin(ServerPlayer player) { + ModNetwork.sendToPlayer(player, list()); + if (seesProblems(player)) { + MCHeli.packs().reportProblems(player::sendSystemMessage); + } + PackSubmissions.onLogin(player); + } + + public static void onLogout(ServerPlayer player) { + TRANSFERS.remove(player.getUUID()); + PackSubmissions.onLogout(player); + } + + /** The packs the server uses now, as the client should load them. */ + public static PackListPacket list() { + List entries = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + try { + entries.add(new PackListPacket.Entry(pack.id(), pack.manifest().version(), pack.hash(), (int) pack.archiveSize())); + } catch (IOException e) { + MCHeli.LOGGER.error("Content pack {} cannot be sent to players", pack, e); + } + } + return new PackListPacket(List.copyOf(entries)); + } + + /** After a reload: drops transfers in progress and sends every player the new list. */ + public static void sendListToAll(MinecraftServer server) { + TRANSFERS.clear(); + ModNetwork.sendToAll(server, list()); + } + + /** Operators, and the owner of a single player or LAN world, are told what is wrong with the packs. */ + public static boolean seesProblems(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + return server.isSingleplayerOwner(player.getGameProfile()) + || player.createCommandSourceStack().hasPermission(Commands.LEVEL_GAMEMASTERS); + } + + static void request(ServerPlayer player, List hashes) { + Deque queue = TRANSFERS.computeIfAbsent(player.getUUID(), id -> new ArrayDeque<>()); + for (String hash : hashes) { + ContentPack pack = find(hash); + if (pack == null) { + MCHeli.LOGGER.debug("{} asked for content pack {}, which the server does not use", player.getScoreboardName(), hash); + continue; + } + if (queue.stream().noneMatch(t -> t.pack == pack)) { + queue.add(new Transfer(pack)); + } + } + } + + private static @Nullable ContentPack find(String hash) { + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.hash().equals(hash)) { + return pack; + } + } + return null; + } + + public static void pump(MinecraftServer server) { + Iterator>> it = TRANSFERS.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey()); + Deque queue = entry.getValue(); + if (player == null || queue.isEmpty()) { + it.remove(); + continue; + } + for (int sent = 0; sent < CHUNKS_PER_TICK && !queue.isEmpty(); sent++) { + Transfer transfer = queue.peek(); + PackDataPacket chunk = transfer.next(); + if (chunk == null) { + queue.poll(); + continue; + } + ModNetwork.sendToPlayer(player, chunk); + if (transfer.done()) { + queue.poll(); + } + } + } + } + + /** One pack on its way to one player. */ + private static final class Transfer { + private final ContentPack pack; + private byte @Nullable [] data; + private int offset; + + Transfer(ContentPack pack) { + this.pack = pack; + } + + /** The next chunk, or {@code null} when the pack could not be read. */ + @Nullable PackDataPacket next() { + if (data == null) { + try { + data = pack.archive(); + } catch (IOException e) { + MCHeli.LOGGER.error("Could not read content pack {} to send it", pack, e); + return null; + } + } + int end = Math.min(offset + PackDataPacket.CHUNK_SIZE, data.length); + PackDataPacket chunk = new PackDataPacket(pack.hash(), offset, data.length, Arrays.copyOfRange(data, offset, end)); + offset = end; + return chunk; + } + + boolean done() { + return data != null && offset >= data.length; + } + } +} From 10dc4b78d1e16b65c7369819f3f64d380c6bbe58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:58 +0800 Subject: [PATCH 1281/1461] Add network/PilotStatePacket.java to the 1.20.6 shared sources --- .../mcheli/network/PilotStatePacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java new file mode 100644 index 00000000..6426f060 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Sent by the pilot every tick. Position, yaw and pitch already travel through the vanilla + * vehicle move packet; this carries the parts vanilla does not know about. + * + * @param keys the pilot's key state as packed by {@link AircraftEntity.ControlInput#write()}, so + * other players see wheels steer and tracks turn + */ +public record PilotStatePacket(int entityId, float throttle, float reverse, float roll, boolean hovering, byte keys) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pilot_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PilotStatePacket::encode, PilotStatePacket::decode); + + public static void encode(PilotStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeFloat(msg.throttle); + buf.writeFloat(msg.reverse); + buf.writeFloat(msg.roll); + buf.writeBoolean(msg.hovering); + buf.writeByte(msg.keys); + } + + public static PilotStatePacket decode(FriendlyByteBuf buf) { + return new PilotStatePacket(buf.readVarInt(), buf.readFloat(), buf.readFloat(), buf.readFloat(), buf.readBoolean(), buf.readByte()); + } + + public static void handle(PilotStatePacket msg, ServerPlayer sender) { + if (sender == null) { + return; + } + // Only the player actually flying this aircraft may drive it. + if (sender.getVehicle() instanceof AircraftEntity aircraft + && aircraft.getId() == msg.entityId + && aircraft.getControllingPassenger() == sender + && Float.isFinite(msg.throttle) + && Float.isFinite(msg.reverse) + && Float.isFinite(msg.roll)) { + aircraft.acceptPilotState(msg.throttle, msg.reverse, msg.roll, msg.hovering, msg.keys); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 0ec76c6182adeef793948040a728dcb86a78431a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:58 +0800 Subject: [PATCH 1282/1461] Add network/SpotPacket.java to the 1.20.6 shared sources --- .../mcheli/network/SpotPacket.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java new file mode 100644 index 00000000..1ee72286 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/SpotPacket.java @@ -0,0 +1,78 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.weapon.Spotting; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; + +/** + * The attack key through a rangefinder, {@code MCH_PacketIndSpotEntity}: spot players and + * vehicles, spot mobs, or mark a point for the team. Spotting uses a charge; marking is free. + */ +public record SpotPacket(Mode mode) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("spot"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(SpotPacket::encode, SpotPacket::decode); + + /** The rangefinder's modes, in the order its mode key steps through them. */ + public enum Mode { + PLAYERS_AND_VEHICLES(Spotting.PLAYERS | Spotting.GROUND_VEHICLES | Spotting.HELICOPTERS | Spotting.PLANES), + MOBS(Spotting.OTHERS | Spotting.MONSTERS), + MARK_POINT(0); + + final int filter; + + Mode(int filter) { + this.filter = filter; + } + } + + public static void encode(SpotPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.mode.ordinal()); + } + + public static SpotPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Mode[] modes = Mode.values(); + if (ordinal < 0 || ordinal >= modes.length) { + throw new IllegalArgumentException("Unknown spot mode " + ordinal); + } + return new SpotPacket(modes[ordinal]); + } + + public static void handle(SpotPacket msg, ServerPlayer sender) { + if (sender == null || !RangefinderItem.canUse(sender)) { + return; + } + ItemStack stack = sender.getItemInHand(InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + stack = sender.getItemInHand(InteractionHand.OFF_HAND); + if (!(stack.getItem() instanceof RangefinderItem)) { + return; + } + } + boolean done; + if (msg.mode == Mode.MARK_POINT) { + done = Spotting.markPoint(sender); + } else if (RangefinderItem.charges(stack) > 0) { + if (!sender.getAbilities().instabuild) { + // An empty rangefinder stays: it only needs redstone, unlike a worn out tool. + stack.setDamageValue(stack.getDamageValue() + 1); + } + done = Spotting.spot(sender, msg.mode.filter); + } else { + done = false; + } + ModSounds.playAt(sender, done ? "pi" : "ng", 1.0F, 1.0F); + } + + @Override + public Type type() { + return TYPE; + } +} From 74be919e7a7b9cf41b45e91b959370bff253f95e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:59 +0800 Subject: [PATCH 1283/1461] Add network/TeamPacket.java to the 1.20.6 shared sources --- .../mcheli/network/TeamPacket.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java new file mode 100644 index 00000000..14b2db42 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/TeamPacket.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A button of the team manager, {@code MCH_PacketIndMultiplayCommand}. Operators only; the + * server checks again. + * + * @param name the new team's name, for {@link Action#CREATE_TEAM} + * @param color the new team's colour, as a vanilla team colour id + */ +public record TeamPacket(Action action, String name, int color, boolean friendlyFire) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("team"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TeamPacket::encode, TeamPacket::decode); + + public enum Action { + PVP, + SHUFFLE, + CREATE_TEAM, + JUMP_TO_SPAWN, + DESTROY_ALL + } + + public TeamPacket(Action action) { + this(action, "", 0, false); + } + + public static void encode(TeamPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.action.ordinal()); + buf.writeUtf(msg.name, 16); + buf.writeVarInt(msg.color); + buf.writeBoolean(msg.friendlyFire); + } + + public static TeamPacket decode(FriendlyByteBuf buf) { + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown team action " + ordinal); + } + return new TeamPacket(actions[ordinal], buf.readUtf(16), buf.readVarInt(), buf.readBoolean()); + } + + public static void handle(TeamPacket msg, ServerPlayer sender) { + if (sender == null || !TeamManager.mayManage(sender)) { + return; + } + switch (msg.action) { + case PVP -> TeamManager.togglePvp(sender); + case SHUFFLE -> TeamManager.shuffle(sender); + case CREATE_TEAM -> TeamManager.create(sender, msg.name, msg.color, msg.friendlyFire); + case JUMP_TO_SPAWN -> TeamManager.jumpToSpawn(sender); + case DESTROY_ALL -> TeamManager.destroyAll(sender); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 69197e4caccbbc695fc963b53eea2e38541caf6a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:29:59 +0800 Subject: [PATCH 1284/1461] Add network/TestFlightPacket.java to the 1.20.6 shared sources --- .../mcheli/network/TestFlightPacket.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java new file mode 100644 index 00000000..db12a390 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/TestFlightPacket.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.workshop.TestFlight; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** The workshop's Test flight button: fly the vehicle just saved. The server checks who is asking. */ +public record TestFlightPacket(String vehicle) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("test_flight"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(TestFlightPacket::encode, TestFlightPacket::decode); + private static final int MAX_NAME = 64; + + public static void encode(TestFlightPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.vehicle, MAX_NAME); + } + + public static TestFlightPacket decode(FriendlyByteBuf buf) { + return new TestFlightPacket(buf.readUtf(MAX_NAME)); + } + + public static void handle(TestFlightPacket msg, ServerPlayer sender) { + if (sender != null) { + TestFlight.start(sender, msg.vehicle); + } + } + + @Override + public Type type() { + return TYPE; + } +} From eb86080529c49959c34db34bdaf7e09479e61465 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:00 +0800 Subject: [PATCH 1285/1461] Add network/WeaponStatePacket.java to the 1.20.6 shared sources --- .../mcheli/network/WeaponStatePacket.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java new file mode 100644 index 00000000..292f643e --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to crew: the magazine and reserve of one weapon after it fired, reloaded or was + * resupplied. The original's {@code MCH_PacketNotifyAmmoNum}. + */ +public record WeaponStatePacket(int entityId, int weapon, int ammo, int rest) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("weapon_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(WeaponStatePacket::encode, WeaponStatePacket::decode); + + public static void encode(WeaponStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.ammo); + buf.writeVarInt(msg.rest); + } + + public static WeaponStatePacket decode(FriendlyByteBuf buf) { + return new WeaponStatePacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(WeaponStatePacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player != null && player.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.applyAmmo(msg.weapon, msg.ammo, msg.rest); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 6d6bc1f1b6f9a98a3d883b75cdebe876677beafc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:00 +0800 Subject: [PATCH 1286/1461] Add particle/GlowParticleOptions.java to the 1.20.6 shared sources --- .../mcheli/particle/GlowParticleOptions.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java new file mode 100644 index 00000000..22f73709 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A soft ball of light blended on top of the scene: the flash of an explosion, a burning wreck, an + * ember. Spawned on the client only, but the codecs let {@code /particle mcheli:glow} show it too. + * + * @param color RGB of the light + * @param size radius in blocks at its largest + * @param lifetime ticks until it has faded out + * @param flicker whether it wavers like a fire rather than fading smoothly like a flash + */ +public record GlowParticleOptions(int color, float size, int lifetime, boolean flicker) implements ParticleOptions { + /** White hot at the centre of a blast. */ + public static final int FLASH = 0xFFE2B0; + /** Burning fuel and the fire inside the smoke. */ + public static final int FIRE = 0xFF8A2E; + /** Embers and the deeper red at the edge of a fireball. */ + public static final int EMBER = 0xFF5A1E; + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + Codec.INT.fieldOf("color").forGetter(GlowParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(GlowParticleOptions::size), + ExtraCodecs.POSITIVE_INT.fieldOf("lifetime").forGetter(GlowParticleOptions::lifetime), + Codec.BOOL.optionalFieldOf("flicker", false).forGetter(GlowParticleOptions::flicker)) + .apply(i, GlowParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, GlowParticleOptions::color, + ByteBufCodecs.FLOAT, GlowParticleOptions::size, + ByteBufCodecs.VAR_INT, GlowParticleOptions::lifetime, + ByteBufCodecs.BOOL, GlowParticleOptions::flicker, + GlowParticleOptions::new); + + public static GlowParticleOptions flash(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, false); + } + + public static GlowParticleOptions fire(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, true); + } + + @Override + public ParticleType getType() { + return ModParticles.GLOW.get(); + } +} From 4fdbf3d4c0521d8c98c384834687538879ff6d53 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:01 +0800 Subject: [PATCH 1287/1461] Add particle/SmokeParticleOptions.java to the 1.20.6 shared sources --- .../mcheli/particle/SmokeParticleOptions.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java new file mode 100644 index 00000000..b8151034 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/particle/SmokeParticleOptions.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.particle; + +import net.minecraft.util.ExtraCodecs; +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; + +/** + * A puff of coloured smoke that swells and pales as it drifts, {@code MCH_EntityParticleSmoke} + * with {@code toWhite} set. The smoke grenades use it to mark a spot. + * + * @param color RGB the puff starts with + * @param size the original's particle size; a puff ends up about {@code size / 5} blocks across + * @param lifetime ticks the puff lasts, or 0 for the grenade smoke's three to five seconds + */ +public record SmokeParticleOptions(int color, float size, int lifetime) implements ParticleOptions { + public SmokeParticleOptions(int color, float size) { + this(color, size, 0); + } + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + Codec.INT.fieldOf("color").forGetter(SmokeParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(SmokeParticleOptions::size), + ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("lifetime", 0).forGetter(SmokeParticleOptions::lifetime)) + .apply(i, SmokeParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, SmokeParticleOptions::color, + ByteBufCodecs.FLOAT, SmokeParticleOptions::size, + ByteBufCodecs.VAR_INT, SmokeParticleOptions::lifetime, + SmokeParticleOptions::new); + + @Override + public ParticleType getType() { + return ModParticles.SMOKE.get(); + } +} From 6f9100a79e4238d6f72d24db53cd75d2d0c95da5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:01 +0800 Subject: [PATCH 1288/1461] Add platform/Platform.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/platform/Platform.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Platform.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Platform.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Platform.java new file mode 100644 index 00000000..e951f496 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Platform.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; + +import java.nio.file.Path; +import java.util.Objects; + +/** + * What the shared code needs from the mod loader. Each loader module installs its implementation + * first thing during mod construction, before anything else of ours runs. + * + *

Kept small on purpose: registration and events go the other way, with the loader module + * calling into the shared code (see {@code Registration} and the {@code *Hooks} classes). + */ +public interface Platform { + + static Platform get() { + Platform platform = Holder.instance; + if (platform == null) { + throw new IllegalStateException("No mod loader platform installed yet"); + } + return platform; + } + + static void install(Platform platform) { + if (Holder.instance != null) { + throw new IllegalStateException("Platform already installed: " + Holder.instance.loaderName()); + } + Holder.instance = Objects.requireNonNull(platform); + } + + /** {@code forge}, {@code neoforge} or {@code fabric}, for logs and {@code /mcheli status}. */ + String loaderName(); + + /** The game directory, which holds the {@code mcheli} folder. */ + Path gameDir(); + + /** The config directory, which holds the client config. */ + Path configDir(); + + /** Our version as the loader read it from the mod metadata. */ + String modVersion(); + + /** Whether this is a physical client, as opposed to a dedicated server. */ + boolean isClient(); + + void sendToServer(CustomPacketPayload payload); + + void sendToPlayer(ServerPlayer player, CustomPacketPayload payload); + + /** + * A builder for one of our creative tabs, placed after {@code after}. Vanilla's builder is not + * public on every loader and only Forge and NeoForge can order tabs. + */ + CreativeModeTab.Builder creativeTab(ResourceKey after); + + final class Holder { + private static volatile Platform instance; + + private Holder() { + } + } +} From f8c16343cfe5e24668aac6dbcf48462a17148a1e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:02 +0800 Subject: [PATCH 1289/1461] Add platform/Registered.java to the 1.20.6 shared sources --- .../mcheli/platform/Registered.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Registered.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Registered.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Registered.java new file mode 100644 index 00000000..e90f274f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Registered.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Supplier; + +/** + * A registry entry the shared code declares and the loader module registers, like Forge's + * {@code RegistryObject}. {@link #get()} works once the loader has registered it. + */ +public final class Registered implements Supplier { + private final ResourceLocation id; + private volatile T value; + + Registered(ResourceLocation id) { + this.id = id; + } + + public ResourceLocation id() { + return id; + } + + @Override + public T get() { + T v = value; + if (v == null) { + throw new IllegalStateException("Registry entry " + id + " is not registered yet"); + } + return v; + } + + void bind(T value) { + this.value = value; + } +} From 77d5c66265a93bfbb20d10a8cf500355fd661ebd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:02 +0800 Subject: [PATCH 1290/1461] Add platform/Registration.java to the 1.20.6 shared sources --- .../mcheli/platform/Registration.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Registration.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Registration.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Registration.java new file mode 100644 index 00000000..dee79e8c --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/platform/Registration.java @@ -0,0 +1,80 @@ +package com.jnhprojects.mcheli.platform; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.ResourceKey; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Every vanilla registry entry the mod adds. The shared registry classes declare their entries + * here during mod construction; the loader module then registers them its own way: Forge and + * NeoForge from their register event, Fabric straight away. + */ +public final class Registration { + private static final List> PENDING = new ArrayList<>(); + private static final Set>> REGISTRIES = new LinkedHashSet<>(); + + private Registration() { + } + + /** + * Declares an entry. + * + * @param factory makes the value; it gets the entry's key for {@code Item.Properties#setId} + * and the like + */ + public static synchronized Registered register(ResourceKey> registry, String name, + Function, T> factory) { + ResourceLocation id = new ResourceLocation(MCHeli.MOD_ID, name); + Registered entry = new Registered<>(id); + PENDING.add(new Pending<>(registry, entry, factory)); + REGISTRIES.add(registry); + return entry; + } + + /** The registries with entries of ours, in the order they were first used. */ + public static synchronized List>> registries() { + return List.copyOf(REGISTRIES); + } + + /** + * A key from {@link #registries()} with its element type filled in, for the loader's generic + * register calls. The type is whatever the caller needs; entries are only ever handed to the + * registry they were declared for. + */ + @SuppressWarnings("unchecked") + public static ResourceKey> typed(ResourceKey> registry) { + return (ResourceKey>) registry; + } + + /** + * Creates our entries for one registry, in declaration order, and hands each to the loader. + * Called once per registry. + */ + public static synchronized void registerAll(ResourceKey> registry, BiConsumer sink) { + for (Pending pending : PENDING) { + if (pending.registry.equals(registry)) { + @SuppressWarnings("unchecked") + Pending ours = (Pending) pending; + ours.create(sink); + } + } + } + + private record Pending(ResourceKey> registry, Registered entry, + Function, T> factory) { + @SuppressWarnings("unchecked") + void create(BiConsumer sink) { + T value = factory.apply(ResourceKey.create((ResourceKey>) registry, entry.id())); + entry.bind(value); + sink.accept(entry.id(), value); + } + } +} From 362e56e6cceae0fa2cb76851290d7ce28df282f2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:02 +0800 Subject: [PATCH 1291/1461] Add registry/ModBlockEntities.java to the 1.20.6 shared sources --- .../mcheli/registry/ModBlockEntities.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java new file mode 100644 index 00000000..28270640 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModBlockEntities.java @@ -0,0 +1,21 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlockEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; + + +/** Our block entities: the drafting table's, which draws its model. */ +public final class ModBlockEntities { + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.BLOCK_ENTITY_TYPE, + "drafting_table", key -> BlockEntityType.Builder.of(DraftingTableBlockEntity::new, ModBlocks.DRAFTING_TABLE.get()).build(null)); + + private ModBlockEntities() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 953f6fe7e9b624606db3f18fbb2e28b341f84d25 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:03 +0800 Subject: [PATCH 1292/1461] Add registry/ModBlocks.java to the 1.20.6 shared sources --- .../mcheli/registry/ModBlocks.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java new file mode 100644 index 00000000..c833e532 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModBlocks.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.block.DraftingTableBlock; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockBehaviour; + +/** Our blocks: the drafting table, lit or not. */ +public final class ModBlocks { + public static final Registered DRAFTING_TABLE = Registration.register(Registries.BLOCK, "drafting_table", + key -> new DraftingTableBlock(BlockBehaviour.Properties.of() + .strength(0.2F) + .sound(SoundType.METAL) + .noOcclusion() + .lightLevel(state -> state.getValue(DraftingTableBlock.LIT) ? 15 : 0))); + + private ModBlocks() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 6a94d2e09396043f3fd2868f5dd4c814a31875e3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:03 +0800 Subject: [PATCH 1293/1461] Add registry/ModCreativeTabs.java to the 1.20.6 shared sources --- .../mcheli/registry/ModCreativeTabs.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java new file mode 100644 index 00000000..870aa8b8 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java @@ -0,0 +1,72 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** + * One tab per vehicle kind, listing a stack for each loaded definition of that kind, and one for + * the equipment and throwables. Those lists are read from the content registry whenever a tab is + * built, so reloaded content shows up. + */ +public final class ModCreativeTabs { + public static final Registered HELICOPTERS = register("helicopters", "helicopter", "ah-64", + CreativeModeTabs.SPAWN_EGGS); + public static final Registered PLANES = register("planes", "plane", "f-35b", key(HELICOPTERS)); + public static final Registered TANKS = register("tanks", "tank", "m1a2", key(PLANES)); + public static final Registered VEHICLES = register("vehicles", "vehicle", "mk15", key(TANKS)); + public static final Registered EQUIPMENT = Registration.register(Registries.CREATIVE_MODE_TAB, "equipment", + key -> Platform.get().creativeTab(key(VEHICLES)) + .title(Component.translatable("itemGroup.mcheli.equipment")) + .icon(() -> new ItemStack(ModItems.WRENCH.get())) + .displayItems((params, output) -> { + ModItems.EQUIPMENT.forEach(item -> output.accept(item.get())); + MCHeli.content().throwables().forEach(def -> output.accept(ThrowableItem.stackOf(def))); + }) + .build()); + + private ModCreativeTabs() { + } + + private static ResourceKey key(Registered tab) { + return ResourceKey.create(Registries.CREATIVE_MODE_TAB, tab.id()); + } + + private static Registered register(String name, String kind, String icon, ResourceKey after) { + return Registration.register(Registries.CREATIVE_MODE_TAB, name, key -> Platform.get().creativeTab(after) + .title(Component.translatable("itemGroup.mcheli." + name)) + .icon(() -> { + AircraftDefinition def = MCHeli.content().any(icon); + return def != null ? AircraftItem.stackOf(def) : new ItemStack(ModItems.forKind(kind)); + }) + .displayItems((params, output) -> { + for (AircraftDefinition def : MCHeli.content().all()) { + if (def.kind().equals(kind)) { + output.accept(AircraftItem.stackOf(def)); + } + } + }) + .build()); + } + + /** Our tabs, for rebuilding their contents after a content reload. */ + public static List all() { + return List.of(HELICOPTERS.get(), PLANES.get(), TANKS.get(), VEHICLES.get(), EQUIPMENT.get()); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 807e430939ab17389c403bf9f5ef7c7bf03431df Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:04 +0800 Subject: [PATCH 1294/1461] Add registry/ModDataComponents.java to the 1.20.6 shared sources --- .../mcheli/registry/ModDataComponents.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java new file mode 100644 index 00000000..377a5783 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java @@ -0,0 +1,34 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.Codec; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.codec.ByteBufCodecs; + +public final class ModDataComponents { + /** + * The definition a vehicle item places, such as {@code ah-64}. Saved with the stack and sent + * to clients; the item itself only says which kind of vehicle it is. + */ + public static final Registered> VEHICLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "vehicle", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + /** The definition a throwable item throws, such as {@code m18_red}. */ + public static final Registered> THROWABLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "throwable", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + private ModDataComponents() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 3e1761fa889456fa6263486d8c0e6c5e82bc7c04 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:04 +0800 Subject: [PATCH 1295/1461] Add registry/ModEntities.java to the 1.20.6 shared sources --- .../mcheli/registry/ModEntities.java | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java new file mode 100644 index 00000000..bc6526db --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java @@ -0,0 +1,170 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ChainEntity; +import com.jnhprojects.mcheli.entity.ContainerEntity; +import com.jnhprojects.mcheli.entity.GltdEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.ParachuteEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.RappelEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.ThrowableEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; + +/** + * One entity type per vehicle kind. The concrete vehicle is stored on the entity, the same approach + * the original took, so adding content never changes the entity registry. + */ +public final class ModEntities { + public static final Registered> HELICOPTER = register("helicopter", HelicopterEntity::new); + public static final Registered> PLANE = register("plane", PlaneEntity::new); + public static final Registered> TANK = register("tank", TankEntity::new); + public static final Registered> VEHICLE = register("vehicle", GroundVehicleEntity::new); + + public static final Registered> BULLET = projectile("bullet", BulletEntity::new); + public static final Registered> ROCKET = projectile("rocket", RocketEntity::new); + public static final Registered> BOMB = projectile("bomb", BombEntity::new); + public static final Registered> AA_MISSILE = projectile("aa_missile", AAMissileEntity::new); + public static final Registered> AS_MISSILE = projectile("as_missile", ASMissileEntity::new); + public static final Registered> AT_MISSILE = projectile("at_missile", ATMissileEntity::new); + public static final Registered> TV_MISSILE = projectile("tv_missile", TvMissileEntity::new); + public static final Registered> TORPEDO = projectile("torpedo", TorpedoEntity::new); + public static final Registered> MARKER_ROCKET = projectile("marker_rocket", MarkerRocketEntity::new); + public static final Registered> DISPENSED_ITEM = projectile("dispensed_item", DispensedItemEntity::new); + public static final Registered> FLARE = Registration.register(Registries.ENTITY_TYPE, "flare", key -> EntityType.Builder + .of(FlareEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(2) + .fireImmune() + .noSave() + .build(key.location().toString())); + public static final Registered> A10 = Registration.register(Registries.ENTITY_TYPE, "a10", key -> EntityType.Builder + .of(A10Entity::new, MobCategory.MISC) + .sized(5.0F, 3.0F) + .clientTrackingRange(32) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key.location().toString())); + + public static final Registered> PARACHUTE = Registration.register(Registries.ENTITY_TYPE, "parachute", key -> EntityType.Builder + .of(ParachuteEntity::new, MobCategory.MISC) + .sized(1.5F, 0.6F) + .clientTrackingRange(10) + .updateInterval(1) + .build(key.location().toString())); + + public static final Registered> THROWABLE = Registration.register(Registries.ENTITY_TYPE, "throwable", key -> EntityType.Builder + .of(ThrowableEntity::new, MobCategory.MISC) + .sized(0.25F, 0.25F) + .clientTrackingRange(8) + .updateInterval(2) + .noSave() + .build(key.location().toString())); + + public static final Registered> GLTD = Registration.register(Registries.ENTITY_TYPE, "gltd", key -> EntityType.Builder + .of(GltdEntity::new, MobCategory.MISC) + .sized(0.5F, 0.5F) + .clientTrackingRange(10) + .updateInterval(3) + .build(key.location().toString())); + + public static final Registered> CONTAINER = Registration.register(Registries.ENTITY_TYPE, "container", key -> EntityType.Builder + .of(ContainerEntity::new, MobCategory.MISC) + .sized(2.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key.location().toString())); + + public static final Registered> CHAIN = Registration.register(Registries.ENTITY_TYPE, "chain", key -> EntityType.Builder + .of(ChainEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(2) + .fireImmune() + .build(key.location().toString())); + + public static final Registered> RAPPEL = Registration.register(Registries.ENTITY_TYPE, "rappel", key -> EntityType.Builder + .of(RappelEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(10) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key.location().toString())); + + public static final Registered> UAV_STATION = Registration.register(Registries.ENTITY_TYPE, "uav_station", key -> EntityType.Builder + .of(UavStationEntity::new, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(10) + .updateInterval(3) + .fireImmune() + .build(key.location().toString())); + + private ModEntities() { + } + + /** Projectiles are a metre cube like the original's, and never saved. */ + private static Registered> projectile(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key.location().toString())); + } + + private static Registered> register(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .build(key.location().toString())); + } + + /** The entity type that hosts a definition of this kind. */ + public static EntityType typeFor(AircraftDefinition def) { + if (def instanceof HelicopterDefinition) { + return HELICOPTER.get(); + } + if (def instanceof PlaneDefinition) { + return PLANE.get(); + } + if (def instanceof TankDefinition) { + return TANK.get(); + } + return VEHICLE.get(); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 07114bcf696558da1eb1223183016ff022ee965b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:05 +0800 Subject: [PATCH 1296/1461] Add registry/ModItems.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/registry/ModItems.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java new file mode 100644 index 00000000..ca2691f2 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java @@ -0,0 +1,102 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.entity.UavStationEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.ChainItem; +import com.jnhprojects.mcheli.item.FuelItem; +import com.jnhprojects.mcheli.item.LauncherItem; +import com.jnhprojects.mcheli.item.LegacyAircraftItem; +import com.jnhprojects.mcheli.item.ParachuteItem; +import com.jnhprojects.mcheli.item.PlacedEntityItem; +import com.jnhprojects.mcheli.item.RangefinderItem; +import com.jnhprojects.mcheli.item.ThrowableItem; +import com.jnhprojects.mcheli.item.UavStationItem; +import com.jnhprojects.mcheli.item.WrenchItem; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; + +import java.util.List; +import java.util.Map; + +/** + * One item per vehicle kind; the stack says which vehicle (see {@link AircraftItem}). The 1.0.0 + * items, one per vehicle, are kept for the content present at startup so old worlds keep them. The + * equipment (fuel can, wrench, parachute and so on) has one item each, and the throwables share one. + */ +public final class ModItems { + public static final Registered HELICOPTER = kind("helicopter"); + public static final Registered PLANE = kind("plane"); + public static final Registered TANK = kind("tank"); + public static final Registered VEHICLE = kind("vehicle"); + + public static final Registered FUEL = Registration.register(Registries.ITEM, "fuel", key -> new FuelItem(new Item.Properties())); + public static final Registered WRENCH = Registration.register(Registries.ITEM, "wrench", key -> new WrenchItem(new Item.Properties())); + public static final Registered PARACHUTE = Registration.register(Registries.ITEM, "parachute", key -> new ParachuteItem(new Item.Properties())); + public static final Registered GLTD = Registration.register(Registries.ITEM, "gltd", + key -> new PlacedEntityItem(ModEntities.GLTD, new Item.Properties().stacksTo(1))); + public static final Registered RANGEFINDER = Registration.register(Registries.ITEM, "rangefinder", key -> new RangefinderItem(new Item.Properties())); + public static final Registered CONTAINER = Registration.register(Registries.ITEM, "container", + key -> new PlacedEntityItem(ModEntities.CONTAINER, new Item.Properties())); + public static final Registered CHAIN = Registration.register(Registries.ITEM, "chain", key -> new ChainItem(new Item.Properties())); + public static final Registered STINGER_MISSILE = Registration.register(Registries.ITEM, "fim92_bullet", key -> new Item(new Item.Properties().stacksTo(16))); + public static final Registered STINGER = Registration.register(Registries.ITEM, "fim92", + key -> new LauncherItem("fim92", () -> ModItems.STINGER_MISSILE.get(), new Item.Properties())); + public static final Registered JAVELIN_MISSILE = Registration.register(Registries.ITEM, "fgm148_bullet", key -> new Item(new Item.Properties().stacksTo(16))); + public static final Registered JAVELIN = Registration.register(Registries.ITEM, "fgm148", + key -> new LauncherItem("fgm148", () -> ModItems.JAVELIN_MISSILE.get(), new Item.Properties())); + public static final Registered UAV_STATION = Registration.register(Registries.ITEM, "uav_station", + key -> new UavStationItem(UavStationEntity.STATION, new Item.Properties().stacksTo(1))); + public static final Registered UAV_CONTROLLER = Registration.register(Registries.ITEM, "uav_station2", + key -> new UavStationItem(UavStationEntity.PORTABLE, new Item.Properties().stacksTo(1))); + public static final Registered DRAFTING_TABLE = Registration.register(Registries.ITEM, "drafting_table", + key -> new BlockItem(ModBlocks.DRAFTING_TABLE.get(), new Item.Properties())); + /** Every throwable; the stack says which (see {@link ThrowableItem}). */ + public static final Registered THROWABLE = Registration.register(Registries.ITEM, "throwable", key -> new ThrowableItem(new Item.Properties())); + + /** The equipment tab, in the order it lists them. */ + public static final List> EQUIPMENT = List.of(FUEL, WRENCH, PARACHUTE, GLTD, RANGEFINDER, CONTAINER, CHAIN, + STINGER, STINGER_MISSILE, JAVELIN, JAVELIN_MISSILE, UAV_STATION, UAV_CONTROLLER, DRAFTING_TABLE); + + private static final Map> BY_KIND = + Map.of("helicopter", HELICOPTER, "plane", PLANE, "tank", TANK, "vehicle", VEHICLE); + + private ModItems() { + } + + private static Registered kind(String kind) { + return Registration.register(Registries.ITEM, kind, key -> new AircraftItem(kind, new Item.Properties().stacksTo(1))); + } + + /** + * Declares the entries, with a 1.0.0 item for each original vehicle in {@code content}; the + * loader module registers them. + */ + public static void init(ContentRegistry content) { + for (AircraftDefinition def : content.all()) { + String name = def.name(); + // Only the imported content existed in 1.0.0; pack vehicles never had their own item. + if (!def.pack().equals(ContentSource.ORIGINAL) || BY_KIND.containsKey(name) || EQUIPMENT.stream().anyMatch(e -> e.id().getPath().equals(name))) { + // 1.0.0 could not have had such an item either: it would have clashed the same way. + continue; + } + String kind = def.kind(); + Registration.register(Registries.ITEM, name, key -> new LegacyAircraftItem(kind, name, + new Item.Properties().stacksTo(1))); + } + } + + /** The generic item of a vehicle kind ({@code helicopter}, {@code plane}, {@code tank} or {@code vehicle}). */ + public static Item forKind(String kind) { + Registered item = BY_KIND.get(kind); + if (item == null) { + throw new IllegalArgumentException("Unknown vehicle kind " + kind); + } + return item.get(); + } +} From c6c5cacdd76b9ac31b35afc808d47c66fc841fd4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:05 +0800 Subject: [PATCH 1297/1461] Add registry/ModMenus.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/registry/ModMenus.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java new file mode 100644 index 00000000..0dac0052 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModMenus.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.menu.DraftingTableMenu; +import com.jnhprojects.mcheli.menu.UavStationMenu; +import com.jnhprojects.mcheli.menu.VehicleMenu; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; + +/** Our screens with slots. The cargo chest uses vanilla's chest menus and needs no entry here. */ +public final class ModMenus { + public static final Registered> VEHICLE = Registration.register(Registries.MENU, "vehicle", + key -> new MenuType<>(VehicleMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> DRAFTING_TABLE = Registration.register(Registries.MENU, "drafting_table", + key -> new MenuType<>(DraftingTableMenu::new, FeatureFlags.VANILLA_SET)); + public static final Registered> UAV_STATION = Registration.register(Registries.MENU, "uav_station", + key -> new MenuType<>(UavStationMenu::new, FeatureFlags.VANILLA_SET)); + + private ModMenus() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 7d4ef62c7bda81a2c78f1712b32213a893afb229 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:06 +0800 Subject: [PATCH 1298/1461] Add registry/ModParticles.java to the 1.20.6 shared sources --- .../mcheli/registry/ModParticles.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java new file mode 100644 index 00000000..4ed53acd --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java @@ -0,0 +1,48 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.jnhprojects.mcheli.particle.SmokeParticleOptions; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; + +public final class ModParticles { + /** Ignores the particle limit: an explosion that loses its flash to the cap looks broken. */ + public static final Registered> GLOW = Registration.register(Registries.PARTICLE_TYPE, "glow", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return GlowParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return GlowParticleOptions.STREAM_CODEC; + } + }); + + /** Smoke grenades: a marker that vanishes under the particle limit is no marker. */ + public static final Registered> SMOKE = Registration.register(Registries.PARTICLE_TYPE, "smoke", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return SmokeParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return SmokeParticleOptions.STREAM_CODEC; + } + }); + + private ModParticles() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From f12b373aa32f8ae88911b1e502842a6c34f645b6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:06 +0800 Subject: [PATCH 1299/1461] Add team/Markers.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/team/Markers.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/team/Markers.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/team/Markers.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/team/Markers.java new file mode 100644 index 00000000..4d869373 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/team/Markers.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.team; + +import net.minecraft.core.BlockPos; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * What this client's team has spotted, {@code MCH_GuiTargetMarker}: the spotted entities, each for + * as long as it was spotted, and the last marked point. Filled by {@code MarkerPacket} on the + * client's main thread and drawn by the HUD; a dedicated server never touches it. + */ +public final class Markers { + /** A marked point stays up this long, unless another replaces it. */ + public static final int POINT_TICKS = 60 * 20; + + private static final Map SPOTTED = new HashMap<>(); + private static @Nullable BlockPos point; + private static int pointTicks; + + private Markers() { + } + + public static void spotted(int[] entities, int ticks) { + for (int id : entities) { + SPOTTED.merge(id, ticks, Math::max); + } + } + + public static void point(BlockPos pos) { + point = pos; + pointTicks = POINT_TICKS; + } + + /** Entity ids and ticks left. */ + public static Map spotted() { + return SPOTTED; + } + + public static @Nullable BlockPos point() { + return point; + } + + public static void tick() { + SPOTTED.replaceAll((id, ticks) -> ticks - 1); + SPOTTED.values().removeIf(ticks -> ticks <= 0); + if (pointTicks > 0 && --pointTicks == 0) { + point = null; + } + } + + public static void clear() { + SPOTTED.clear(); + point = null; + pointTicks = 0; + } +} From ee134cbfb192d68bcaf0ea1a2ed681fc53987a2a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:07 +0800 Subject: [PATCH 1300/1461] Find the respawn point in TeamManager on 1.20.6, which has no DimensionTransition --- .../jnhprojects/mcheli/team/TeamManager.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java new file mode 100644 index 00000000..5244b4d6 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/team/TeamManager.java @@ -0,0 +1,128 @@ +package com.jnhprojects.mcheli.team; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * The team manager's actions, {@code MCH_Multiplay} and {@code MCH_MultiplayPacketHandler}: for + * operators running a match. Teams are the vanilla scoreboard teams, so {@code /team} and the + * player list see the same ones. + */ +public final class TeamManager { + /** The colours a team can have. */ + public static final List COLORS = Arrays.stream(ChatFormatting.values()).filter(ChatFormatting::isColor).toList(); + + private TeamManager() { + } + + /** Operators only, as the original checked the scoreboard command's permission. */ + public static boolean mayManage(Player player) { + return player.hasPermissions(Commands.LEVEL_GAMEMASTERS); + } + + /** Switches the server's PVP setting and says which way it went. There is no game rule for it here, so it lasts until a restart. */ + public static void togglePvp(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + boolean pvp = !server.isPvpAllowed(); + server.setPvpAllowed(pvp); + broadcast(server, Component.translatable(pvp ? "message.mcheli.team.pvp_on" : "message.mcheli.team.pvp_off")); + } + + /** {@code shuffleTeam}: every player online goes into one of the teams in turn, in random order. */ + public static void shuffle(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + Scoreboard scoreboard = server.getScoreboard(); + List teams = new ArrayList<>(scoreboard.getPlayerTeams()); + if (teams.isEmpty()) { + player.displayClientMessage(Component.translatable("message.mcheli.team.no_teams"), true); + return; + } + List players = new ArrayList<>(server.getPlayerList().getPlayers()); + Collections.shuffle(players); + Collections.shuffle(teams); + for (int i = 0; i < players.size(); i++) { + scoreboard.addPlayerToTeam(players.get(i).getScoreboardName(), teams.get(i % teams.size())); + } + broadcast(server, Component.translatable("message.mcheli.team.shuffled", teams.size())); + } + + /** The original's create team screen: a name, a colour and friendly fire. */ + public static void create(ServerPlayer player, String name, int color, boolean friendlyFire) { + Scoreboard scoreboard = player.level().getServer().getScoreboard(); + String trimmed = name.trim(); + if (trimmed.isEmpty() || trimmed.length() > 16 || trimmed.contains(" ") || scoreboard.getPlayerTeam(trimmed) != null) { + player.displayClientMessage(Component.translatable("message.mcheli.team.bad_name", trimmed), true); + return; + } + PlayerTeam team = scoreboard.addPlayerTeam(trimmed); + team.setColor(COLORS.get(Math.floorMod(color, COLORS.size()))); + team.setAllowFriendlyFire(friendlyFire); + player.displayClientMessage(Component.translatable("message.mcheli.team.created", trimmed), true); + } + + /** {@code jumpSpawnPoint}: every player back to where they would respawn. */ + public static void jumpToSpawn(ServerPlayer player) { + for (ServerPlayer target : List.copyOf(player.level().getServer().getPlayerList().getPlayers())) { + target.stopRiding(); + toRespawnPoint(target); + } + } + + private static void toRespawnPoint(ServerPlayer target) { + MinecraftServer server = target.getServer(); + ServerLevel level = server.getLevel(target.getRespawnDimension()); + BlockPos spawn = target.getRespawnPosition(); + Vec3 at = level != null && spawn != null + ? Player.findRespawnPositionAndUseSpawnBlock(level, spawn, target.getRespawnAngle(), target.isRespawnForced(), false).orElse(null) + : null; + if (at == null) { + level = server.overworld(); + at = Vec3.atBottomCenterOf(level.getSharedSpawnPos()); + } + target.teleportTo(level, at.x, at.y, at.z, target.getYRot(), target.getXRot()); + } + + /** {@code destoryAllAircraft}: every loaded vehicle is gone, its crew left standing. */ + public static void destroyAll(ServerPlayer player) { + int count = 0; + for (ServerLevel level : player.level().getServer().getAllLevels()) { + for (AircraftEntity vehicle : level.getEntities(EntityTypeTest.forClass(AircraftEntity.class), e -> true)) { + vehicle.ejectPassengers(); + vehicle.discard(); + count++; + } + } + player.displayClientMessage(Component.translatable("message.mcheli.team.destroyed", count), true); + } + + /** {@code entity} and everyone on its team. */ + public static List teammates(ServerPlayer entity) { + List out = new ArrayList<>(); + for (ServerPlayer player : entity.level().getServer().getPlayerList().getPlayers()) { + if (player == entity || player.isAlliedTo(entity)) { + out.add(player); + } + } + return out; + } + + private static void broadcast(MinecraftServer server, Component message) { + server.getPlayerList().broadcastSystemMessage(message, false); + } +} From 2347d150e275800618d9027be80cbad99efbae22 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:07 +0800 Subject: [PATCH 1301/1461] Add weapon/CasStrike.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/weapon/CasStrike.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java new file mode 100644 index 00000000..81763693 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/CasStrike.java @@ -0,0 +1,124 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A close air support run, {@code MCH_WeaponCAS}: three A-10 passes over a spot, 45 ticks apart, + * the second and third offset to either side. Vehicles with a CAS weapon drive theirs from their + * {@link WeaponSet}; the GLTD keeps one of these. + */ +public final class CasStrike { + /** Ticks from the call to the first pass: the time it takes the A-10 to arrive. */ + public static final int FIRST_PASS = 50; + private static final int BETWEEN_PASSES = 45; + private static final int PASSES = 3; + private static final RandomSource RANDOM = RandomSource.create(); + + private final String weapon; + private double x; + private double y; + private double z; + private int direction; + private @Nullable Entity shooter; + private @Nullable Entity user; + private int passes = PASSES; + private int ticksToNext; + + /** @param weapon the CAS weapon definition the A-10s fire, such as {@code a10gau8} */ + public CasStrike(String weapon) { + this.weapon = weapon; + } + + /** Whether passes are still to come. */ + public boolean isRunning() { + return passes < PASSES; + } + + /** + * Calls the strike on {@code at}. The A-10 comes in from one side of the caller's view, + * picked at random, never straight from behind or towards them. + */ + public void start(Vec3 at, float callerYaw, @Nullable Entity shooter, @Nullable Entity user) { + x = at.x; + y = at.y; + z = at.z; + direction = Math.floorMod(approach(callerYaw), 4); + this.shooter = shooter; + this.user = user; + passes = 0; + ticksToNext = FIRST_PASS; + } + + /** The original's approach: the quarter the caller faces, turned one way or the other. */ + static int approach(float callerYaw) { + int facing = (int) Mth.positiveModulo(callerYaw + 45.0F, 360.0F) / 90; + return facing + (RANDOM.nextBoolean() ? -1 : 1); + } + + public void tick(ServerLevel level, WeaponDefinition def) { + if (passes >= PASSES || --ticksToNext > 0) { + return; + } + Vec3 offset = passOffset(passes, direction); + spawnA10(level, weapon, def, direction, x + offset.x, y + 20.0, z + offset.z, shooter, user); + passes++; + ticksToNext = BETWEEN_PASSES; + } + + /** The first pass is dead on; the second strays to one side of the track, the third to the other. */ + static Vec3 passOffset(int pass, int direction) { + if (pass == 0) { + return Vec3.ZERO; + } + double sign = pass == 1 ? 1.0 : -1.0; + if (direction % 2 == 0) { + return new Vec3(RANDOM.nextDouble() * 10.0 * sign, 0.0, (RANDOM.nextDouble() - 0.5) * 10.0); + } + return new Vec3((RANDOM.nextDouble() - 0.5) * 10.0, 0.0, RANDOM.nextDouble() * 10.0 * sign); + } + + /** One A-10, starting 90 blocks short of the spot and flying over it at three blocks a tick. */ + static void spawnA10(ServerLevel level, String weapon, WeaponDefinition def, int direction, double x, double y, double z, + @Nullable Entity shooter, @Nullable Entity user) { + double mx = 0.0; + double mz = 0.0; + switch (direction) { + case 0 -> { + mz = 3.0; + z -= 90.0; + } + case 1 -> { + mx = -3.0; + x += 90.0; + } + case 2 -> { + mz = -3.0; + z += 90.0; + } + default -> { + mx = 3.0; + x -= 90.0; + } + } + A10Entity a10 = ModEntities.A10.get().create(level); + if (a10 == null) { + return; + } + a10.setPos(x, y, z); + a10.setup(weapon, direction, mx, mz, shooter, user); + a10.power = def.power; + a10.explosionPower = def.explosion; + a10.acceleration = def.acceleration; + level.addFreshEntity(a10); + ModSounds.playAt(level, x, y, z, "a-10_snd", 150.0F, 1.0F); + } +} From 50cc28dd55883b518976ae1a0a76c330b9fd6387 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:07 +0800 Subject: [PATCH 1302/1461] Add weapon/Combat.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/weapon/Combat.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java new file mode 100644 index 00000000..f671d8e7 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +/** Damage rules shared by bullets and explosions. */ +public final class Combat { + private Combat() { + } + + /** The {@code DamageFactor} kind a target belongs to, or {@code null} for anything else. */ + public static @Nullable String kind(Entity entity) { + return switch (entity) { + case Player p -> "player"; + case HelicopterEntity h -> "helicopter"; + case PlaneEntity p -> "plane"; + case TankEntity t -> "tank"; + case GroundVehicleEntity v -> "vehicle"; + default -> null; + }; + } + + public static float factor(@Nullable WeaponDefinition def, Entity target) { + String kind = kind(target); + return def != null && kind != null ? def.damageFactor(kind) : 1.0F; + } + + /** + * Deals weapon damage. The original scaled every living target's invulnerability window by + * {@code HurtResistantTime}, zero by default, so each round of a burst lands. + */ + public static boolean hurt(Entity target, DamageSource source, float damage, @Nullable WeaponDefinition def) { + if (!(target.level() instanceof ServerLevel)) { + return false; + } + if (target instanceof LivingEntity living) { + living.invulnerableTime = 0; + } + return target.hurt(source, damage * factor(def, target)); + } + + /** The convention tag every loader fills, so modded panes count too. */ + private static final TagKey GLASS_PANES = TagKey.create(Registries.BLOCK, new ResourceLocation("c", "glass_panes")); + + /** + * {@code BulletBreakableBlocks} with its default list: panes, grass, flowers, vines, wheat, + * sugar cane and lily pads, which a round shoots through. + */ + public static boolean isBulletBreakable(BlockState state) { + return state.is(GLASS_PANES) + || state.is(BlockTags.FLOWERS) + || state.is(Blocks.SHORT_GRASS) || state.is(Blocks.TALL_GRASS) + || state.is(Blocks.FERN) || state.is(Blocks.LARGE_FERN) + || state.is(Blocks.VINE) || state.is(Blocks.WHEAT) + || state.is(Blocks.SUGAR_CANE) || state.is(Blocks.LILY_PAD); + } +} From 7bb24feacb25fe30fd337a619ee9856ebc8652ae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:08 +0800 Subject: [PATCH 1303/1461] Add weapon/ExplosionEffect.java to the 1.20.6 shared sources --- .../mcheli/weapon/ExplosionEffect.java | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java new file mode 100644 index 00000000..4f4b0dc8 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java @@ -0,0 +1,232 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.HashSet; +import java.util.Set; + +/** + * The look of an MCHeli explosion, ported from {@code MCH_Explosion.effectExplosion} and + * {@code effectExplosionInWater}: a fireball, ground dust thrown up from the crater, and columns + * and rings of smoke that hang around for seconds. It runs on each client that receives the + * explosion; the burning debris arcs are real flares spawned by the server. + */ +public final class ExplosionEffect { + /** The largest crash fireball drawn; beyond this the particle count gets out of hand. */ + public static final float MAX_CRASH_SIZE = 12.0F; + /** The widest explosion flash, in blocks; past this it washes out the whole screen. */ + private static final float MAX_FLASH_RADIUS = 30.0F; + + private ExplosionEffect() { + } + + /** + * Not in the original: a crashing vehicle goes up in a far bigger show than its fuel explosion + * alone makes. The same shell of smoke as a missile hit in mid-air, a cluster of fireballs + * around it, a plume of smoke that keeps climbing for seconds and burning fuel thrown about. + * The fire glows for several seconds afterwards, which at night lights up the smoke above it. + */ + public static void crash(Level level, double x, double y, double z, float size) { + play(level, x, y, z, size, false); + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, Math.min(size * 3.0F, MAX_FLASH_RADIUS), 20), + true, x, y + size * 0.3, z, 0.0, 0.0, 0.0); + int fireballs = Math.max(3, (int) (size / 2.0F)); + for (int i = 0; i < fireballs; i++) { + double r = size * 0.5 * random.nextDouble(); + double a = random.nextDouble() * Math.PI * 2.0; + double fx = x + Math.cos(a) * r; + double fy = y + random.nextDouble() * size * 0.4; + double fz = z + Math.sin(a) * r; + level.addAlwaysVisibleParticle(ParticleTypes.EXPLOSION_EMITTER, true, fx, fy, fz, 1.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size * (0.4F + random.nextFloat() * 0.3F), + 80 + random.nextInt(60)), true, fx, fy, fz, 0.0, 0.01, 0.0); + } + for (int i = 0; i < size * 4; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + x + (random.nextDouble() - 0.5) * size * 0.6, y + random.nextDouble() * 2.0, z + (random.nextDouble() - 0.5) * size * 0.6, + (random.nextDouble() - 0.5) * 0.02, 0.05 + random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.02); + } + for (int i = 0; i < size * 10; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = (0.2 + random.nextDouble() * 0.4) * size / 6.0; + double up = random.nextDouble() * speed; + level.addAlwaysVisibleParticle(random.nextInt(4) == 0 ? ParticleTypes.LAVA : ParticleTypes.FLAME, true, x, y + 0.5, z, + Math.cos(a) * speed, up, Math.sin(a) * speed); + if (i % 4 == 0) { + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.5F + random.nextFloat() * 0.4F, + 20 + random.nextInt(20)), true, x, y + 0.5, z, Math.cos(a) * speed, up, Math.sin(a) * speed); + } + } + } + + /** + * Not in the original: the light of the blast itself, a white hot flash inside a wider red + * glow, and a few embers thrown out. Faint by day; at night it lights up the smoke. + */ + private static void flash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + float radius = Math.min(2.0F + size * 1.5F, MAX_FLASH_RADIUS); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, radius, 8 + (int) size), true, x, y, z, 0.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.EMBER, Math.min(radius * 1.6F, MAX_FLASH_RADIUS), + 14 + (int) (size * 2.0F)), true, x, y, z, 0.0, 0.0, 0.0); + int embers = Math.min((int) (size * 2.0F), 24); + for (int i = 0; i < embers; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = 0.2 + random.nextDouble() * 0.3 * Math.min(size, 4.0F); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 15 + random.nextInt(15)), true, x, y, z, Math.cos(a) * speed, 0.1 + random.nextDouble() * speed, Math.sin(a) * speed); + } + } + + public static void play(Level level, double x, double y, double z, float size, boolean inWater) { + if (size <= 0.0F) { + return; + } + if (inWater) { + splash(level, x, y, z, size); + return; + } + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(size >= 2.0F ? ParticleTypes.EXPLOSION_EMITTER : ParticleTypes.EXPLOSION, true, x, y, z, 1.0, 0.0, 0.0); + flash(level, x, y, z, size); + + int count = 0; + int puffEvery = (int) Math.max(size, 4.0F); + for (BlockPos pos : affectedBlocks(level, x, y, z, size)) { + count++; + double bx = pos.getX() + random.nextFloat(); + double by = pos.getY() + random.nextFloat(); + double bz = pos.getZ() + random.nextFloat(); + double mx = bx - x; + double my = by - y; + double mz = bz - z; + double d = Math.sqrt(mx * mx + my * my + mz * mz); + if (d == 0.0) { + continue; + } + double push = 0.5 / (d / size + 0.1) * (random.nextFloat() * random.nextFloat() + 0.3F) * 0.5; + mx = mx / d * push; + my = my / d * push; + mz = mz / d * push; + double px = (bx + x) / 2.0; + double py = (by + y) / 2.0; + double pz = (bz + z) / 2.0; + double angle = Math.toRadians(random.nextInt(360)); + + if (count % 4 == 0) { + // Dirt and stone thrown up from the ground under the blast. + BlockState ground = level.getBlockState(BlockPos.containing(px + 0.5, py - 0.5, pz + 0.5)); + if (!ground.isAir()) { + float force = Math.min(size / 3.0F, 2.0F) * (0.5F + random.nextFloat() * 0.5F); + BlockParticleOption dust = new BlockParticleOption(ParticleTypes.BLOCK, ground); + for (int i = 0; i < 3; i++) { + level.addAlwaysVisibleParticle(dust, true, px, py + 1.0, pz, + Math.sin(angle) * force * 0.5, 0.5 + my / 5.0 * force, Math.cos(angle) * force * 0.5); + } + } + } + if (size <= 1.0F || count % puffEvery == 0) { + // Smoke either shoots up as a column or rolls out along the ground. + if (random.nextBoolean()) { + my *= 3.0; + mx *= 0.1; + mz *= 0.1; + } else { + my *= 0.2; + mx *= 3.0; + mz *= 3.0; + } + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, px, py, pz, mx * 0.3, my * 0.3, mz * 0.3); + for (int i = 0; i < 2; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, px, py, pz, mx, my, mz); + } + if (random.nextInt(3) == 0) { + level.addAlwaysVisibleParticle(ParticleTypes.FLAME, true, px, py, pz, mx * 0.5, my * 0.5, mz * 0.5); + } + } + } + } + + /** + * The blocks the original's effect ray marched through: sixteen by sixteen rays from the + * centre, each running until block resistance used up its strength. + */ + @SuppressWarnings("deprecation") // The plain block resistance is what the original used here. + private static Set affectedBlocks(Level level, double x, double y, double z, float size) { + Set out = new HashSet<>(); + RandomSource random = level.getRandom(); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + for (int k = 0; k < 16; k++) { + if (i != 0 && i != 15 && j != 0 && j != 15 && k != 0 && k != 15) { + continue; + } + double dx = i / 15.0F * 2.0F - 1.0F; + double dy = j / 15.0F * 2.0F - 1.0F; + double dz = k / 15.0F * 2.0F - 1.0F; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + dx /= len; + dy /= len; + dz /= len; + float strength = size * (0.7F + random.nextFloat() * 0.6F); + double cx = x; + double cy = y; + double cz = z; + for (float step = 0.3F; strength > 0.0F; strength -= step * 0.75F) { + cursor.set(Mth.floor(cx), Mth.floor(cy), Mth.floor(cz)); + BlockState state = level.getBlockState(cursor); + if (!state.isAir()) { + strength -= (state.getBlock().getExplosionResistance() + 0.3F) * step; + } + if (strength > 0.0F) { + out.add(cursor.immutable()); + } + cx += dx * step; + cy += dy * step; + cz += dz * step; + } + } + } + } + return out; + } + + /** {@code effectExplosionInWater}: a dome of spray from the water around the charge. */ + private static void splash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + int range = Math.max((int) (size + 0.5), 1); + int ex = (int) (x + 0.5); + int ey = (int) (y + 0.5); + int ez = (int) (z + 0.5); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int dy = -range; dy <= range; dy++) { + for (int dx = -range; dx <= range; dx++) { + for (int dz = -range; dz <= range; dz++) { + if (dx * dx + dy * dy + dz * dz >= range * range) { + continue; + } + cursor.set(ex + dx, ey + dy, ez + dz); + if (!level.getFluidState(cursor).is(FluidTags.WATER)) { + continue; + } + for (int n = random.nextInt(2); n > 0; n--) { + double up = 1.0 - Math.sqrt(dx * dx + dz * dz) / range + random.nextFloat() * 0.4 * range * 0.4; + level.addAlwaysVisibleParticle(ParticleTypes.SPLASH, true, cursor.getX(), cursor.getY() + 1.0, cursor.getZ(), + (double) dx / range * (random.nextFloat() - 0.2), up, (double) dz / range * (random.nextFloat() - 0.2)); + level.addAlwaysVisibleParticle(ParticleTypes.BUBBLE, true, cursor.getX(), cursor.getY(), cursor.getZ(), 0.0, up * 0.5, 0.0); + } + } + } + } + } +} From 2da8987414a2c43e693d440b216b8350e92dc516 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:08 +0800 Subject: [PATCH 1304/1461] Add weapon/Explosions.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/weapon/Explosions.java | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java new file mode 100644 index 00000000..4ab83969 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java @@ -0,0 +1,215 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.network.ExplosionEffectPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Port of {@code MCH_Explosion}. + * + *

The original explosion had two sizes: one for blocks and one, doubled, for entities, with its + * own damage curve and the weapon's per-target {@code DamageFactor}. Entity damage is computed + * here as it was; the blocks, particles and sound go through a vanilla explosion of the block size, + * so mob griefing rules, protection mods and the client effects all behave as players expect. + */ +public final class Explosions { + /** Players this far away still see the smoke of an explosion. */ + private static final double EFFECT_RANGE = 256.0; + /** Share of the blast knockback an empty vehicle takes; they are far heavier than a mob. */ + private static final double VEHICLE_PUSH = 0.25; + + private Explosions() { + } + + /** What an explosion request needs besides its position and size. */ + public record Params(@Nullable Entity exploder, @Nullable Entity cause, float size, float blockSize, + boolean flaming, boolean destroyBlocks, int fireSeconds, boolean inWater, + @Nullable WeaponDefinition weapon) { + } + + /** + * @return whether any entity took damage, which the shooter hears as a hit confirmation + */ + public static boolean explode(ServerLevel level, double x, double y, double z, Params p) { + Vec3 center = new Vec3(x, y, z); + boolean hit = damageEntities(level, center, p); + + boolean breakBlocks = p.destroyBlocks && p.blockSize > 0.0F && ServerConfig.explosionsBreakBlocks(); + float radius = breakBlocks ? p.blockSize : Math.max(p.size, 0.5F); + level.explode(p.exploder, explosionSource(level, p), new Calculator(p.inWater, level), + x, y, z, radius, p.flaming && breakBlocks && ServerConfig.explosionsStartFires(), + breakBlocks ? Level.ExplosionInteraction.MOB : Level.ExplosionInteraction.NONE); + ModNetwork.sendNear(level, x, y, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y, z, p.size, p.inWater)); + if (!p.inWater) { + throwDebris(level, x, y, z, p.size); + } + return hit; + } + + /** + * The look of a vehicle going up when it crashes: no damage or blocks of its own (the crash + * explosion does those), but the {@link ExplosionEffect#crash big fireball} on every nearby + * client and twice the burning debris. Drawn a little above the impact so its smoke spreads + * into the air the way a missile hit's does, rather than straight into the ground. + */ + public static void crashFireball(ServerLevel level, double x, double y, double z, float size) { + float visual = Math.clamp(size * 2.0F, 6.0F, ExplosionEffect.MAX_CRASH_SIZE); + double lift = visual * 0.25; + ModNetwork.sendNear(level, x, y + lift, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y + lift, z, visual, false, true)); + throwDebris(level, x, y, z, visual, 24); + } + + private static void throwDebris(ServerLevel level, double x, double y, double z, float size) { + throwDebris(level, x, y, z, size, 12); + } + + /** + * Big blasts throw burning debris that arcs away trailing smoke, which the original made of + * flares. One piece per point of size, up to {@code maxPieces}. + */ + private static void throwDebris(ServerLevel level, double x, double y, double z, float size, int maxPieces) { + if (size < 4.0F) { + return; + } + int pieces = Math.min((int) size * maxPieces / 12, maxPieces); + for (int i = 0; i < pieces; i++) { + double angle = Math.toRadians(level.getRandom().nextInt(360)); + double speed = Math.min(size / 12.0F, 0.6) * (0.5F + level.getRandom().nextFloat() * 0.5F); + FlareEntity debris = ModEntities.FLARE.get().create(level); + if (debris != null) { + double spread = size / 2.0; + debris.launch(x + (level.getRandom().nextDouble() - 0.5) * spread, y + 2.0, z + (level.getRandom().nextDouble() - 0.5) * spread, + Math.sin(angle) * speed, (1.0 + level.getRandom().nextFloat() * 0.2) * speed, Math.cos(angle) * speed, 2.0F, 0, false); + level.addFreshEntity(debris); + } + } + } + + private static DamageSource explosionSource(ServerLevel level, Params p) { + return level.damageSources().explosion(p.exploder, p.cause); + } + + /** {@code doExplosionA} for entities: twice the block size, the 1.7.10 damage curve and knockback. */ + private static boolean damageEntities(ServerLevel level, Vec3 center, Params p) { + float size = p.size * 2.0F; + if (size <= 0.0F) { + return false; + } + AABB area = new AABB(center, center).inflate(size + 1.0); + DamageSource source = explosionSource(level, p); + boolean hit = false; + for (Entity entity : level.getEntities(p.exploder, area)) { + double d7 = Math.sqrt(entity.distanceToSqr(center)) / size; + if (d7 > 1.0) { + continue; + } + double dx = entity.getX() - center.x; + double dy = entity.getEyeY() - center.y; + double dz = entity.getZ() - center.z; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist == 0.0) { + continue; + } + dx /= dist; + dy /= dist; + dz /= dist; + double exposure = Explosion.getSeenPercent(center, entity); + double impact = (1.0 - d7) * exposure; + float damage = (int) ((impact * impact + impact) / 2.0 * 8.0 * size + 1.0); + if (damage > 0.0F && !(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) + && !(entity instanceof FallingBlockEntity)) { + // Our own rounds caught in the blast do not count as hitting something. + if (!(entity instanceof ProjectileEntity bullet) || bullet.user() != p.cause) { + hit = true; + } + } + Combat.hurt(entity, source, damage, p.weapon); + if (entity instanceof AircraftEntity vehicle) { + // Not in the original: a vehicle someone is driving is simulated on their client, + // so a push here only snaps it back a step. An empty one still rocks a little. + if (vehicle.getControllingPassenger() == null) { + entity.push(dx * impact * VEHICLE_PUSH * 0.4, dy * impact * VEHICLE_PUSH * 0.1, dz * impact * VEHICLE_PUSH * 0.4); + } + } else if (!(entity instanceof ProjectileEntity)) { + entity.push(dx * impact * 0.4, dy * impact * 0.1, dz * impact * 0.4); + } + if (damage > 0.0F && p.fireSeconds > 0) { + double fire = 1.0 - dist / size; + if (fire > 0.0) { + entity.igniteForSeconds((int) Math.ceil(fire * p.fireSeconds)); + } + } + } + return hit; + } + + /** + * Vanilla explosion settings for the block pass: no entity damage or knockback (done above), + * and under water the blocks crumble the way the original's water charges made them. + */ + private static final class Calculator extends ExplosionDamageCalculator { + private final boolean inWater; + private final Level level; + + Calculator(boolean inWater, Level level) { + this.inWater = inWater; + this.level = level; + } + + // Vanilla's own calculator reads the block's resistance the same deprecated way. + @SuppressWarnings("deprecation") + @Override + public Optional getBlockExplosionResistance(Explosion explosion, BlockGetter getter, BlockPos pos, + BlockState block, FluidState fluid) { + if (!inWater) { + return super.getBlockExplosionResistance(explosion, getter, pos, block, fluid); + } + if (block.isAir()) { + return Optional.empty(); + } + float resistance = block.getBlock().getExplosionResistance(); + return Optional.of(resistance * (level.getRandom().nextFloat() * 0.2F + 0.2F)); + } + + @Override + public boolean shouldDamageEntity(Explosion explosion, Entity entity) { + return false; + } + + @Override + public float getKnockbackMultiplier(Entity entity) { + return 0.0F; + } + } + + /** {@code playExplosionSound}: the bang of a round that has no explosive charge. */ + public static void playSound(Level level, double x, double y, double z) { + float pitch = (1.0F + (level.getRandom().nextFloat() - level.getRandom().nextFloat()) * 0.2F) * 0.7F; + level.playSound(null, x, y, z, SoundEvents.GENERIC_EXPLODE.value(), SoundSource.BLOCKS, 4.0F, pitch); + } +} From a3888a8140b157dc843c90c51fdde8e2480b2b0a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:09 +0800 Subject: [PATCH 1305/1461] Add weapon/FlareDispenser.java to the 1.20.6 shared sources --- .../mcheli/weapon/FlareDispenser.java | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java new file mode 100644 index 00000000..ff583c8b --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java @@ -0,0 +1,194 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.LevelEvent; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and smoke dischargers, ported from {@code MCH_Flare} and the flare half of + * {@code MCH_MissileDetector}. While a salvo burns, every missile homing on the vehicle within 60 + * blocks is decoyed. + * + *

The ten {@code FlareType} values pick a pattern: 1 random scatter, 2 and 3 to the sides and + * rear, 4 and 5 trailing, 10 a ring of smoke grenades. Unknown types fall back to type 1. + */ +public final class FlareDispenser { + private static final int DECOY_RANGE = 60; + + /** {@code num, interval, tickWait, tickEnable, numFlareMax} per type. */ + private static final int[][] TYPES = new int[11][]; + + static { + TYPES[1] = new int[]{1, 3, 200, 100, 16}; + TYPES[2] = new int[]{3, 5, 300, 200, 16}; + TYPES[3] = new int[]{2, 3, 200, 100, 16}; + TYPES[4] = new int[]{1, 3, 200, 100, 16}; + TYPES[5] = new int[]{2, 3, 200, 100, 16}; + TYPES[10] = new int[]{8, 1, 250, 60, 1}; + for (int i : new int[]{0, 6, 7, 8, 9}) { + TYPES[i] = TYPES[1]; + } + } + + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final RandomSource random = RandomSource.create(); + private int tick; + private int fired; + private int type; + private int typeIndex; + + FlareDispenser(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + } + + public boolean isFitted() { + return !def.flareTypes.isEmpty(); + } + + /** Waiting out the reload of the last salvo. */ + public boolean isPreparing() { + return tick != 0; + } + + /** The salvo is still burning, so incoming missiles are being decoyed. */ + public boolean isActive() { + int[] p = params(type); + return tick != 0 && tick > p[2] - p[3]; + } + + public int ticksLeft() { + return tick; + } + + public int cooldown() { + return params(type)[2]; + } + + public int currentType() { + return def.flareTypes.isEmpty() ? 0 : def.flareTypes.get(typeIndex % def.flareTypes.size()); + } + + public void nextType() { + if (!def.flareTypes.isEmpty()) { + typeIndex = (typeIndex + 1) % def.flareTypes.size(); + } + } + + /** {@code useFlare}: starts a salvo of the current type if the last one has finished. */ + public boolean use() { + if (!isFitted() || tick != 0 || vehicle.isDestroyed()) { + return false; + } + type = currentType(); + fired = 0; + // The client waited 50 ticks longer before showing the dispenser as ready again. + tick = params(type)[2] + (vehicle.level().isClientSide() ? 50 : 0); + return true; + } + + private static int[] params(int type) { + return type >= 0 && type < TYPES.length && TYPES[type] != null ? TYPES[type] : TYPES[1]; + } + + void tick() { + if (type <= 0 || vehicle.isRemoved()) { + return; + } + if (tick > 0) { + tick--; + } + int[] p = params(type); + if (!vehicle.level().isClientSide()) { + if (tick > 0 && tick % p[1] == 0 && fired < p[4]) { + Vec3 at = vehicle.toWorld(def.flarePosition); + spawnSalvo(new Vec3(at.x + vehicle.xo - vehicle.getX(), at.y + vehicle.yo - vehicle.getY(), at.z + vehicle.zo - vehicle.getZ())); + } + if (isActive()) { + decoyMissiles(); + } + } + } + + private void spawnSalvo(Vec3 at) { + fired++; + int[] p = params(type); + Vec3 m = vehicle.getDeltaMovement(); + double x = at.x - m.x * 2.0; + double y = at.y - m.y * 2.0 - 1.0; + double z = at.z - m.z * 2.0; + vehicle.level().levelEvent(LevelEvent.SOUND_DISPENSER_PROJECTILE_LAUNCH, net.minecraft.core.BlockPos.containing(x, y, z), 0); + for (int i = 0; i < p[0]; i++) { + double tx = 0.0; + double ty = m.y; + double tz = 0.0; + int fuse = 0; + double r = vehicle.getYRot(); + double fy = y; + switch (type) { + case 1 -> { + tx = Mth.sin(random.nextFloat() * 360.0F); + tz = Mth.cos(random.nextFloat() * 360.0F); + } + case 2, 3 -> { + if (i == 0) { + r += 90.0; + } else if (i == 1) { + r -= 90.0; + } else if (i == 2) { + r += 180.0; + } + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 0.6; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 0.6; + } + case 4 -> { + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 1.3; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 1.3; + } + case 5 -> { + r = Math.toRadians(r); + tx = (-Math.sin(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + tz = (Math.cos(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + } + default -> { + } + } + tx += m.x; + ty += m.y / 2.0; + tz += m.z; + if (type == 10) { + r += 360.0 / p[0] / 2.0 + i * (360.0 / p[0]); + r = Math.toRadians(r); + tx = -Math.sin(r) * 2.0; + tz = Math.cos(r) * 2.0; + ty = 0.7; + fy += 2.0; + fuse = 10; + } + FlareEntity flare = ModEntities.FLARE.get().create(vehicle.level()); + if (flare != null) { + flare.launch(x, fy, z, tx * 0.5, ty * 0.5, tz * 0.5, 6.0F, fuse, type == 4); + vehicle.level().addFreshEntity(flare); + } + } + } + + /** {@code destroyMissile}: missiles homing on this vehicle or its crew lose their target. */ + private void decoyMissiles() { + for (ProjectileEntity missile : vehicle.level().getEntitiesOfClass(ProjectileEntity.class, + vehicle.getBoundingBox().inflate(DECOY_RANGE), p -> p.target() != null)) { + if (missile.target() == vehicle || vehicle.hasPassenger(missile.target())) { + missile.decoy(); + } + } + } +} From bd23eb19f201153bc1cc819c27d4a189ce81e763 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:09 +0800 Subject: [PATCH 1306/1461] Add weapon/Guidance.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/weapon/Guidance.java | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java new file mode 100644 index 00000000..b8142e92 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java @@ -0,0 +1,220 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Predicate; + +/** + * Lock-on for seeking missiles, ported from {@code MCH_WeaponGuidanceSystem}. It runs on the + * client of the crew member aiming; the locked entity travels to the server with the fire request. + * + *

A target is acquired inside a cone around the aim, then the lock count climbs while it stays + * in the cone. Stealth shrinks both the range and the cone and stretches the time to lock. + */ +public final class Guidance { + public final boolean canLockInAir; + public final boolean canLockOnGround; + public final boolean canLockInWater = false; + public final boolean ridableOnly; + public final double lockRange = 200.0; + public final int lockAngle = 5; + private final int lockCountMax; + private final @Nullable Predicate filter; + + private @Nullable Entity target; + private @Nullable Entity lastLocked; + private int lockCount; + private int prevLockCount; + private int continueLockCount; + private int soundCount; + + public Guidance(boolean air, boolean ground, boolean ridableOnly, int lockTime, @Nullable Predicate filter) { + this.canLockInAir = air; + this.canLockOnGround = ground; + this.ridableOnly = ridableOnly; + this.lockCountMax = Math.max(lockTime, 1); + this.filter = filter; + } + + public int lockCount() { + return lockCount; + } + + public int lockCountMax() { + float stealth = stealth(target); + return (int) (lockCountMax + lockCountMax * stealth); + } + + public @Nullable Entity target() { + return lockCount > 0 && target != null && !target.isRemoved() ? target : null; + } + + public @Nullable Entity lastLocked() { + return lastLocked; + } + + public boolean isLocked() { + return lockCount == lockCountMax() && lastLocked != null; + } + + /** Called every client tick: a lock decays unless {@link #lock} kept it alive this tick. */ + public void tick() { + if (lockCount != prevLockCount) { + prevLockCount = lockCount; + } else { + lockCount = 0; + prevLockCount = 0; + } + } + + /** + * Advances the lock for one tick of aiming from {@code user}'s view. + * + * @param eye where the view is seen from. The original used the player's position, which on a + * 1.7.10 client was eye height; gunner sights and launcher cameras sit higher still, + * and a 5 degree cone measured from the wrong point misses targets close by. + * @return true once the lock is complete; {@link #lastLocked()} is the target to send + */ + public boolean lock(Entity user, Vec3 eye) { + if (lockCount == 0) { + acquire(user, eye); + return false; + } + if (target == null || target.isRemoved() || !inLockableState(target)) { + clear(); + return false; + } + double range = lockRange * (1.0F - stealth(target)); + if (user.distanceToSqr(target) >= range * range) { + clear(); + return false; + } + soundCount = (soundCount + 1) % 15; + if (inCone(eye, user.getYRot(), user.getXRot(), target, lockAngle)) { + if (lockCount < lockCountMax()) { + lockCount++; + } + } else if (continueLockCount > 0) { + continueLockCount--; + if (continueLockCount <= 0 && lockCount > 0) { + lockCount--; + } + } else { + continueLockCount = 0; + lockCount--; + } + if (lockCount >= lockCountMax()) { + if (continueLockCount <= 0) { + continueLockCount = Math.min(lockCountMax() / 3, 20); + } + lastLocked = target; + prevLockCount = lockCount - 1; + return true; + } + return false; + } + + /** True on the tick the target should hear that it is being tracked. */ + public boolean shouldNotifyTarget() { + return lockCount > 0 && soundCount == 1; + } + + private void acquire(Entity user, Vec3 eye) { + Entity best = null; + double bestDistance = lockRange * lockRange * 2.0; + for (Entity entity : user.level().getEntities(user, user.getBoundingBox().inflate(lockRange), this::canLock)) { + double d = entity.distanceToSqr(user); + float stealth = 1.0F - stealth(entity); + double range = lockRange * stealth; + float angle = lockAngle * (stealth / 2.0F + 0.5F); + if (d < range * range && d < bestDistance && inCone(eye, user.getYRot(), user.getXRot(), entity, angle)) { + Vec3 to = new Vec3(entity.getX(), entity.getY() + entity.getBbHeight() / 2.0F, entity.getZ()); + HitResult hit = user.level().clip(new ClipContext(eye, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, user)); + if (hit.getType() == HitResult.Type.MISS) { + best = entity; + bestDistance = d; + } + } + } + target = best; + if (best != null) { + lockCount++; + } + } + + public void clear() { + target = null; + lockCount = 0; + continueLockCount = 0; + soundCount = 0; + } + + private boolean canLock(Entity entity) { + if (ridableOnly && entity instanceof Player && entity.getVehicle() == null) { + return false; + } + if (!(entity instanceof LivingEntity) && !(entity instanceof AircraftEntity)) { + return false; + } + if (filter != null && !filter.test(entity)) { + return false; + } + return inLockableState(entity); + } + + private boolean inLockableState(Entity entity) { + if (!canLockInWater && entity.isInWater()) { + return false; + } + boolean onGround = isOnGround(entity); + return onGround ? canLockOnGround : canLockInAir; + } + + /** On the ground, or less than twelve blocks above something solid. */ + public static boolean isOnGround(Entity entity) { + if (entity.onGround()) { + return true; + } + BlockPos.MutableBlockPos pos = BlockPos.containing(entity.getX() + 0.5, entity.getY() + 0.5, entity.getZ() + 0.5).mutable(); + for (int i = 0; i < 12; i++) { + if (!entity.level().getBlockState(pos).isAir() && !entity.level().getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + pos.move(0, -1, 0); + } + return false; + } + + public static float stealth(@Nullable Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + return vehicle.stealth(); + } + return entity != null && entity.getVehicle() instanceof AircraftEntity vehicle ? vehicle.stealth() : 0.0F; + } + + /** {@code inLockRange}: whether {@code target} is within {@code angle} degrees of the aim. */ + public static boolean inCone(Entity from, float yaw, float pitch, Entity target, float angle) { + return inCone(from.position(), yaw, pitch, target, angle); + } + + public static boolean inCone(Vec3 from, float yaw, float pitch, Entity target, float angle) { + double dx = target.getX() - from.x; + double dy = target.getY() + target.getBbHeight() / 2.0F - from.y; + double dz = target.getZ() - from.z; + float targetYaw = (float) Math.toDegrees(Math.atan2(dz, dx)); + float diffYaw = Mth.positiveModulo(targetYaw - yaw - 90.0F, 360.0F); + float targetPitch = -(float) Math.toDegrees(Math.atan2(dy, Math.sqrt(dx * dx + dz * dz))); + float diffPitch = targetPitch - pitch; + return (diffYaw < angle || diffYaw > 360.0F - angle) && Math.abs(diffPitch) < angle; + } +} From 3f9fe536c16a4e5249973812dab308d62f666eae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:10 +0800 Subject: [PATCH 1307/1461] Add weapon/LegacyItems.java to the 1.20.6 shared sources --- .../mcheli/weapon/LegacyItems.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java new file mode 100644 index 00000000..fe7b760f --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import org.jspecify.annotations.Nullable; + +/** + * Maps the 1.7.10 item names and damage values in the content files to today's items. Names + * that still exist resolve directly; the few that were renamed or flattened are listed here. + */ +public final class LegacyItems { + private static final String[] DYES = { + "ink_sac", "red_dye", "green_dye", "cocoa_beans", "lapis_lazuli", "purple_dye", "cyan_dye", "light_gray_dye", + "gray_dye", "pink_dye", "lime_dye", "yellow_dye", "light_blue_dye", "magenta_dye", "orange_dye", "bone_meal"}; + + private LegacyItems() { + } + + public static @Nullable Item item(String name, int damage) { + if (name.isEmpty()) { + return null; + } + String id = switch (name) { + case "dye" -> DYES[Math.floorMod(damage, DYES.length)]; + case "boat" -> "oak_boat"; + case "reeds" -> "sugar_cane"; + case "wool" -> "white_wool"; + case "log" -> "oak_log"; + case "planks" -> "oak_planks"; + default -> name.startsWith("minecraft:") ? name.substring("minecraft:".length()) : name; + }; + ResourceLocation key = ResourceLocation.tryParse(id.contains(":") ? id : "minecraft:" + id); + if (key == null) { + return null; + } + Item item = BuiltInRegistries.ITEM.get(key); + return item == Items.AIR ? null : item; + } +} From 8a36e9c1b8c2a5f9ac0198e8ec7977bb8fabb551 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:10 +0800 Subject: [PATCH 1308/1461] Add weapon/ModDamageTypes.java to the 1.20.6 shared sources --- .../mcheli/weapon/ModDamageTypes.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java new file mode 100644 index 00000000..e26fca48 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * Damage types defined in {@code data/mcheli/damage_type}. Bullets and missiles use their own type + * so death messages name the gun instead of calling it a thrown snowball, which is what the + * original's {@code causeThrownDamage} amounted to. + */ +public final class ModDamageTypes { + public static final ResourceKey BULLET = + ResourceKey.create(Registries.DAMAGE_TYPE, new ResourceLocation(MCHeli.MOD_ID, "bullet")); + + private ModDamageTypes() { + } + + public static DamageSource bullet(Level level, Entity projectile, @Nullable Entity owner) { + return new DamageSource(level.registryAccess().lookupOrThrow(Registries.DAMAGE_TYPE).getOrThrow(BULLET), projectile, owner); + } +} From df538c476e387194a705b8cc1ac2d3658cc3e11b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:11 +0800 Subject: [PATCH 1309/1461] Add weapon/Resupply.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/weapon/Resupply.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java new file mode 100644 index 00000000..533f5fba --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** + * Resupplying a weapon from a crew member's own items, the {@code supplyAmmo} path of the + * original's vehicle screen. One supply costs the weapon's {@code Item} lines and adds + * {@code SuppliedNum} rounds; it only works while the vehicle stands on something. + */ +public final class Resupply { + private Resupply() { + } + + /** {@code canPlayerSupplyAmmo}: whether {@link #supply} would do anything right now. */ + public static boolean canSupply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.set(weapon) : null; + if (set == null || vehicle.isDestroyed() || player.getVehicle() != vehicle || !vehicle.canSupply()) { + return false; + } + if (set.ammo() + set.restAmmo() >= set.maxAmmo()) { + return false; + } + return player.getAbilities().instabuild || canPay(player.getInventory(), set.definition()); + } + + /** Pays for and adds one supply of {@code weapon}. Server only. */ + public static boolean supply(ServerPlayer player, AircraftEntity vehicle, int weapon) { + if (!canSupply(player, vehicle, weapon)) { + return false; + } + WeaponSet set = vehicle.weapons().set(weapon); + if (!player.getAbilities().instabuild) { + pay(player.getInventory(), set.definition()); + } + set.supply(); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + return true; + } + + private static boolean canPay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null || inventory.countItem(item) < cost.count()) { + return false; + } + } + return true; + } + + private static void pay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + int left = cost.count(); + for (int i = 0; i < inventory.getContainerSize() && left > 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.is(item)) { + continue; + } + if (item == Items.WATER_BUCKET || item == Items.LAVA_BUCKET) { + // Buckets give their bucket back, one at a time. + inventory.setItem(i, new ItemStack(Items.BUCKET)); + left--; + } else { + int take = Math.min(left, stack.getCount()); + stack.shrink(take); + left -= take; + } + } + } + inventory.setChanged(); + } +} From b1d691050d34e5365ed0c7f0dfe7b2348acb3b29 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:11 +0800 Subject: [PATCH 1310/1461] Add weapon/ServerFiring.java to the 1.20.6 shared sources --- .../mcheli/weapon/ServerFiring.java | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java new file mode 100644 index 00000000..f7b6e969 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java @@ -0,0 +1,417 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * What each weapon type does when the server fires it: the {@code shot} methods of the + * {@code MCH_Weapon*} classes. Every method returns whether a round was used. + */ +public final class ServerFiring implements WeaponSet.Trigger { + public static final ServerFiring INSTANCE = new ServerFiring(); + private static final RandomSource RANDOM = RandomSource.create(); + + private ServerFiring() { + } + + @Override + public boolean fire(WeaponSet set, int mount, Shot shot) { + if (!(shot.shooter.level() instanceof ServerLevel level)) { + return false; + } + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_1, MACHINE_GUN_2 -> gun(level, set, shot); + case ROCKET -> rocket(level, set, shot); + case BOMB -> bomb(level, set, shot); + case AA_MISSILE -> aaMissile(level, set, mount, shot); + case AS_MISSILE -> asMissile(level, set, shot); + case AT_MISSILE -> atMissile(level, set, mount, shot); + case TV_MISSILE -> tvMissile(level, set, mount, shot); + case TORPEDO -> torpedo(level, set, shot); + case CAS -> cas(level, set, shot); + case MARKER_ROCKET -> markerRocket(level, set, shot); + case DISPENSER -> dispenser(level, set, shot); + case TARGETING_POD -> targetingPod(level, set, shot); + case DUMMY, SMOKE -> false; + case null -> false; + }; + } + + private static @Nullable T spawn(ServerLevel level, EntityType type, WeaponSet set, Shot shot, + Vec3 dir, float yaw, float pitch, double acceleration) { + T e = type.create(level); + if (e == null) { + return null; + } + e.launch(shot.x, shot.y, shot.z, dir.x, dir.y, dir.z, yaw, pitch, acceleration); + e.setWeapon(set.name()); + e.setParameters(set.definition(), shot.shooter, shot.user); + return e; + } + + private static Vec3 forward(float yaw, float pitch) { + return Vec3.directionFromRotation(pitch, yaw); + } + + private static void sound(WeaponSet set, Entity at) { + sound(set, at, soundName(set)); + } + + private static String soundName(WeaponSet set) { + return set.ownSound != null ? set.ownSound.sound() : set.definition().sound; + } + + private static void sound(WeaponSet set, Entity at, String name) { + if (set.soundAllowed) { + WeaponDefinition w = set.definition(); + float rnd = w.soundPitchRandom; + float pitch = set.ownSound != null && set.ownSound.pitch() != null ? set.ownSound.pitch() : w.soundPitch; + ModSounds.playAt(at, name, w.soundVolume, pitch * (1.0F - rnd) + RANDOM.nextFloat() * rnd); + } + } + + // Guns, rockets, bombs + + private static boolean gun(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + BulletEntity e = spawn(level, ModEntities.BULLET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, w.acceleration); + if (e == null) { + return false; + } + if (w.type == WeaponType.MACHINE_GUN_2 && w.modeNum >= 2) { + // Mode 0 fires ball ammunition, which only bangs; mode 1 is high explosive. + e.explosionPower = shot.option1 == 0 ? -w.explosion : w.explosion; + } + Vec3 m = e.getDeltaMovement(); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean rocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + RocketEntity e = spawn(level, ModEntities.ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + if (shot.option1 == 0 && set.definition().modeNum > 1) { + e.piercing = 0; + } + level.addFreshEntity(e); + return true; + } + + private static boolean bomb(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + if (w.destruct) { + // A drone's self destruct charge. + if (shot.shooter instanceof HelicopterEntity drone && drone.definition() != null && drone.definition().uav) { + Explosions.explode(level, drone.getX(), drone.getY(), drone.getZ(), + new Explosions.Params(null, shot.user, w.explosion, w.explosionBlock, w.flaming, true, 0, false, null)); + sound(set, shot.shooter); + drone.ejectPassengers(); + drone.discard(); + } + return true; + } + sound(set, shot.shooter); + Vec3 motion = shot.shooter.getDeltaMovement(); + BombEntity e = spawn(level, ModEntities.BOMB.get(), set, shot, motion, shot.shooter.getYRot(), 0.0F, w.acceleration); + if (e == null) { + return false; + } + e.setDeltaMovement(motion); + level.addFreshEntity(e); + return true; + } + + // Missiles + + private static float fixedYaw(WeaponSet set, int mount) { + return set.mounts().get(mount).yaw(); + } + + private static float fixedPitch(WeaponSet set, int mount) { + return set.mounts().get(mount).pitch(); + } + + private static boolean aaMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + sound(set, shot.shooter); + float yaw = shot.shooter.getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + AAMissileEntity e = spawn(level, ModEntities.AA_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + level.addFreshEntity(e); + return true; + } + + /** Ray from the shooter along the aimer's view, as the original's AS missile and CAS sights. */ + private static @Nullable BlockHitResult sight(ServerLevel level, Shot shot, double eyeHeight, double range) { + Entity aimer = shot.aimer(); + Vec3 from = shot.shooter.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(forward(aimer.getYRot(), aimer.getXRot()).scale(range)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + return hit.getType() == HitResult.Type.BLOCK ? hit : null; + } + + private static boolean nextToWater(ServerLevel level, BlockPos pos) { + if (level.getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + for (Direction d : Direction.values()) { + if (level.getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + private static boolean asMissile(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 1.62, 250.0); + if (hit == null || nextToWater(level, hit.getBlockPos())) { + return false; + } + Entity aimer = shot.aimer(); + ASMissileEntity e = spawn(level, ModEntities.AS_MISSILE.get(), set, shot, forward(aimer.getYRot(), aimer.getXRot()), + aimer.getYRot(), aimer.getXRot(), set.definition().acceleration); + if (e == null) { + return false; + } + e.targetX = hit.getLocation().x; + e.targetY = hit.getLocation().y; + e.targetZ = hit.getLocation().z; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean atMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + ATMissileEntity e = spawn(level, ModEntities.AT_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + e.guidanceType = shot.option2; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean tvMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.aimer().getXRot() + fixedPitch(set, mount); + boolean tvGuided = shot.option1 == 0; + float acceleration = set.definition().acceleration * (tvGuided ? 1.0F : 1.5F); + TvMissileEntity e = spawn(level, ModEntities.TV_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, acceleration); + if (e == null) { + return false; + } + level.addFreshEntity(e); + if (shot.shooter instanceof AircraftEntity vehicle) { + vehicle.setTvMissile(e); + } + sound(set, shot.shooter); + return true; + } + + private static boolean torpedo(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Vec3 vehicleMotion = shot.shooter.getDeltaMovement(); + float yaw = shot.yaw; + float pitch = shot.pitch; + Vec3 target = null; + if (w.guidedTorpedo) { + Entity aimer = shot.aimer(); + yaw = aimer.getYRot(); + pitch = aimer.getXRot(); + Vec3 from = aimer.position(); + Vec3 to = from.add(forward(yaw, pitch).scale(150.0)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + if (hit.getType() != HitResult.Type.BLOCK || !nextToWater(level, hit.getBlockPos())) { + return false; + } + target = hit.getLocation(); + } + Vec3 m = forward(yaw, pitch).scale(w.acceleration).add(vehicleMotion); + double speed = m.length(); + TorpedoEntity e = spawn(level, ModEntities.TORPEDO.get(), set, shot, m, yaw, 0.0F, speed); + if (e == null) { + return false; + } + e.setDeltaMovement(m); + e.accelerationInWater = w.accelerationInWater; + if (target != null) { + e.targetX = target.x; + e.targetY = target.y; + e.targetZ = target.z; + } + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + // Support weapons + + private static boolean markerRocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + MarkerRocketEntity e = spawn(level, ModEntities.MARKER_ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + e.setStatus(1); + level.addFreshEntity(e); + return true; + } + + private static boolean dispenser(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + DispensedItemEntity e = spawn(level, ModEntities.DISPENSED_ITEM.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + Vec3 m = shot.shooter.getDeltaMovement().add(e.getDeltaMovement().scale(0.5)); + e.setDeltaMovement(m); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + return true; + } + + /** + * Spots everything of the weapon's {@code Target} kinds inside its cone, which then glows for + * {@code MarkTime}. Marking a block for the team needs the team manager of a later phase. + */ + private static boolean targetingPod(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Entity aimer = shot.aimer(); + if ((w.target & WeaponDefinition.TARGET_BLOCK) != 0) { + // {@code MCH_Multiplay.markPoint}: the block under the crew's view, 300 blocks out, goes to the team. + boolean marked = aimer instanceof ServerPlayer player && Spotting.markPoint(player, new Vec3(shot.x, shot.y, shot.z), 300.0); + sound(set, aimer, marked ? soundName(set) : "ng"); + return true; + } + List spotted = new ArrayList<>(); + double range = Math.max(w.length, 1.0F); + for (LivingEntity entity : level.getEntitiesOfClass(LivingEntity.class, aimer.getBoundingBox().inflate(range))) { + if (entity == aimer || shot.shooter.hasPassenger(entity) || aimer.distanceToSqr(entity) > range * range + || !matchesTarget(entity, w.target) || !Guidance.inCone(aimer, aimer.getYRot(), aimer.getXRot(), entity, w.angle)) { + continue; + } + spotted.add(entity.getId()); + } + if (aimer instanceof ServerPlayer player) { + Spotting.tellTeam(player, spotted, Math.max(w.markTime, 20)); + } + sound(set, shot.shooter, !spotted.isEmpty() ? soundName(set) : "ng"); + return true; + } + + private static boolean matchesTarget(LivingEntity entity, int bits) { + Entity vehicle = entity.getVehicle(); + if (vehicle instanceof PlaneEntity) { + return (bits & WeaponDefinition.TARGET_PLANES) != 0; + } + if (vehicle instanceof HelicopterEntity) { + return (bits & WeaponDefinition.TARGET_HELICOPTERS) != 0; + } + if (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity) { + return (bits & WeaponDefinition.TARGET_GROUND_VEHICLES) != 0; + } + if (entity instanceof Player) { + return (bits & WeaponDefinition.TARGET_PLAYERS) != 0; + } + if (entity instanceof Enemy) { + return (bits & WeaponDefinition.TARGET_MONSTERS) != 0; + } + return (bits & WeaponDefinition.TARGET_OTHERS) != 0; + } + + // Close air support + + private static boolean cas(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 2.0, 150.0); + if (hit == null) { + return false; + } + Vec3 at = hit.getLocation(); + set.casX = at.x; + set.casY = at.y; + set.casZ = at.z; + set.casDirection = Math.floorMod(CasStrike.approach(shot.aimer().getYRot()), 4); + set.casUser = shot.user; + set.casShooter = shot.shooter; + set.casStartTick = CasStrike.FIRST_PASS; + set.casCount = 0; + if (shot.user instanceof ServerPlayer player) { + ModSounds.playTo(player, set.definition().sound, 1.0F, 1.0F); + } + return true; + } + + /** {@code MCH_WeaponCAS.update}: three passes, 45 ticks apart, the later ones offset to the sides. */ + static void tick(AircraftEntity vehicle, WeaponSet set) { + if (set.casCount >= 3 || set.countWait == 0 || set.ticksSinceShot(0) != set.casStartTick + || !(vehicle.level() instanceof ServerLevel level)) { + return; + } + Vec3 offset = CasStrike.passOffset(set.casCount, set.casDirection); + CasStrike.spawnA10(level, set.name(), set.definition(), set.casDirection, set.casX + offset.x, set.casY + 20.0, + set.casZ + offset.z, set.casShooter, set.casUser); + set.casStartTick = set.ticksSinceShot(0) + 45; + set.casCount++; + } +} From 5ef92af223ed3200c79404e4c718f13dd6f4ffad Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:12 +0800 Subject: [PATCH 1311/1461] Add weapon/Shot.java to the 1.20.6 shared sources --- .../com/jnhprojects/mcheli/weapon/Shot.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java new file mode 100644 index 00000000..6a9f53cf --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java @@ -0,0 +1,40 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +/** + * One trigger pull, the port of {@code MCH_WeaponParam}. The weapon set fills in the aim and the + * muzzle position; the client fills in the two option values (a locked target, a fire mode) and + * the server receives them with the fire request. + */ +public final class Shot { + /** The vehicle firing, or the player for hand held weapons. */ + public final Entity shooter; + /** Who pulled the trigger. Damage and kills are credited to this entity. */ + public final @Nullable Entity user; + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + public float roll; + public int option1; + public int option2; + public boolean infiniteAmmo; + /** Set by the weapon set when the magazine ran dry with this shot. */ + public boolean reload; + + public Shot(Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + this.x = shooter.getX(); + this.y = shooter.getY(); + this.z = shooter.getZ(); + } + + /** The entity whose view steers guided weapons: the user if there is one, else the shooter. */ + public Entity aimer() { + return user != null ? user : shooter; + } +} From 3a494451264fb56eb40e4a9398e517672a9a7cf5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:12 +0800 Subject: [PATCH 1312/1461] Add weapon/Spotting.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/weapon/Spotting.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java new file mode 100644 index 00000000..10167f4a --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/Spotting.java @@ -0,0 +1,158 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.network.MarkerPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.team.TeamManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +/** + * Spotting with the rangefinder, {@code MCH_Multiplay.spotEntity} and {@code markPoint}. + * + *

Spotted targets and marked points go to the spotter and their team only, as screen markers + * the HUD draws ({@code MCH_GuiTargetMarker}); nobody else sees them. + */ +public final class Spotting { + // The original's target filter bits. + public static final int OTHERS = 1; + public static final int MONSTERS = 2; + public static final int PLAYERS = 4; + public static final int GROUND_VEHICLES = 8; + public static final int HELICOPTERS = 16; + public static final int PLANES = 32; + + /** {@code RangeFinderSpotDist}. */ + public static final double RANGE = 400.0; + /** {@code RangeFinderSpotTime} in ticks, for players and vehicles. */ + private static final int PLAYER_TIME = 15 * 20; + /** Mobs stay marked for a minute. */ + private static final int MOB_TIME = 60 * 20; + /** Half the width of the cone a spot covers, in degrees: the original's angle times two. */ + private static final double CONE = 40.0; + + private Spotting() { + } + + static boolean matches(int filter, Entity entity) { + int bit; + if (entity instanceof PlaneEntity) { + bit = PLANES; + } else if (entity instanceof HelicopterEntity) { + bit = HELICOPTERS; + } else if (entity instanceof AircraftEntity) { + bit = GROUND_VEHICLES; + } else if (entity instanceof Player) { + bit = PLAYERS; + } else if (entity instanceof Enemy) { + bit = MONSTERS; + } else if (entity instanceof LivingEntity) { + bit = OTHERS; + } else { + return false; + } + return (filter & bit) != 0; + } + + /** + * Marks what {@code spotter} sees within {@link #RANGE} and the cone around their view: never + * a teammate, and only targets in plain sight. + * + * @return whether anything was spotted + */ + public static boolean spot(ServerPlayer spotter, int filter) { + ServerLevel level = spotter.serverLevel(); + Vec3 eye = spotter.getEyePosition(); + Vec3 look = spotter.getLookAngle(); + int ticks = (filter & ~(OTHERS | MONSTERS)) == 0 ? MOB_TIME : PLAYER_TIME; + List spotted = new ArrayList<>(); + for (Entity entity : level.getEntities(spotter, spotter.getBoundingBox().inflate(RANGE), e -> matches(filter, e))) { + if (isFriendly(spotter, entity)) { + continue; + } + Vec3 to = entity.getBoundingBox().getCenter().subtract(eye); + double dist = to.length(); + if (dist < 1.0 || dist > RANGE || Math.toDegrees(Math.acos(to.scale(1.0 / dist).dot(look))) > CONE) { + continue; + } + if (level.clip(new ClipContext(eye, entity.getBoundingBox().getCenter(), ClipContext.Block.COLLIDER, + ClipContext.Fluid.NONE, spotter)).getType() == HitResult.Type.BLOCK) { + continue; + } + spotted.add(entity.getId()); + } + tellTeam(spotter, spotted, ticks); + return !spotted.isEmpty(); + } + + /** Sends the spotted entities to {@code spotter} and their team, {@code MCH_PacketNotifySpotedEntity}. */ + public static void tellTeam(ServerPlayer spotter, List entities, int ticks) { + if (entities.isEmpty()) { + return; + } + MarkerPacket packet = MarkerPacket.spotted(entities.stream().mapToInt(Integer::intValue).toArray(), ticks); + for (ServerPlayer player : TeamManager.teammates(spotter)) { + ModNetwork.sendToPlayer(player, packet); + } + } + + /** Teammates, and a vehicle crewed by one, are never spotted. */ + private static boolean isFriendly(ServerPlayer spotter, Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + for (Entity crew : vehicle.getPassengers()) { + if (crew.isAlliedTo(spotter)) { + return true; + } + } + return false; + } + return entity.isAlliedTo(spotter); + } + + /** + * Tells the spotter and their team where they are looking, as block coordinates. + * + * @return whether the view ended on a block within range + */ + public static boolean markPoint(ServerPlayer spotter) { + return markPoint(spotter, spotter.getEyePosition(), RANGE); + } + + /** + * As above, looking from {@code from} along the spotter's view: a targeting pod marks from + * the vehicle, out to {@code range}. + */ + public static boolean markPoint(ServerPlayer spotter, Vec3 from, double range) { + ServerLevel level = spotter.serverLevel(); + BlockHitResult hit = level.clip(new ClipContext(from, from.add(spotter.getLookAngle().scale(range)), + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, spotter)); + if (hit.getType() != HitResult.Type.BLOCK) { + return false; + } + BlockPos pos = hit.getBlockPos(); + MarkerPacket marker = MarkerPacket.point(pos); + Component message = Component.translatable("message.mcheli.mark_point", spotter.getDisplayName(), pos.getX(), pos.getY(), pos.getZ()); + for (ServerPlayer player : level.getServer().getPlayerList().getPlayers()) { + if (player == spotter || player.isAlliedTo(spotter)) { + player.sendSystemMessage(message); + ModNetwork.sendToPlayer(player, marker); + } + } + return true; + } +} From 63ec898b2fc2632b8e156e385fc44545bc7be2b5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:13 +0800 Subject: [PATCH 1313/1461] Add weapon/VehicleWeapons.java to the 1.20.6 shared sources --- .../mcheli/weapon/VehicleWeapons.java | 598 ++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java new file mode 100644 index 00000000..02f4c39b --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java @@ -0,0 +1,598 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.weapon.MuzzleFlash; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.WeaponStatePacket; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The armament of one vehicle: its weapon sets, which one each seat has selected, the flares, and + * the effects every client derives from the synced firing state. Ported from the weapon handling + * in {@code MCH_EntityAircraft} ({@code createWeapon}, {@code switchWeapon}, + * {@code useCurrentWeapon}, {@code updateWeapons} and {@code updateWeaponsRotation}). + */ +public final class VehicleWeapons { + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final boolean clientSide; + private final List sets = new ArrayList<>(); + private final int[] selected; + private final @Nullable Guidance[] guidance; + private final RandomSource random = RandomSource.create(); + + /** One bit per mount, set while that mount shows muzzle effects. Synced by the vehicle. */ + private int useStat; + private int prevUseStat; + + private final float[] bayAngle; + private final float[] prevBayAngle; + + private final int[] sentAmmo; + private final int[] sentRest; + + public final FlareDispenser flares; + + // Recoil that kicks the whole hull, from Recoil in the weapon file. + private int recoilCount; + private float recoilValue; + private float recoilYaw; + + public VehicleWeapons(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + this.clientSide = vehicle.level().isClientSide(); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + if (weapon != null && !group.mounts().isEmpty()) { + WeaponSet set = new WeaponSet(weapon, group.mounts(), clientSide); + set.ownSound = def.weaponSounds.get(group.weaponType()); + sets.add(set); + } + } + selected = new int[def.seatCount()]; + Arrays.fill(selected, -1); + guidance = new Guidance[sets.size()]; + for (int i = 0; i < sets.size(); i++) { + WeaponDefinition w = sets.get(i).definition(); + if (w.type == WeaponType.AA_MISSILE || w.type == WeaponType.AT_MISSILE) { + boolean aa = w.type == WeaponType.AA_MISSILE; + guidance[i] = new Guidance(aa, !aa, w.ridableOnly, w.lockTime, this::canLock); + } + } + bayAngle = new float[def.weaponBays.size()]; + prevBayAngle = new float[def.weaponBays.size()]; + sentAmmo = new int[sets.size()]; + sentRest = new int[sets.size()]; + Arrays.fill(sentAmmo, -1); + flares = new FlareDispenser(vehicle, def); + } + + public List sets() { + return sets; + } + + public @Nullable WeaponSet set(int id) { + return id >= 0 && id < sets.size() ? sets.get(id) : null; + } + + public int indexOf(String weapon) { + for (int i = 0; i < sets.size(); i++) { + if (sets.get(i).name().equalsIgnoreCase(weapon)) { + return i; + } + } + return -1; + } + + public @Nullable WeaponSet byName(String weapon) { + return set(indexOf(weapon)); + } + + public @Nullable Guidance guidance(int id) { + return id >= 0 && id < guidance.length ? guidance[id] : null; + } + + /** A vehicle does not lock its own crew or cargo. */ + private boolean canLock(Entity entity) { + return entity != vehicle && !vehicle.hasPassenger(entity); + } + + // Selection + + public int selected(int seat) { + return seat >= 0 && seat < selected.length ? selected[seat] : -1; + } + + public @Nullable WeaponSet current(int seat) { + return set(selected(seat)); + } + + /** The seat that operates a weapon: its own seat, or the pilot while that seat has no player. */ + public int operatorSeat(int id) { + WeaponSet set = set(id); + if (set == null) { + return -1; + } + WeaponMount mount = set.firstMount(); + if (mount.usableByPilot() && !(vehicle.occupant(mount.seatIndex()) instanceof Player)) { + return 0; + } + return mount.seatIndex(); + } + + /** {@code getNextWeaponID}: the next weapon {@code seat} may use, stepping forwards or back. */ + public int next(int seat, int step) { + if (sets.isEmpty() || seat < 0) { + return -1; + } + int id = selected(seat); + for (int i = 0; i < sets.size(); i++) { + id = step >= 0 ? (id + 1) % sets.size() : id > 0 ? id - 1 : sets.size() - 1; + WeaponMount mount = sets.get(id).firstMount(); + int owner = mount.seatIndex(); + if (owner == seat || seat == 0 && mount.usableByPilot() && !(vehicle.occupant(owner) instanceof Player)) { + return id; + } + } + return -1; + } + + /** {@code initCurrentWeapon}: a player took a seat. Server only. */ + public void onSeated(Entity entity, int seat) { + if (seat < 0 || seat >= selected.length) { + return; + } + selected[seat] = -1; + if (entity instanceof Player) { + switchTo(entity, seat, next(seat, 1)); + } else { + vehicle.publishWeaponSelection(); + } + } + + public void onLeft(int seat) { + if (seat >= 0 && seat < selected.length) { + selected[seat] = -1; + vehicle.publishWeaponSelection(); + } + } + + /** {@code switchWeapon}. Server only; the selection reaches clients through synced data. */ + public void switchTo(Entity user, int seat, int id) { + if (seat < 0 || seat >= selected.length || sets.isEmpty()) { + return; + } + WeaponSet before = current(seat); + if (before != null) { + before.reload(); + } + selected[seat] = Math.min(id, sets.size() - 1); + WeaponSet now = current(seat); + if (now != null) { + now.onSwitchWeapon(vehicle.hasInfiniteAmmo(user)); + } + vehicle.publishWeaponSelection(); + } + + public String encodeSelection() { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < selected.length; i++) { + if (i > 0) { + b.append(','); + } + b.append(selected[i]); + } + return b.toString(); + } + + /** Applies the server's selection on a client, running the switch delay where it changed. */ + public void decodeSelection(String encoded, @Nullable Entity localPlayer) { + String[] parts = encoded.isEmpty() ? new String[0] : encoded.split(","); + for (int i = 0; i < selected.length; i++) { + int id; + try { + id = i < parts.length ? Integer.parseInt(parts[i]) : -1; + } catch (NumberFormatException e) { + id = -1; + } + if (selected[i] != id) { + selected[i] = id; + WeaponSet set = set(id); + Entity occupant = vehicle.occupant(i); + if (set != null && occupant != null && occupant == localPlayer) { + set.onSwitchWeapon(vehicle.hasInfiniteAmmo(occupant)); + Guidance g = guidance(id); + if (g != null) { + g.clear(); + } + } + } + } + } + + // Firing + + /** + * {@code useCurrentWeapon}: fires the selected weapon of {@code seat} if it is ready. The + * trigger decides what the shot does on this side. + */ + public boolean use(Entity user, int seat, Shot shot, WeaponSet.Trigger trigger) { + int id = selected(seat); + WeaponSet set = set(id); + if (set == null || !set.canUse() || vehicle.isDestroyed()) { + return false; + } + shot.infiniteAmmo = vehicle.hasInfiniteAmmo(user); + int mount = set.currentMount(); + // As the original did, face the vehicle where the user looks for the length of the shot. + boolean followUser = vehicle.aimsWithUser(set); + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + if (followUser) { + vehicle.setYRot(user.getYRot()); + vehicle.setXRot(user.getXRot()); + } + boolean fired; + try { + fired = set.use(shot, (s, m, sh) -> { + Vec3 muzzle = muzzleOffset(s, m, 1.0F); + sh.x += muzzle.x; + sh.y += muzzle.y; + sh.z += muzzle.z; + return trigger.fire(s, m, sh); + }); + } finally { + if (followUser) { + vehicle.setYRot(hullYaw); + vehicle.setXRot(hullPitch); + } + } + if (!fired) { + return false; + } + String group = set.definition().group; + if (!group.isEmpty()) { + for (WeaponSet other : sets) { + if (other != set && group.equals(other.definition().group)) { + other.waitForGroup(shot.reload); + } + } + } + if (!clientSide) { + int bit = bitOf(id, mount); + if (bit < 32) { + useStat |= 1 << bit; + } + } + return true; + } + + /** World offset of a mount from the vehicle origin; turret mounts turn with the turret. */ + public Vec3 muzzleOffset(WeaponSet set, int mount, float partialTicks) { + WeaponMount m = set.mounts().get(mount); + Vector3 local = m.turret() ? vehicle.onTurret(def, m.position(), partialTicks) : m.position(); + Vector3f v = vehicle.toWorldOffset(local); + return new Vec3(v.x, v.y, v.z); + } + + private int bitOf(int id, int mount) { + int shift = 0; + for (int i = 0; i < id; i++) { + shift += sets.get(i).mounts().size(); + } + return shift + mount; + } + + /** Whether a mount is cooling down after a shot; missile parts are hidden meanwhile. */ + public boolean isMountUsed(int id, int mount) { + int bit = bitOf(id, mount); + return bit < 32 && (useStat & 1 << bit) != 0; + } + + public int useStat() { + return useStat; + } + + public void setUseStat(int stat) { + useStat = stat; + } + + public void switchMode(int seat) { + WeaponSet set = current(seat); + if (set != null) { + set.switchMode(); + } + } + + // Ticking + + /** {@code updateWeapons} and {@code updateWeaponsRotation}, once per tick on both sides. */ + public void tick() { + if (sets.isEmpty()) { + flares.tick(); + return; + } + int previous = prevUseStat; + if (!clientSide) { + useStat |= usedStat(); + vehicle.publishWeaponUse(useStat); + } else { + useStat = vehicle.syncedWeaponUse(); + } + prevUseStat = useStat; + + float yaw = Mth.wrapDegrees(vehicle.getYRot()); + float pitch = Mth.wrapDegrees(vehicle.getXRot()); + int bit = 0; + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + boolean longDelay = set.isLongDelay(); + boolean anyUsed = false; + for (int mount = 0; mount < set.mounts().size(); mount++) { + boolean wasUsed = bit < 32 && (previous & 1 << bit) != 0; + boolean used = bit < 32 && (useStat & 1 << bit) != 0; + if (longDelay && wasUsed && used) { + used = false; + } + anyUsed |= used; + if (!wasUsed && used && set.definition().recoil > 0.0F) { + recoilCount = 30; + recoilValue = set.definition().recoil; + recoilYaw = set.rotationYaw; + } + if (clientSide && used) { + muzzleEffects(set, mount, yaw); + } + set.tickRecoil(mount, used); + bit++; + } + set.tick(anyUsed); + if (!clientSide) { + ServerFiring.tick(vehicle, set); + } + updateRotation(set); + } + if (!clientSide) { + useStat = 0; + syncAmmo(); + } else { + for (Guidance g : guidance) { + if (g != null) { + g.tick(); + } + } + } + if (recoilCount > 0) { + recoilCount--; + } + tickBays(); + if (clientSide) { + smoke(); + } + flares.tick(); + } + + private int usedStat() { + int stat = 0; + int bit = 0; + for (WeaponSet set : sets) { + for (int mount = 0; mount < set.mounts().size() && bit < 32; mount++) { + stat |= set.isUsed(mount) ? 1 << bit : 0; + bit++; + } + } + return stat; + } + + /** Turret aim of one set, following whoever operates it, within the mount's traverse limits. */ + private void updateRotation(WeaponSet set) { + if (vehicle.isDestroyed()) { + return; + } + WeaponMount mount = set.firstMount(); + int seat = mount.seatIndex(); + Entity operator = vehicle.occupant(seat); + if (mount.usableByPilot() && !(operator instanceof Player)) { + operator = vehicle.occupant(0); + } + float hullYaw = Mth.wrapDegrees(vehicle.getYRot()); + float hullPitch = Mth.wrapDegrees(vehicle.getXRot()); + float turretYaw = vehicle.turretYaw(); + if (operator instanceof Player) { + if ((int) mount.minYaw() != 0 || (int) mount.maxYaw() != 0) { + float ty = mount.turret() ? turretYaw : 0.0F; + float ey = Mth.wrapDegrees(operator.getYRot() - hullYaw - mount.defaultYaw() - ty); + if (Math.abs((int) mount.minYaw()) < 360 && Math.abs((int) mount.maxYaw()) < 360) { + float targetYaw = Mth.clamp(ey, mount.minYaw(), mount.maxYaw()); + float wy = set.rotationYaw - mount.defaultYaw() - ty; + if (targetYaw < wy) { + wy = wy - targetYaw > 15.0F ? wy - 15.0F : targetYaw; + } else if (targetYaw > wy) { + wy = targetYaw - wy > 15.0F ? wy + 15.0F : targetYaw; + } + set.rotationYaw = wy + mount.defaultYaw() + ty; + } else { + set.rotationYaw = ey + ty; + } + } + float ep = Mth.wrapDegrees(operator.getXRot() - hullPitch); + set.rotationPitch = Mth.clamp(ep, mount.minPitch(), mount.maxPitch()); + set.rotationTurretYaw = 0.0F; + } else { + set.rotationTurretYaw = turretYaw; + } + } + + /** {@code updateRecoil}: the hull kick of a big gun, applied on the simulating side. */ + public float @Nullable [] recoilKick() { + if (recoilCount < 12) { + return null; + } + float rel = (float) Math.toRadians(recoilYaw - vehicle.roll()); + float kick = Mth.cos((float) Math.toRadians(recoilCount * 6)) * recoilValue; + return new float[]{kick * Mth.cos(rel), kick * Mth.sin(rel)}; + } + + /** {@code updateWeaponBay}: bays swing open while a seated crew member has their weapon selected. */ + private void tickBays() { + for (int i = 0; i < bayAngle.length; i++) { + WeaponBayPart bay = def.weaponBays.get(i); + boolean open = false; + for (String weapon : bay.weapons()) { + int id = indexOf(weapon); + for (int seat = 0; seat < selected.length && id >= 0; seat++) { + if (selected[seat] == id && vehicle.occupant(seat) != null) { + open = true; + } + } + } + prevBayAngle[i] = bayAngle[i]; + bayAngle[i] = Mth.clamp(bayAngle[i] + (open ? 3.0F : -3.0F), 0.0F, 90.0F); + } + } + + public float bayAngle(int bay, float partialTicks) { + return bay < bayAngle.length ? Mth.lerp(partialTicks, prevBayAngle[bay], bayAngle[bay]) : 0.0F; + } + + // Effects, client only + + /** {@code spawnParticleMuzzleFlash} plus the spent case of guns with {@code SetCartridge}. */ + private void muzzleEffects(WeaponSet set, int mount, float hullYaw) { + Level level = vehicle.level(); + WeaponDefinition w = set.definition(); + Vec3 offset = muzzleOffset(set, mount, 1.0F); + double px = vehicle.getX() + offset.x; + double py = vehicle.getY() + offset.y; + double pz = vehicle.getZ() + offset.z; + Vec3 dir = Vec3.directionFromRotation(vehicle.getXRot() + set.rotationPitch, hullYaw + set.rotationYaw); + for (MuzzleFlash mf : w.muzzleSmokes) { + double x = px + dir.x * mf.distance(); + double y = py + dir.y * mf.distance(); + double z = pz + dir.z * mf.distance(); + for (int i = 0; i < mf.count(); i++) { + double r = mf.range(); + level.addParticle(mf.size() >= 2.0F ? ParticleTypes.LARGE_SMOKE : ParticleTypes.SMOKE, + x + (random.nextDouble() - 0.5) * r, y + (random.nextDouble() - 0.5) * r, z + (random.nextDouble() - 0.5) * r, + (random.nextDouble() - 0.5) * 0.05, 0.02, (random.nextDouble() - 0.5) * 0.05); + } + } + for (MuzzleFlash mf : w.muzzleFlashes) { + int rgb = mf.color().argb() & 0xFFFFFF; + level.addParticle(new DustParticleOptions(Vec3.fromRGB24(rgb).toVector3f(), Mth.clamp(mf.size() * 4.0F, 0.5F, 4.0F)), + px + dir.x * mf.distance(), py + dir.y * mf.distance(), pz + dir.z * mf.distance(), 0.0, 0.0, 0.0); + } + if (w.cartridge != null) { + Vec3 motion = vehicle.getDeltaMovement(); + Vec3 side = Vec3.directionFromRotation(0.0F, hullYaw + set.rotationYaw + w.cartridge.yaw() + 90.0F) + .scale(0.1 + w.cartridge.acceleration()); + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, new ItemStack(Items.GOLD_NUGGET)), + px, py, pz, motion.x + side.x, motion.y + 0.1, motion.z + side.z); + } + } + + /** {@code onUpdate_ParticleSmoke}: smoke generators trail while the pilot has them selected. */ + private void smoke() { + WeaponSet set = current(0); + if (set == null || set.definition().type != WeaponType.SMOKE || vehicle.visibleThrottle() <= 0.1F) { + return; + } + WeaponDefinition w = set.definition(); + Vec3 back = Vec3.directionFromRotation(vehicle.getXRot(), vehicle.getYRot()).reverse(); + int rgb = w.color.argb() & 0xFFFFFF; + for (int mount = 0; mount < set.mounts().size(); mount++) { + if (random.nextFloat() > vehicle.visibleThrottle() * 1.5F) { + continue; + } + Vec3 offset = muzzleOffset(set, mount, 1.0F); + for (int i = 0; i < w.smokeNum; i++) { + vehicle.level().addParticle(new DustParticleOptions(Vec3.fromRGB24(rgb).toVector3f(), Mth.clamp(w.smokeSize * 2.0F, 0.5F, 4.0F)), + vehicle.getX() + offset.x + back.x, vehicle.getY() + offset.y + back.y, vehicle.getZ() + offset.z + back.z, + back.x * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.y * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.z * w.acceleration + (random.nextDouble() - 0.5) * 0.2); + } + } + } + + // Ammunition + + /** Sends magazine and reserve counts to the crew whenever they change. Server only. */ + private void syncAmmo() { + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + if (set.ammo() != sentAmmo[id] || set.restAmmo() != sentRest[id]) { + sentAmmo[id] = set.ammo(); + sentRest[id] = set.restAmmo(); + WeaponStatePacket packet = new WeaponStatePacket(vehicle.getId(), id, set.ammo(), set.restAmmo()); + for (Entity passenger : vehicle.getPassengers()) { + if (passenger instanceof ServerPlayer player) { + ModNetwork.sendToPlayer(player, packet); + } + } + } + } + } + + /** Forces a full resend, for a player who just boarded. */ + public void resendAmmo() { + Arrays.fill(sentAmmo, -1); + } + + public void applyAmmo(int id, int ammo, int rest) { + WeaponSet set = set(id); + if (set != null) { + set.setAmmo(ammo); + set.setRestAmmo(rest); + } + } + + /** {@code reloadAllWeapon}: tops up every magazine from its reserve. */ + public void reloadAll() { + for (WeaponSet set : sets) { + set.reloadMagazine(); + } + } + + public int[] saveAmmo() { + int[] out = new int[sets.size() * 2]; + for (int i = 0; i < sets.size(); i++) { + out[i * 2] = sets.get(i).ammo(); + out[i * 2 + 1] = sets.get(i).restAmmo(); + } + return out; + } + + public void loadAmmo(int[] saved) { + for (int i = 0; i < sets.size() && i * 2 + 1 < saved.length; i++) { + sets.get(i).setAmmo(Math.min(saved[i * 2], sets.get(i).magazineSize())); + sets.get(i).setRestAmmo(saved[i * 2 + 1]); + } + } +} From cb294af9962786a4ad2b152391098fda61f9729f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:13 +0800 Subject: [PATCH 1314/1461] Add weapon/WeaponSet.java to the 1.20.6 shared sources --- .../jnhprojects/mcheli/weapon/WeaponSet.java | 505 ++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java new file mode 100644 index 00000000..3f539030 --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java @@ -0,0 +1,505 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.WeaponSound; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * One selectable weapon of a vehicle: every mount that fires the same weapon, with the magazine, + * reload, heat and recoil they share. A port of {@code MCH_WeaponSet} together with the parts of + * {@code MCH_WeaponBase} and {@code MCH_WeaponCreator} that are not specific to a weapon type. + * + *

Both sides keep one. The server's is authoritative for ammunition and decides what is fired; + * the client's runs with the slightly longer intervals the original used there, so the client never + * asks to fire faster than the server accepts. + */ +public final class WeaponSet { + private static final int SWITCH_WAIT = 15; + + /** Fires one mount. Returns whether the shot happened, which consumes a round. */ + @FunctionalInterface + public interface Trigger { + boolean fire(WeaponSet set, int mount, Shot shot); + } + + private final RandomSource random = RandomSource.create(); + private final WeaponDefinition def; + private final List mounts; + private final boolean clientSide; + + public final int interval; + public final int heatCount; + private final int numMode; + + private int currentMount; + private int mode; + private int ammo; + private int restAmmo; + public int currentHeat; + private int cooldownSpeed = 1; + public int countWait; + public int countReloadWait; + private final int[] lastUsedCount; + /** Ticks since each mount last fired, {@code MCH_WeaponBase.tick}. */ + final int[] ticks; + public int soundWait; + /** Whether the shot being fired may play its sound, see {@code SoundDelay}. */ + public boolean soundAllowed = true; + /** The vehicle's own sound for this weapon, from {@code WeaponSound}. */ + public @Nullable WeaponSound ownSound; + + /** Aim relative to the hull, following the crew member who operates the weapon. */ + public float rotationYaw; + public float rotationPitch; + public float prevRotationYaw; + public float prevRotationPitch; + public final float defaultRotationYaw; + /** How far behind the pilot's view a turret weapon lags, for drawing. */ + public float rotationTurretYaw; + + public float barrelSpeed; + public float barrel; + public float prevBarrel; + private final Recoil[] recoil; + + // Close air support run, server only. See ServerFiring. + @Nullable Entity casUser; + @Nullable Entity casShooter; + double casX; + double casY; + double casZ; + int casDirection; + int casStartTick; + int casCount = 3; + + public WeaponSet(WeaponDefinition def, List mounts, boolean clientSide) { + this.def = def; + this.mounts = List.copyOf(mounts); + this.clientSide = clientSide; + this.lastUsedCount = new int[mounts.size()]; + this.ticks = new int[mounts.size()]; + this.recoil = new Recoil[mounts.size()]; + for (int i = 0; i < recoil.length; i++) { + recoil[i] = new Recoil(def.recoilBufCount, def.recoilBufCountSpeed); + } + this.defaultRotationYaw = mounts.isEmpty() ? 0.0F : mounts.getFirst().defaultYaw(); + this.rotationYaw = defaultRotationYaw; + this.prevRotationYaw = defaultRotationYaw; + + int wait = def.delay; + if (def.type == WeaponType.CAS && wait > -250) { + wait = -250; + } + int heat = def.heatCount; + if (def.maxHeatCount > 0 && heat < 2) { + heat = 2; + } + if (clientSide) { + wait = clientInterval(wait); + heat++; + } + this.interval = wait; + this.heatCount = heat; + this.numMode = def.type == WeaponType.MACHINE_GUN_2 && def.explosion == 0 ? 0 : def.modeNum; + } + + /** The original gave the client longer intervals so its requests never outran the server. */ + private static int clientInterval(int wait) { + if (wait < 4) { + return wait + 1; + } else if (wait < 7) { + return wait + 2; + } else if (wait < 10) { + return wait + 3; + } else if (wait < 20) { + return wait + 6; + } + wait += 10; + return wait >= 40 ? -wait : wait; + } + + public WeaponDefinition definition() { + return def; + } + + public String name() { + return def.name(); + } + + public List mounts() { + return mounts; + } + + public WeaponMount firstMount() { + return mounts.getFirst(); + } + + public int currentMount() { + return currentMount; + } + + // Ammunition + + public int ammo() { + return ammo; + } + + public int magazineSize() { + return def.round; + } + + public int restAmmo() { + return restAmmo; + } + + public int maxAmmo() { + return def.maxAmmo; + } + + public void setAmmo(int n) { + ammo = n; + } + + /** Everything not in the magazine, capped so magazine plus reserve never exceed {@code MaxAmmo}. */ + public void setRestAmmo(int n) { + restAmmo = Math.min(n, def.maxAmmo - ammo); + } + + /** {@code supplyRestAllAmmo}: one resupply adds {@code SuppliedNum} rounds. */ + public boolean supply() { + if (restAmmo + ammo < def.maxAmmo) { + setRestAmmo(restAmmo + ammo + def.suppliedNum); + return true; + } + return false; + } + + public boolean isInPreparation() { + return countWait < 0 || countReloadWait > 0; + } + + public boolean isReloading() { + return countReloadWait > 0; + } + + public boolean canUse() { + return countWait == 0; + } + + public boolean isLongDelay() { + return def.delay > 4; + } + + public boolean isOverheated() { + return def.maxHeatCount > 0 && currentHeat >= def.maxHeatCount; + } + + public void reload() { + if (magazineSize() > 0 && ammo < magazineSize() && def.reloadTime > 0) { + countReloadWait = def.reloadTime; + if (clientSide) { + ammo = 0; + } else { + countReloadWait = Math.max(countReloadWait - 20, 1); + } + } + } + + public void reloadMagazine() { + int needed = magazineSize() - ammo; + if (needed > 0) { + needed = Math.min(needed, restAmmo); + ammo += needed; + setRestAmmo(restAmmo - needed); + } + } + + // Modes + + public int mode() { + return def.fixMode > 0 ? def.fixMode : mode; + } + + public boolean switchMode() { + if (def.fixMode > 0) { + return false; + } + int before = mode(); + mode = numMode > 0 ? (mode() + 1) % numMode : 0; + if (before == mode()) { + return false; + } + waitAfterSwitch(SWITCH_WAIT); + return true; + } + + /** {@code getName}: the display name with the mode suffix the weapon class added. */ + public String displayName() { + String suffix = switch (def.type) { + case MACHINE_GUN_2 -> mode() == 0 ? "" : " [HE]"; + case ROCKET -> mode() == 0 ? "" : " [HEIAP]"; + case AT_MISSILE -> mode() == 1 ? " [TA]" : ""; + case TV_MISSILE -> switch (mode()) { + case 0 -> " [TV]"; + case 2 -> " [TA]"; + default -> ""; + }; + case null, default -> ""; + }; + return def.displayName + suffix; + } + + public void onSwitchWeapon(boolean infiniteAmmo) { + waitAfterSwitch(clientSide ? SWITCH_WAIT + 10 : SWITCH_WAIT); + currentMount = 0; + if (infiniteAmmo) { + ammo = magazineSize(); + } + } + + private void waitAfterSwitch(int wait) { + if (countWait >= -wait) { + countWait = countWait > wait ? -countWait : -wait; + } + } + + /** Another weapon of the same {@code Group} fired; this one waits and reloads with it. */ + public void waitForGroup(boolean reload) { + countWait = interval; + countReloadWait = 0; + if (reload && magazineSize() > 0 && def.reloadTime > 0) { + countReloadWait = clientSide ? def.reloadTime : Math.max(def.reloadTime - 20, 1); + } + } + + /** + * {@code getLandInDistance}: how far out, measured flat, a shell fired now would come down, + * stepping its arc a tick at a time as the projectile does; -1 if it would not come down in + * range. Mortars show it on the HUD. + */ + public double landingDistance(Entity shooter) { + if (def.gravity >= 0.0F || mounts.isEmpty()) { + return -1.0; + } + WeaponMount mount = mounts.get(currentMount); + float yaw = Mth.wrapDegrees(shooter.getYRot() + rotationYaw + mount.yaw()); + float pitch = Mth.wrapDegrees(shooter.getXRot() + rotationPitch + mount.pitch()); + Vec3 dir = Vec3.directionFromRotation(pitch, yaw); + double acc = Math.min(def.acceleration, 4.0F); + double my = dir.y * def.acceleration; + if (my <= 0.0 || acc <= 0.0) { + return -1.0; + } + double mx = dir.x * def.acceleration; + double mz = dir.z * def.acceleration; + double gravity = def.gravity * (def.acceleration / acc); + double steps = my / def.gravity; + if (steps < -12.0) { + double f = steps / -12.0; + mx *= f; + my *= f; + mz *= f; + gravity *= f * f * 0.95; + } + Level level = shooter.level(); + double x = shooter.getX(); + double y = shooter.getY() + 3.0; + double z = shooter.getZ(); + for (int i = 0; i < 50; i++) { + Vec3 from = new Vec3(x, y, z); + Vec3 to = new Vec3(x + mx, y + my, z + mz); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shooter)); + if (hit.getType() == HitResult.Type.BLOCK) { + return Math.hypot(hit.getBlockPos().getX() - shooter.getX(), hit.getBlockPos().getZ() - shooter.getZ()); + } + my += gravity; + x += mx; + y += my; + z += mz; + if (y < shooter.getY()) { + return Math.hypot(x - shooter.getX(), z - shooter.getZ()); + } + } + return -1.0; + } + + // Firing + + /** + * Fires the next mount if the set is ready. The aim is the shooter's rotation plus this set's + * turret aim and the mount's fixed offset, scattered by {@code Accuracy}. + */ + public boolean use(Shot shot, Trigger trigger) { + if (magazineSize() > 0 && ammo <= 0 || isOverheated()) { + return false; + } + WeaponMount mount = mounts.get(currentMount); + soundAllowed = soundWait == 0; + float yaw = shot.shooter.getYRot() + rotationYaw + mount.yaw(); + float pitch = shot.shooter.getXRot() + rotationPitch + mount.pitch(); + if (def.accuracy > 0.0F) { + yaw += (random.nextFloat() - 0.5F) * def.accuracy; + pitch += (random.nextFloat() - 0.5F) * def.accuracy; + } + shot.yaw = Mth.wrapDegrees(yaw); + shot.pitch = Mth.wrapDegrees(pitch); + if (!trigger.fire(this, currentMount, shot)) { + return false; + } + ticks[currentMount] = 0; + + if (def.maxHeatCount > 0) { + cooldownSpeed = 1; + currentHeat += heatCount; + if (currentHeat >= def.maxHeatCount) { + currentHeat += 30; + } + } + if (def.soundDelay > 0 && soundWait == 0) { + soundWait = def.soundDelay; + } + lastUsedCount[currentMount] = Math.abs(interval); + if (isCooldownReloadTime() && def.reloadTime - 10 > lastUsedCount[currentMount]) { + lastUsedCount[currentMount] = def.reloadTime - 10; + } + currentMount = (currentMount + 1) % mounts.size(); + countWait = interval; + countReloadWait = 0; + if (ammo > 0) { + ammo--; + } + if (ammo <= 0) { + if (shot.infiniteAmmo && restAmmo < magazineSize()) { + setRestAmmo(magazineSize()); + } + reload(); + shot.reload = true; + } + return true; + } + + /** Missiles keep their mount empty for the whole reload, which hides the missile part meanwhile. */ + private boolean isCooldownReloadTime() { + return def.type == WeaponType.AA_MISSILE || def.type == WeaponType.AS_MISSILE || def.type == WeaponType.AT_MISSILE; + } + + /** Whether {@code mount} fired recently enough to show muzzle effects or keep a missile hidden. */ + public boolean isUsed(int mount) { + if (mount >= lastUsedCount.length) { + return false; + } + int count = lastUsedCount[mount]; + return interval >= 4 && count > interval / 2 || count >= 4; + } + + public int ticksSinceShot(int mount) { + return ticks[mount]; + } + + // Ticking + + /** {@code MCH_WeaponSet.update}, once per tick on both sides. */ + public void tick(boolean used) { + if (countReloadWait > 0) { + countReloadWait--; + if (countReloadWait == 0) { + reloadMagazine(); + } + } + for (int i = 0; i < lastUsedCount.length; i++) { + if (lastUsedCount[i] > 0) { + if (lastUsedCount[i] != 4 || currentMount == 0 && canUse() && (ammo > 0 || def.maxAmmo <= 0)) { + lastUsedCount[i]--; + } + } + } + if (currentHeat > 0) { + if (currentHeat < def.maxHeatCount) { + cooldownSpeed++; + } + currentHeat = Math.max(currentHeat - (cooldownSpeed / 20 + 1), 0); + } + if (countWait > 0) { + countWait--; + } else if (countWait < 0) { + countWait++; + } + prevRotationYaw = rotationYaw; + prevRotationPitch = rotationPitch; + if (countWait != 0) { + for (int i = 0; i < ticks.length; i++) { + ticks[i]++; + } + } + if (soundWait > 0) { + soundWait--; + } + + if (used && barrelSpeed < 75.0F) { + barrelSpeed = Math.min(barrelSpeed + 25 + random.nextInt(3), 74.0F); + } + prevBarrel = barrel; + barrel += barrelSpeed; + if (barrel >= 360.0F) { + barrel -= 360.0F; + prevBarrel -= 360.0F; + } + if (barrelSpeed > 0.0F) { + barrelSpeed = Math.max(barrelSpeed - 1.48F, 0.0F); + } + } + + /** {@code updateWeapon}: the recoil kick of one mount. */ + public void tickRecoil(int mount, boolean used) { + Recoil r = recoil[mount]; + r.prev = r.value; + if (used && r.count <= 0) { + r.count = r.max; + } + if (r.count > 0) { + if (r.max <= 1) { + r.value = 1.0F; + } else if (r.max == 2) { + r.value = r.count == 2 ? 1.0F : 0.6F; + } else { + if (r.count > r.max / 2) { + r.count -= r.speed; + } + r.value = Mth.sin((float) r.count / r.max * Mth.PI); + } + r.count--; + } else { + r.value = 0.0F; + } + } + + public float recoil(int mount, float partialTicks) { + Recoil r = recoil[Math.min(mount, recoil.length - 1)]; + return Mth.lerp(partialTicks, r.prev, r.value); + } + + private static final class Recoil { + final int max; + final int speed; + int count; + float value; + float prev; + + Recoil(int max, int speed) { + this.max = max; + this.speed = speed; + } + } +} From 3490cd955425f1dd8ba4a4c495423c845c98cc4e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:13 +0800 Subject: [PATCH 1315/1461] Add workshop/TestFlight.java to the 1.20.6 shared sources --- .../mcheli/workshop/TestFlight.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java diff --git a/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java new file mode 100644 index 00000000..4fe57c2c --- /dev/null +++ b/minecraft-1.20.6/src/main/java/com/jnhprojects/mcheli/workshop/TestFlight.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.workshop; + +import com.jnhprojects.mcheli.ContentReload; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.phys.Vec3; + +/** + * Flying a vehicle straight from the workshop. The pack was just saved on the machine this server + * runs on, so loading it again is enough. That only holds for the owner of a single player or LAN + * game: a dedicated server does not get a pack until an operator approves it, and then it is no + * longer a test, so the screen does not offer it there and the server refuses it too. + */ +public final class TestFlight { + /** How far up to look for room when the vehicle does not fit where the player stands. */ + private static final int MAX_RISE = 48; + + private TestFlight() { + } + + public static void start(ServerPlayer player, String vehicle) { + MinecraftServer server = player.level().getServer(); + if (!server.isSingleplayerOwner(player.getGameProfile())) { + player.displayClientMessage(Component.translatable("message.mcheli.workshop.test_flight_owner_only"), true); + return; + } + if (player.isPassenger()) { + player.displayClientMessage(Component.translatable("message.mcheli.workshop.test_flight_riding"), true); + return; + } + ContentReload.reload(server); + AircraftDefinition def = MCHeli.content().any(vehicle); + if (def == null) { + player.sendSystemMessage(Component.translatable("message.mcheli.workshop.test_flight_no_vehicle", vehicle)); + MCHeli.packs().reportProblems(player::sendSystemMessage); + return; + } + ServerLevel level = player.serverLevel(); + AircraftEntity aircraft = ModEntities.typeFor(def).create(level); + if (aircraft == null) { + return; + } + aircraft.setType(def); + Vec3 start = player.position(); + for (int rise = 0; rise <= MAX_RISE; rise++) { + aircraft.moveTo(start.x, start.y + rise, start.z, player.getYRot(), 0.0F); + if (level.noCollision(aircraft, aircraft.getBoundingBox())) { + break; + } + } + aircraft.startTestFlight(start); + level.addFreshEntity(aircraft); + player.startRiding(aircraft, true); + } +} From eeffdc124b08079273afff6142d5208f95d19346 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:14 +0800 Subject: [PATCH 1316/1461] Add assets/mcheli/blockstates/drafting_table.json to the 1.20.6 shared sources --- .../resources/assets/mcheli/blockstates/drafting_table.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/assets/mcheli/blockstates/drafting_table.json diff --git a/minecraft-1.20.6/src/main/resources/assets/mcheli/blockstates/drafting_table.json b/minecraft-1.20.6/src/main/resources/assets/mcheli/blockstates/drafting_table.json new file mode 100644 index 00000000..82992af9 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/assets/mcheli/blockstates/drafting_table.json @@ -0,0 +1,5 @@ +{ + "multipart": [ + { "apply": { "model": "mcheli:block/drafting_table" } } + ] +} From f60140d9550ef7338fee49dd22f93a79f843a194 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:14 +0800 Subject: [PATCH 1317/1461] Add assets/mcheli/models/block/drafting_table.json to the 1.20.6 shared sources --- .../mcheli/models/block/drafting_table.json | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/assets/mcheli/models/block/drafting_table.json diff --git a/minecraft-1.20.6/src/main/resources/assets/mcheli/models/block/drafting_table.json b/minecraft-1.20.6/src/main/resources/assets/mcheli/models/block/drafting_table.json new file mode 100644 index 00000000..8dfa5bcc --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/assets/mcheli/models/block/drafting_table.json @@ -0,0 +1,27 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "minecraft:block/oak_planks", + "legs": "minecraft:block/iron_block", + "board": "minecraft:block/oak_planks", + "paper": "minecraft:block/white_concrete" + }, + "elements": [ + { "from": [1, 0, 2], "to": [2.5, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 2], "to": [15, 10, 3.5], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [1, 0, 12.5], "to": [2.5, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [13.5, 0, 12.5], "to": [15, 10, 14], "faces": { + "north": {"texture": "#legs"}, "south": {"texture": "#legs"}, "east": {"texture": "#legs"}, "west": {"texture": "#legs"}, "down": {"texture": "#legs"} } }, + { "from": [0, 10, 1], "to": [16, 11.5, 15], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { + "north": {"texture": "#board"}, "south": {"texture": "#board"}, "east": {"texture": "#board"}, "west": {"texture": "#board"}, + "up": {"texture": "#board"}, "down": {"texture": "#board"} } }, + { "from": [2, 11.5, 2.5], "to": [14, 11.6, 13.5], + "rotation": { "origin": [8, 10.5, 8], "axis": "x", "angle": -22.5 }, + "faces": { "up": {"texture": "#paper"} } } + ] +} From 1b31c9d2bba8f64d1c0f4ee5d0b0044dfe94a64e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:15 +0800 Subject: [PATCH 1318/1461] Add assets/mcheli/models/item/drafting_table.json to the 1.20.6 shared sources --- .../resources/assets/mcheli/models/item/drafting_table.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/assets/mcheli/models/item/drafting_table.json diff --git a/minecraft-1.20.6/src/main/resources/assets/mcheli/models/item/drafting_table.json b/minecraft-1.20.6/src/main/resources/assets/mcheli/models/item/drafting_table.json new file mode 100644 index 00000000..355ce4a7 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/assets/mcheli/models/item/drafting_table.json @@ -0,0 +1,3 @@ +{ + "parent": "mcheli:block/drafting_table" +} From 6300533bb50273b76ab381658de68242d48d6297 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:15 +0800 Subject: [PATCH 1319/1461] Add assets/mcheli/particles/glow.json to the 1.20.6 shared sources --- .../src/main/resources/assets/mcheli/particles/glow.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/assets/mcheli/particles/glow.json diff --git a/minecraft-1.20.6/src/main/resources/assets/mcheli/particles/glow.json b/minecraft-1.20.6/src/main/resources/assets/mcheli/particles/glow.json new file mode 100644 index 00000000..296e5397 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/assets/mcheli/particles/glow.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "mcheli:glow" + ] +} From d10efe33817da4d069fa734caa70fab905d767c1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:16 +0800 Subject: [PATCH 1320/1461] Add assets/mcheli/particles/smoke.json to the 1.20.6 shared sources --- .../resources/assets/mcheli/particles/smoke.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/assets/mcheli/particles/smoke.json diff --git a/minecraft-1.20.6/src/main/resources/assets/mcheli/particles/smoke.json b/minecraft-1.20.6/src/main/resources/assets/mcheli/particles/smoke.json new file mode 100644 index 00000000..977381be --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/assets/mcheli/particles/smoke.json @@ -0,0 +1,16 @@ +{ + "textures": [ + "minecraft:big_smoke_0", + "minecraft:big_smoke_1", + "minecraft:big_smoke_2", + "minecraft:big_smoke_3", + "minecraft:big_smoke_4", + "minecraft:big_smoke_5", + "minecraft:big_smoke_6", + "minecraft:big_smoke_7", + "minecraft:big_smoke_8", + "minecraft:big_smoke_9", + "minecraft:big_smoke_10", + "minecraft:big_smoke_11" + ] +} From 8b5903fb1ea47948004de89175ea29d887365a7f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:16 +0800 Subject: [PATCH 1321/1461] Add assets/mcheli/shaders/post/thermal.json to the 1.20.6 shared sources --- .../assets/mcheli/shaders/post/thermal.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/assets/mcheli/shaders/post/thermal.json diff --git a/minecraft-1.20.6/src/main/resources/assets/mcheli/shaders/post/thermal.json b/minecraft-1.20.6/src/main/resources/assets/mcheli/shaders/post/thermal.json new file mode 100644 index 00000000..10f9a6cc --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/assets/mcheli/shaders/post/thermal.json @@ -0,0 +1,17 @@ +{ + "targets": [ + "swap" + ], + "passes": [ + { + "name": "mcheli_thermal", + "intarget": "minecraft:main", + "outtarget": "swap" + }, + { + "name": "blit", + "intarget": "swap", + "outtarget": "minecraft:main" + } + ] +} From c0aee9708a61ab26b38fa9b5e4da12df5da67c66 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:17 +0800 Subject: [PATCH 1322/1461] Add assets/mcheli/textures/particle/glow.png to the 1.20.6 shared sources --- .../assets/mcheli/textures/particle/glow.png | Bin 0 -> 705 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-1.20.6/src/main/resources/assets/mcheli/textures/particle/glow.png diff --git a/minecraft-1.20.6/src/main/resources/assets/mcheli/textures/particle/glow.png b/minecraft-1.20.6/src/main/resources/assets/mcheli/textures/particle/glow.png new file mode 100644 index 0000000000000000000000000000000000000000..339b97292b3a86cec21a5a2c2d4e9159da1241c6 GIT binary patch literal 705 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFm-skIEGZjoxNt=CF&@0?BmDQ z$G05=nLjo)u{~0d=sc02G=rmQ;>?7sgruZLOlR&iZR9TC);p3q<+=OyiMy`WR_B%6 ze!zcpW%0hxt6uJ`UOHv3s-)z$2S*IQ{M+2aBkl2V^1+p-UiNMId~?@~iHS`VvGKGtbJ(?@PMX`Q_JU)-AEGyIw3c zQrRtbha-<|yVkj!cRxOeDwmZ>DLa>nOo)xwXnye(C@s6DY4^dsUB4m0R;efKDR26HaPb4r4<;3I zGNE&B-ud`Y+f9cz$|Q5W??V51i?`b=Utv|8`!Zyb_`%f=Ny@jd)@AxOp)i%w?!a<|&&Tjr8oN`JQToU~(`->fg1 zZdP9Nqf5}cIn2IwE7PsJ)h7Sugyi*n%i6MQ@`K~}e`S@^nPlVK#j7xFV z+_j8uPnwhRR~zlgnR{cs;OihI&w3VnZD^oOJGP2v)|RI;bPnXz1l3;M6Sw&D^QW!V zDOSc=cfEdS|A;M20|xneiH#;17nkTB-(PMJ>hSJ9`!Q!Lt-~I+X1}kkT&Mak7@_E? z^^gDD5jm{g_J>^m^K5+4zM_`ByS7X6x8|EK^8>{;-q`=xrFCc5-2VzEHJ@mS{q(#h zwlHh)?U((R_Hk6-{(1h>N~aR1kZ_?NOSWy;`Ple&>FE<27@gj_D0q6`GXA*nq@huc lV^{v7@AFerZkX0Ht_;k6d;0MSU0~8=@O1TaS?83{1OQn@Sb6{e literal 0 HcmV?d00001 From efa9f86daee2e86b7f7b16088c1767c994cbe077 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:17 +0800 Subject: [PATCH 1323/1461] Add assets/minecraft/shaders/program/mcheli_thermal.fsh to the 1.20.6 shared sources --- .../shaders/program/mcheli_thermal.fsh | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.fsh diff --git a/minecraft-1.20.6/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.fsh b/minecraft-1.20.6/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.fsh new file mode 100644 index 00000000..4a070551 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.fsh @@ -0,0 +1,32 @@ +#version 150 + +// Thermal view, white hot. A post shader only sees colours, so heat is faked: bright and warm +// colours (fire, lava, skin, lamps) read hot, the sky and water cold, and outlines are drawn +// where the brightness changes sharply, like the original's "pencil" shader. + +uniform sampler2D DiffuseSampler; + +in vec2 texCoord; +in vec2 oneTexel; + +out vec4 fragColor; + +const vec3 Gray = vec3(0.3, 0.59, 0.11); + +float heat(vec3 c) { + float luma = dot(c, Gray); + float warmth = clamp(c.r - c.b, 0.0, 1.0); + return clamp(luma * 0.7 + warmth * 0.6, 0.0, 1.0); +} + +void main() { + float c = heat(texture(DiffuseSampler, texCoord).rgb); + float l = heat(texture(DiffuseSampler, texCoord - vec2(oneTexel.x, 0.0)).rgb); + float r = heat(texture(DiffuseSampler, texCoord + vec2(oneTexel.x, 0.0)).rgb); + float u = heat(texture(DiffuseSampler, texCoord - vec2(0.0, oneTexel.y)).rgb); + float d = heat(texture(DiffuseSampler, texCoord + vec2(0.0, oneTexel.y)).rgb); + float edge = clamp(abs(r - l) + abs(d - u), 0.0, 1.0); + + float value = smoothstep(0.08, 0.85, c) + edge * 0.6; + fragColor = vec4(vec3(clamp(value, 0.0, 1.0)), 1.0); +} From 50e96fecaf0733632a2aa9671844b4576a0287d9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:17 +0800 Subject: [PATCH 1324/1461] Add assets/minecraft/shaders/program/mcheli_thermal.json to the 1.20.6 shared sources --- .../shaders/program/mcheli_thermal.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.json diff --git a/minecraft-1.20.6/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.json b/minecraft-1.20.6/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.json new file mode 100644 index 00000000..bb338e9f --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/assets/minecraft/shaders/program/mcheli_thermal.json @@ -0,0 +1,18 @@ +{ + "blend": { + "func": "add", + "srcrgb": "one", + "dstrgb": "zero" + }, + "vertex": "sobel", + "fragment": "mcheli_thermal", + "attributes": [ "Position" ], + "samplers": [ + { "name": "DiffuseSampler" } + ], + "uniforms": [ + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "InSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }, + { "name": "OutSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] } + ] +} From d865b903bebcc35db6d5da9bbd7d7b1231b8469c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:18 +0800 Subject: [PATCH 1325/1461] Add data/mcheli/damage_type/bullet.json to the 1.20.6 shared sources --- .../src/main/resources/data/mcheli/damage_type/bullet.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/damage_type/bullet.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/damage_type/bullet.json b/minecraft-1.20.6/src/main/resources/data/mcheli/damage_type/bullet.json new file mode 100644 index 00000000..92ee6766 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/damage_type/bullet.json @@ -0,0 +1,5 @@ +{ + "exhaustion": 0.1, + "message_id": "mcheli.bullet", + "scaling": "when_caused_by_living_non_player" +} From 9e03040e86646bc010af29209b5437451378af1f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:18 +0800 Subject: [PATCH 1326/1461] Add data/mcheli/loot_tables/blocks/drafting_table.json to the 1.20.6 shared sources --- .../mcheli/loot_tables/blocks/drafting_table.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/loot_tables/blocks/drafting_table.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/loot_tables/blocks/drafting_table.json b/minecraft-1.20.6/src/main/resources/data/mcheli/loot_tables/blocks/drafting_table.json new file mode 100644 index 00000000..610afa08 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/loot_tables/blocks/drafting_table.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { "type": "minecraft:item", "name": "mcheli:drafting_table" } + ], + "conditions": [ + { "condition": "minecraft:survives_explosion" } + ] + } + ] +} From b6fc2c5162ca846a532a92f7cd80c1be86c3b9f0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:19 +0800 Subject: [PATCH 1327/1461] Add data/mcheli/recipes/chain.json to the 1.20.6 shared sources --- .../main/resources/data/mcheli/recipes/chain.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/chain.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/chain.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/chain.json new file mode 100644 index 00000000..92d099ca --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/chain.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "I I", + "III", + "I I" + ], + "result": { + "id": "mcheli:chain" + } +} From 9818ff342831410c159f7d5e7f79ab52f9e733a2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:19 +0800 Subject: [PATCH 1328/1461] Add data/mcheli/recipes/container.json to the 1.20.6 shared sources --- .../resources/data/mcheli/recipes/container.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/container.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/container.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/container.json new file mode 100644 index 00000000..9942f143 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/container.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": {"tag": "c:chests/wooden"}, + "I": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "CCI" + ], + "result": { + "id": "mcheli:container" + } +} From 55e38fd253a1c126c9556ac4a6ec11eb4c025a67 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:20 +0800 Subject: [PATCH 1329/1461] Add data/mcheli/recipes/drafting_table.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/drafting_table.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/drafting_table.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/drafting_table.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/drafting_table.json new file mode 100644 index 00000000..4429cf70 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/drafting_table.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": {"item": "minecraft:crafting_table"}, + "F": {"tag": "minecraft:wooden_fences"}, + "P": {"tag": "minecraft:planks"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "R ", + "PCP", + "F F" + ], + "result": { + "id": "mcheli:drafting_table" + } +} From 402c5d2b061a744cd33a1b2ca9129a017e0f8bef Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:20 +0800 Subject: [PATCH 1330/1461] Add data/mcheli/recipes/fgm148.json to the 1.20.6 shared sources --- .../resources/data/mcheli/recipes/fgm148.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fgm148.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fgm148.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fgm148.json new file mode 100644 index 00000000..c9c7c87e --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fgm148.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:glass_blocks/colorless"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "III", + "GR " + ], + "result": { + "id": "mcheli:fgm148" + } +} From f636cc63d5366123dd07cce5fdebd99c4072e901 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:21 +0800 Subject: [PATCH 1331/1461] Add data/mcheli/recipes/fgm148_bullet.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/fgm148_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fgm148_bullet.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fgm148_bullet.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fgm148_bullet.json new file mode 100644 index 00000000..bd8a2af9 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fgm148_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:gunpowders"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + " R ", + " I ", + " G " + ], + "result": { + "count": 2, + "id": "mcheli:fgm148_bullet" + } +} From b505f4826d6a075346ab97eab75833eaab3bbbc5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:21 +0800 Subject: [PATCH 1332/1461] Add data/mcheli/recipes/fim92.json to the 1.20.6 shared sources --- .../resources/data/mcheli/recipes/fim92.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fim92.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fim92.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fim92.json new file mode 100644 index 00000000..064eac79 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fim92.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:glass_blocks/colorless"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "G ", + "III", + "RI " + ], + "result": { + "id": "mcheli:fim92" + } +} From b12616aa07f1c031c849f7ccba67f5ef168ca2ea Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:22 +0800 Subject: [PATCH 1333/1461] Add data/mcheli/recipes/fim92_bullet.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/fim92_bullet.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fim92_bullet.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fim92_bullet.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fim92_bullet.json new file mode 100644 index 00000000..29bc0018 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fim92_bullet.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:gunpowders"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "R ", + " I ", + " G" + ], + "result": { + "count": 2, + "id": "mcheli:fim92_bullet" + } +} From 08b7677a8d34cb3add2e4f06fc762c5cc7697697 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:22 +0800 Subject: [PATCH 1334/1461] Add data/mcheli/recipes/fuel.json to the 1.20.6 shared sources --- .../main/resources/data/mcheli/recipes/fuel.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fuel.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fuel.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fuel.json new file mode 100644 index 00000000..99e45469 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/fuel.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": {"item": "minecraft:coal"}, + "I": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "ICI", + "III" + ], + "result": { + "id": "mcheli:fuel" + } +} From 6eac33b77592749f93b26ae1e2e66f3d70ff9d85 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:22 +0800 Subject: [PATCH 1335/1461] Add data/mcheli/recipes/gltd.json to the 1.20.6 shared sources --- .../resources/data/mcheli/recipes/gltd.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/gltd.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/gltd.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/gltd.json new file mode 100644 index 00000000..b129a876 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/gltd.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": {"tag": "c:storage_blocks/iron"}, + "D": {"tag": "c:gems/diamond"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + " B ", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:gltd" + } +} From e83cc715d38cf45469c421c7d6769163dd8be894 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:23 +0800 Subject: [PATCH 1336/1461] Add data/mcheli/recipes/m18_green.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/m18_green.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_green.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_green.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_green.json new file mode 100644 index 00000000..66bb96dd --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_green.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"item": "minecraft:green_dye"}, + "Y": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_green", + "minecraft:custom_model_data": 1, + "minecraft:max_stack_size": 1 + } + } +} From c5b060c7b5b0908add60250064b81325e90ebe17 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:23 +0800 Subject: [PATCH 1337/1461] Add data/mcheli/recipes/m18_red.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/m18_red.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_red.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_red.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_red.json new file mode 100644 index 00000000..dafb3688 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_red.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"item": "minecraft:red_dye"}, + "Y": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_red", + "minecraft:custom_model_data": 2, + "minecraft:max_stack_size": 1 + } + } +} From 319aaf55e59a731811b9f530958c00580ca757a6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:24 +0800 Subject: [PATCH 1338/1461] Add data/mcheli/recipes/m18_violet.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/m18_violet.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_violet.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_violet.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_violet.json new file mode 100644 index 00000000..7944152c --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_violet.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"item": "minecraft:purple_dye"}, + "Y": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_violet", + "minecraft:custom_model_data": 3, + "minecraft:max_stack_size": 1 + } + } +} From 4a435aacf92aef1acbc24e0eebb835f906c7e068 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:24 +0800 Subject: [PATCH 1339/1461] Add data/mcheli/recipes/m18_yellow.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/m18_yellow.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_yellow.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_yellow.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_yellow.json new file mode 100644 index 00000000..ca70c1ad --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/m18_yellow.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"item": "minecraft:yellow_dye"}, + "Y": {"tag": "c:ingots/iron"} + }, + "pattern": [ + "DY" + ], + "result": { + "id": "mcheli:throwable", + "components": { + "mcheli:throwable": "m18_yellow", + "minecraft:custom_model_data": 4, + "minecraft:max_stack_size": 1 + } + } +} From 42ec5f2516c5dd93400ab5e51f326fa0ec981c0d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:25 +0800 Subject: [PATCH 1340/1461] Add data/mcheli/recipes/parachute.json to the 1.20.6 shared sources --- .../resources/data/mcheli/recipes/parachute.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/parachute.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/parachute.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/parachute.json new file mode 100644 index 00000000..66118c43 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/parachute.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "S": {"tag": "c:strings"}, + "W": {"tag": "minecraft:wool"} + }, + "pattern": [ + "WWW", + "S S", + " W " + ], + "result": { + "id": "mcheli:parachute" + } +} From a74d4882b108d99791905a38a0d174c7a6ae8508 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:25 +0800 Subject: [PATCH 1341/1461] Add data/mcheli/recipes/rangefinder.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/rangefinder.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/rangefinder.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/rangefinder.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/rangefinder.json new file mode 100644 index 00000000..eb87cd68 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/rangefinder.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": {"tag": "c:glass_blocks/colorless"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "III", + "RGR", + "III" + ], + "result": { + "id": "mcheli:rangefinder" + } +} From 3d1011115ae42770ad0637c96f31fc71605c532b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:26 +0800 Subject: [PATCH 1342/1461] Add data/mcheli/recipes/rangefinder_recharge.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/rangefinder_recharge.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/rangefinder_recharge.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/rangefinder_recharge.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/rangefinder_recharge.json new file mode 100644 index 00000000..23ac1b09 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/rangefinder_recharge.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "equipment", + "ingredients": [ + {"item": "mcheli:rangefinder"}, + {"tag": "c:dusts/redstone"} + ], + "result": { + "id": "mcheli:rangefinder" + } +} From 904b4c060346ef6f24730e113618706a38cae9ef Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:26 +0800 Subject: [PATCH 1343/1461] Add data/mcheli/recipes/uav_station.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/uav_station.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/uav_station.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/uav_station.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/uav_station.json new file mode 100644 index 00000000..47615ed5 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/uav_station.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"tag": "c:gems/diamond"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:storage_blocks/redstone"} + }, + "pattern": [ + "III", + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station" + } +} From 59550b8418d83d65925e55629129a6c1eabb9809 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:26 +0800 Subject: [PATCH 1344/1461] Add data/mcheli/recipes/uav_station2.json to the 1.20.6 shared sources --- .../data/mcheli/recipes/uav_station2.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/uav_station2.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/uav_station2.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/uav_station2.json new file mode 100644 index 00000000..2281cea7 --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/uav_station2.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "D": {"tag": "c:gems/diamond"}, + "I": {"tag": "c:ingots/iron"}, + "R": {"tag": "c:dusts/redstone"} + }, + "pattern": [ + "IDI", + "IRI" + ], + "result": { + "id": "mcheli:uav_station2" + } +} From 289e931901b46c366445eadc92d2f06157225f94 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:27 +0800 Subject: [PATCH 1345/1461] Add data/mcheli/recipes/wrench.json to the 1.20.6 shared sources --- .../resources/data/mcheli/recipes/wrench.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/mcheli/recipes/wrench.json diff --git a/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/wrench.json b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/wrench.json new file mode 100644 index 00000000..3b3ab88c --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/mcheli/recipes/wrench.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "I": {"tag": "c:ingots/iron"} + }, + "pattern": [ + " I ", + " II", + "I " + ], + "result": { + "id": "mcheli:wrench" + } +} From 5013e70dbbd618ddad022764b880c5bcb887fd4f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:27 +0800 Subject: [PATCH 1346/1461] Add data/minecraft/tags/damage_type/is_projectile.json to the 1.20.6 shared sources --- .../data/minecraft/tags/damage_type/is_projectile.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json diff --git a/minecraft-1.20.6/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json b/minecraft-1.20.6/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json new file mode 100644 index 00000000..1042db9e --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "mcheli:bullet" + ] +} From 56716f07725d65a0ae30de66adfb320164253237 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:28 +0800 Subject: [PATCH 1347/1461] Add mcheli_logo.png to the 1.20.6 shared sources --- .../src/main/resources/mcheli_logo.png | Bin 0 -> 23745 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-1.20.6/src/main/resources/mcheli_logo.png diff --git a/minecraft-1.20.6/src/main/resources/mcheli_logo.png b/minecraft-1.20.6/src/main/resources/mcheli_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..95c72198d76d40c8c513965b3c40c83712ee54cf GIT binary patch literal 23745 zcmYhi2|SeF7e9VyVeAH3vJQ%}WZ#z=Nfe=wO16}(vV_PoGbpr(RV-1m9k^W3+#H05LyW&;3lo-#Xe4geT* z2?H$5(8uLlh3?RM+^G{rwjoZ7>A@j(O+%YqnPXv(3ohVbbBWfxnA*g|DmnsC@J^w*!<}JV&wSul!dC2 zLt!s!f3yeeO~1PMF_^Jlz;>iuWCky}UX7-%PPN%Be-|7!waf1JPBX3xr#?ui9P13> z$qlu={F6Yvx3fAWU%aDK(?Y+HWfE`}YVg{{-?_QA-zRNN3C{!luNx30``K#p+_uQ> z>eQRnxLpl*lQh}^f|_$(xZUaW2}b|Ugt*Vii7|GD(eD?ATQeAdss?DCCf2+dn4{E>X`G>og9cU(oP_I^ZE-5r} zL$cd%@N9*d{n6Y1edqq}=#faPjf=N8_kVc0X><2V!``Y`1a;L;aie~C55EJA$;rD_ z&kdaGyb~*_^bJN`_<+I-f+g-_Pm+@Jz-2=K$@pazHV!pBj|MXu-z1?~3@E&s_Dxw+ z92;*-DiOx~iCAY8D2jwb{qhA0gY6RcXeH;m*Y%8rguRIWcl*o#Mkx&k+pBibQwTD2 zr-B54I&-0d<*7H!Z78CWv&IEz#3sjbIULO(;?6VZ78M??L$=jkO%I);xltZaQ~pnt zUn*JiI(<_XU$ZyjothE`?mI_|32hq;*zWfvr}Z&_@7Cn&*L=n+0eWAeoYi9C^)o3_ z&{zt$RQ0t4w8XW%tUD%-Fq~j3*Cz)%Deq+9U!cK_ojAHUn>amG+~Yn>Lcc!vsj(Wi z3`aWI51cXt-lc*06>9ogPn$e7Wl9##ZlwHvNVzV{82#kuvNgfPGQiIaHAJmXH=kOTnIgcebhPTT{t> z=YUFFVKHVIW*yI;KDQAw(>&1}pc;aBPk7rj*1(FU4H>|)QW~HRG!AIh9cv0!txf;) zIjeH+pT(%MBx8c&g+k%EmvE(DiP5-wMs|oVnVtNCi*G__Ylz+&YIY7t<-odau`1l_ z&#!M72OStVbHv`jRk(=#)x0eE>Xdh)r(FZv0fKLwvvfUbgVDcxe!=+1S+iQE`Ex(s zy}f>M>w{v5h@B=C6f(1oZBH3>oGB4Lu3@0{RPdSBt+SH?e;#H=LpI{#qB#XBTSvMu zMZ%-yxZo1N?n1^RO84&t3*W&8y@MI4%I5-FPSY+0x|AZK9B#YZi#%6vhejHL*QB&Z zzKe?^@g-`t_6-4Rzlu(^ZQrj?O8?3f#l`GogmJb0Wi*Z;J@{6=k`4_%*0Juy*%e>( z+UwzdGkfiyurI94uGg53Va9KU$^3;{wsvm+upRrgJ-q&UB`AL;-ybr+O#jmpgLM*d&o7 zQm`mNp%d&;-<1_0L)~wRr#xRW^$wTq3;6g`BqO!k`;r~&yAL@6*aAI_6bc?|bQNg+PJr@S`nVCBJZTpXKMrzl$@%#R00=F#gE<4gmOB5-}{2myYcpwac+^dih z%FCr#w{X3_H_d~oqod2iA;To=hd(a*3lglvM7PTqh@@y(iM$v`O%x{06_yYm(u%?z zep2Ct{nR|TvFVc^tg5G#XWMpuwEuOvUATna%2pX7S`3}$g0?)5=3#-~443%;lK?Mp zPGJ!mBMJ9bav)oZXyv6BO&*5yqTI;FTw~e56gg5#%Kiv7^thnA7TdS*=2=*eu(L;K zNz9CUnax7(zvTOwzb*g3jku#*jYyUPF{zx5B5*^{nJ8+5=dP`@pZr}w{V_WFKU=+) zJ9<>!ePU*l4W)f{mX zBPIJWAm{iu{{sXiJ?-V|Gq~K@0%#+?ea*TOmCNSzmC2l;3hm~bp6o!f%ynz`-qd}{T^UA=TGd&kP%M^Nu z%JrVMt)9aQ#zA-IWvMXR9w0L)7+i@!aA zq+#bnvoL;lDfV@_D|51xTH#;f(4nL^H+lKgu2lc!czAB;YV&290S2!~yEL$yxRpOz z@)qHw^`Y~-k;~~H@81UW*WY0OrRgbKK62F|+wq!6PHoSUDl?ipj{TtY$r|JY>bSmS zG1^&wx(1q}=)IUf(^`*8k|j9brbxg@(4SOCSG-b(MDfZUmo}n(JjFW{^)cYt@uGdZ zhzb28jw2D}P9f3#g%i9V${e$$K;i>ow-;*-kxvgw4}LHj;>ve4L<^e|oNOy_&wXMU}S&)`0s{Ce!Q!GLeN zDd*GG!i&{X-s08|%dVXMu8{SYc$@`wlFM7TylQsy-3ipC0=WfdE5S15A+V8 z?7Hg`S3l?>U!-T4Up)TGOXs;9F>y#!t?+=!V(!pYA#ZsyxYy3Za+s-KmvsKy#h6QM zN6gASZ_1s;VI)b@$2*XsxhZT%aLfSieZbln7R3x35UjE$lANx5%BngZ)Ho)1DEaC8 zd)me|l1o=Pq{Qz$xW#0?fpFJ!z`hCUw>5uy}R73dwF z+Q`0|)t_DW{QPi&s`<`YpP9%QwkqztZ1E|%C05Ry$FLW1^?Dn^r_=9MnQdqeXpnr4 z=S9Sv+cGph@Q0EG>Bs|D+qh8M_I;p$(oJ06Ma!|9dC>gMbaixU{N;Gy%%1B+A>yNp zEQkefx4`^Jj?%_Ir;`S!cdZ9I4GJGxo5EgAWFNEb&bq7nm*2+6Qu5k00qf99;(2+W z3N&wi(2oY_L<~R%VoIZl$Vd@n6ebS9HF!Ze-*20dT=?s;{8nYPN5iTGv95pnv+>V= zyvJUVDvTfaZ3 zwsA7D|Nec^%nL4b)Qj3*b>?xw4UD!d35V-oK7>5PQga*tzbITe68En0a>IE1xzij0 z=5DcW^L_`E)*7~NPjB@uylJ-Ypj-|2iSs+7#HoYt*S=IS?G(B(fLNaz9P3u9r~9zf zPnL%@eY&61(qn>%y?dC8Nfx(!{h=_kE*g1gVW|C6yJq6eD9!mF5k!>}@<*0V=};Yy zYP^v?oHuQA<<_U$X6WlIehFrFcL2u3)O+;$&%E``FA33+(-7Y64U-5z(Qwi-H@ECK zEb4|SOQCU;r=;~Y7*RzhbaU9pJ%O{eQe!uOP1Vfb%HN~|tLeJ{%-&|8zgF9hd zv{VYCaW9x~q2Gv!>a^I*#}0D+5O&U$FyUy+5|6<*IqqJUMY*v?%CUnC*o~UKv*r+l zw|h~x_{EfSEqFoWXGs6=4~`j&d{GySNZ;YO)15Gv55B8GYqOo@4xUpxyOa6V(L?UD z;w#ntaw`>CoZa-Yp>EAfIi8ATDz*#oD*K#`2b#kk11C+kj8ZLb-s*Ap{8*%@IrO?m zQ&3EMIpuKa$a;$QG(+*X{`FKruj2TJV(tC*44l#SR(PEMm$Qsx};E zcSNRzpGDv7Wj`wUElnse%wqN+oDFCrU+aUw2ZJ_1DPgy!bMrTLk~^ZVBy&oMP)xaY zJF{#4cgNo~(|}vk5!=8ucYLKR{_{Dv>@x zOF=x{YR**sX>iIqQ~al)ii;KdUuk1%rE)!q{n}KZx;URq`8TeMY9t4xaVEH>6-pb~ zuK^$61wIb~#cUbkn z-(4YISY7*y|B+cTxul0{f44`HUFNU9z0!C*+pzy#0Qdax`{%j>L0Mc>v7=fA#1E>g zM>Gz%+&-K&@>Oyq6jp*Rbx_ES#>F$?w9nbYHPK|xVM`9JUcIvqA06x0Ef6XHrkMAf z%iw4^N%mUsKXaYK^umY&hHZBchX1yRS$gP6Up(W6b40PknUZ}^M=RyexQDy6oqxEJ zCyw^onf+pXT>YeU9e0w*fih(w?c|vCMPE-pN}YPChlFtVN39n@3qFM&r|jhv=PVo_ zo-VX8QsJNBw|4bGeQ)!&T2%XObKYCvT3K7}`t5@;%dZdtuik&|*S>n^{fDv($8k}( ziYR3$bpkawGKz@$ropJ%pnte^wx`g~)=(H0A9>PfvNgs#V>p9%=8(&oi z87H}!imoreHzc@RBnbBh6SXGwlASb!wWQkb>`f~QOq$>PUH0na`hrq_Qq0EOyMaU} z125~XUJ~=m+XZ;Xs&f}-I;ZCQUxf+?qUntoM$3Kc%zCV&W1E(KMy$8*C|0q- z+6lMFKb4o_En)3*nt3WW<-4WI3w~?&)A=wME1ddAdSi{5rp7$)!Cr&IiSeFbgRui4 zWH_j7okJM@Np{vuUMZ?OKgIc=*+jaH*>tiEmlzp|F)GD)Mr95+6#VS2K4S3rV2C?f zIJ233-J0bbQsM6Si!Tb%B9X_U4L#vr4~V(`E>cBNWw-eAJ&<+>x?k&2J_tE`4?N)S zSCKZj!T;N=uHbp7!FSN{v$^J`avS@TSD;}2-LK;idS zfXVgn^f|l9qWrtxKCk4Re|Yool}xjyFQn*bE*4#(6KwF3U$`1cQuD=1^fGLmWY*CI z8^4NY1xnLN5@+o?oEar}q2cGN^elk2<`q z3*{hR{|;|Yfy#%v*RGyW5`TUyn;PCL(aC|9WL3 z`gL^K5Pa~}8P2|p?iE8TYVG=tr2)P&pIaqISr(G9#2utJ;jIO^pYG28Mcy-W8Gp_L z^|+l#>|(DpA|K_mhM|u^mJ6}YQzu|N)0%hnr=T6GrH3dHdeZ zSc&`*r5c>+<8Ut(L%~fd?PFJT=w9vC+h6druns2Lq3u*q`&vz^_r6K~_*AOd2fWj$2E{S_mH7N2;qD$jtk4h#|LYi1NtG zGR!(iZSeITE)PkT%@ljDo|}HFitCf8#C^H!tzb1Qg`T4VWku3Wp&mF+Z*yW@ppxDCQOXg`8oSIPV;g~ zKgT?eV>Z_u_z*ADcc0lu@%f?WqShik+c!;=%lKeV;gUyAB<4z4pR>r5CZ1yg=Ma4O zNT3EV(bh;$C)`OTeimBHFz;?Nz1&8T@)k2>}>f=v6~QOCV$*9Kaj({6IH&N9!Lhz;W^ z?=h6R+g@zh;$+EePUdj^6SIMDHU(aNRT}-(N3d^i;Mif;%+OrOC~Hj_;@N$;X}c8Et#(&BX+mb zcQ%4k!j|J?*PN-}jZ#*7cLS-TTeBYwD7{|?vDp#MdqO|bzWlM`>dAf5Q26GiCR$gE z9Euq!y&q~UiJZf=|8CE}Or1;Ngf_%q zFP$*xE(KCAe0-vfOBXPm&C@WpK}e-65Y#&2BNXZ_xtg#9fOYRIL%EA;JtMuuTmmN zL|?M7>v8xZ3lYNt-(9cWzSVXo=(Tq(;m|bY;$<`E@IMhocGrH~cgE+u@)g_k!RIo% zYLB+>F~<{{)%PwW@7=fe>Yv=5ooI2&>Pe_S?l4M!Zpm1EGexglz8HNlvIGFd$}tk2+>H@1j#L_mCAY^(L4m^j?#WHRXiy zgZFodk94#^;YIR66ljLHk8^wloviGj`{x^Xhf(7fOu$tiS|)ZWqhrmqZaY5NOHmcC z7g95=Qq>s3Yi`YB%GhRl4Ar^7|H?4;T0$f>_|>$;(RS>_Sucix`ud*{_3bZ$p52J9 zKJsp6j$KK__)hM4HKnfLVOyvOdVCF@F@uylB>>X>fy>tf5$D&t-S-1mZD$X?6ag8( z=ntk0Z0&3*&OOtV-{oqKDtLXx6uqQP%5M+W;dzq7sO|~-y6|rGz0sd31o9`3qOq4A zv&}P3Ic^Hmm0S@tR?Z_wSCO#arg!-POct~%j6Z^tTwg~}ls4v`lo-p6aRTha=Ig)e z3Z`l?=9CgZez%p-^b>n&E$MHNGK00{(e>i27j2Z>mL~1J#fq__TW@xy1~wbU8V6_* zTG%t!QZurzXN25)oS}R34#95M`Od4;0jdVZ0K}n?X<(c=QjLpv+ay9UW=41U$F3wl zg&E9NeKuF5Fx^|5wer9hjE;5C{;V@k+lmS3!Ksz}0Z6dVA&88u!>)AN-?Tl)-Y8^4?kub=Ox>i;%gSk?SOq zK{~4EoGn4P9YY@C4?b6+zz3Wp(bqxcHCUn;KzDf@0G4z0{lzm*Y(;@e;Abr*s4Yxq zgOQjtD#4$xR_{njI2jo@O-Zyr(GbH`uuxJPx%AmX!{GhGsnD(3=s?=vu9vvAYiyZ2 zspWeOm8tbJJC6O6qb+mZu*k`RMG3wu@l7w>OqnFd)m4OiSz0gj!N|whwI{*sPv5ep z<;x-x;R?CEwE)PErQ`;4-BDr(qM-y<;KT3>+xxWmp~#~IrtY&;)QVDWb_ zv2JHt&s>?hns3S#Att?sm2LOUXY|r)wO*#y?^d-G26k9ht<2FM%O72dH?Ca^Fvg6R zT~QWh0U(hJJaLCXb9Q8o|0tAkedML-ak*i3K>9Y=#ePRAS)zA?!3&;jGIXfvUdtwK zo;ze+?Zg#vvA)fN7XI(_^D8J)MQ4U=_GLWTvE_e>I=r$T|$uYD|N zBd7n`0N15jia}dY;iZ6?TXOu(6|uM*0G+V!b!2m9^ijy8KPqh-z8u9q`iAvlyey*j zmk(l6{C?S1QGZ-3+sG0t+bge_t(Hi*1pjIuUOro%AlFVyr;Pf{=2_K$xE$O6ep1%+ zDn*>1J5u=r@ide^1b`<1Wb)uBzojawR}BAHF!BTg3nX)7`hq?Rc7HEVoxSf7+}9Fo z_))#8M6EzDAzbhib?Q;QI<#yfmcqU(6G}L%3k%fuhSK}br0ivw7WRE#w7oTGZ2M{R z?9Oy0oz|YiZRi3pKhS(r_IL>9ITSH4xb2N5~5pVOL$Sw1a`t*YZ zuRkBE=ORrrNC@9rQ`0v+6@SiqxL)yD{pcHVX-bfNO|T>+@xf9!(Pwz$!PKSPbDgi8kaT6Biu^u%F=)RldQC*cD;$r}CVMgq z-NXoHUcCB=z<4=#EaxihUB>kwW650!~H?6NQZYL!s1}t0~K!PtaxX`<>wn( zwhCdiSFs=e&3$O4v*qRWCzrGXbYD#8ikur{4&;^NnNL0(1H1Tu+C*W3vst5H_rfJv ziI-t9msiwGkI0`%!AAG?-K8=i&oxnw5{a-V0FXjYo4O9cB7fXO93*c4%DyRZ_`w(- z5}6{4QYHR94M3q|3tnK-6b^JYx9rpcmv`ONRg0$yM79(2V@AnXiRt>AYE#EXPay0m zrF6N~q^*DXOP^&x)lu~9KOQ&$jj+UY_j^P!FckL|cbteI8k5d}@qdk?-{r)vs1URP zc?-7Oylh7=U!DrSA+V!evbs>WsnSRL+U}T3wNIH=x~)v;%-%h+@FTu#cT!1V5bk=| zqkcl)lqDIocEd3#3OE#~P=I;`aAe>sq9E8dAao2=kBPtYTk|+JjI%dfehc}8yXC8Y zEI?`Biu&y1VEiH^6X`Sx9BpU)yCuHF3g(01JFh=i%{5hIYhm4K_oxqZsVYm(a?SLL zn_0t$^Qj+Q84jA(%~2cAjoM3A2{{p3IZ7#DcOWUC9z zG|c3)J~c&nE%jC#%h}`Qmsg$@c&;@hILM9Jcl4z?@1a-Z>MePau@Q{g=eO5%ho~>{ zs>?5GtBX8*QeZGc$yOMt4gL>cK0?I@9{?^ApdeR~GXS}rR^JPDB_bZ+X>;0(;X5Xi zlV5Dy6?0?Y=v$f7I(g4+f5!G!b7cD%%LoexHE{?NHf~hi`T3>7f_AvJ?n+KcJwt|tc;PBc6w*~UjJ%*s>P2AAr7H$n zUeQdKwPp8ya;Dtj>cJ|~HO&E@*S}RC&X<4BX1_Pju0f6^T=F>UQ!v`^R9H5qEUbP- zpR;zGw`zXqSKvnZN6)3G!(~^0IhOq^wkzBC66=-P-PcuX7Unc+y1Q9_G8Ri8?tahr zhkjkodBD7ORdwPelOH&t#!>hQ5UE{f+CPAC+RIR&?&-)yJh}bhZmxoN%5yJ$c@0Cl zEI#||)u}A0_yoo$4S6B{csA21k<49gw2of2K@es1Vb#{GiC{uya&y8#D|?HoNA6?F zech|qg1f)#QL;I=igQfG@yWMYYH6!&Wxiwb>z#U(Zja0epVm3)qB^5~0&qs3gum-?@zhqw2=hX` z64O*qx2NB)Z5AYwDGFf}H42B>{CJ;uY`!o0{Q zDt?)Lb0uge$+^fXRsjLL|8KNlafb>2rxzVy-AmvHl5_QNu+Gp0g|+oa;?QmYAf3f| zZ~-s8wId23VW9{vfaHuq8Qpa`Zk-iuMEvHGs$H3R7!zUlwvIae+a(Oz#YMr2D6=2H z4Tytq8aG&8cZTE6>_vmpQ+sEh=+n93xHh(SyOPLTeONW8>mr{=3(fltZ<+|QBo}bwIar22&R+KB=R(DojMAA{+tjbg9 zS?{Zft+!=?J$`dAXu?LpA0zdy=H6#EZAgV#r@fMif~z8OZEmNqB&+jid%@PE3%jdZ zG3FZmZz6*_=CiXnDAKf>%q7BRmY+u`k-$E&mG-H$!3%HFVoBORW>d>|h!=q}*W z%s$bKBp$SilMgtH)=dkPUTRk$KQe7RJi+JxAjD)i1)@<^U&{g)MgX6hBG9hCb_BZK z5eDiIpN9fBhw>bt+&eSFoe2o543et&fjh;QIf@NCtl++s|D(oKdOWIQaK8`c_qZlEYOd@Uf$GQ_|n zyNgRA{{tauVKHNVFUy>_s2e&I<<@_~pTH`K-HKo4LjiZ1D>Wy{HT= zA9t7lKu$bC0DlKJKyKON1@qfuJV1e-1jZ)`wtfoDd+*KmctTjnEfRP#Z`wHq8e{uj z7#D(Suq?=Nr!3j*=C#CgzIqHMVA0-z~(%1T;z_KdG_`U>Ce7wxt_bij_zV zAj^V=J8+WNo(Nbrgb8(SJ7+Rdzclp`keH~IsugMa;YNN^`j&5gB(+E#*RM05jv(o? zmv$mZjyfQ18m0}h(^=lW2XSqD#3Qoc&JIPL5qx^Dhg}lQlW4UV(#l&}ccox4^+(@$ zR2dpnaw4? zpAY**m=y5I67pZL&vcJ-6{(vg6Rc74kQ5C`a1 zxc2}0`}>TDr8^4cRn1Bq_h9eKl;$85Z{NvK4pVfc4Oz?MaF9vg%C|dN%^uI%IVFWa zH~SvxHxsIRuN?mDF91WhgU`Ev*j~}Q3bI#En0z{UE$Z#j$O4e)Njz|tT9j?n+Y1i> zo}%iuCz+?>PDXREkW_j~BbhY;F(wv`)DeQmkbg#kqps}hi(e`ml zjjvRY5ICa588=E#^tOU??NAhv!t=Dm&3LmD*J|LnR(4>0aT-lN2zqHwAW@0M7l!+! z1u)5hVD>tK_=V}H`s7od^>-{+#DCVM1^xYKlVSWlOR{vzxxhZwCyI-AuIP>2UOXW@sFQv&*dp)Aaa|}_K=7v>J`5v5 zRAm6bh-(A1mMDl*QUbV|9~Xeo7-Ad-X6e%VaYGM=%ir?>iM3c1(PGaS=E9uD27aj( z0o=ILA@zl)4`*M@WajtPGrUEB`6wcKnL6ktPAUx#i`-9!~>kDRYrmYG_vMY9%N3`Z<6R?-s&pHOK{ynv` z)YdiEU5%Q3S8LU~#$B9`O6tm*vggh@i1;{{GU^p*r*u}w<_*T+kL67dm95qjBCC4Qd z!)Hqw$*0~N>&PyD_R*6*b+OF{ZGu=6Aw`Sj8{Jg%MCK@`? zyt&>#yt&?9v$_6{wz;0xy~e)!D05q+#7%FrfuYUSGO4dE(fM#~GGZAY;#`9*o}A^* z+GE|R6EPJTj9+MpXctdDsfNn!Td(s9bi9}X1H)QN&Tz01wPR$ERo96GXbC4k3<*FG z(@@aa1dNyl9?3#c1y}-WS&$CMnE5iNU?!N4r zMs4Vwd--RM8TeFd8vvq3l{pyCFjw`8S01lw`{3EYe=%_E#aEKeN6+6HI_up*I%SSR z!7~=Fc-N~?9fMdt!PPZvD8k&ydVgABX!wM-Xu{ffxVBPf!q%7FKM^a8%Ew!qF?j66 z=;E743(XsohJbhx4%Bv70Sk-80v{Xbk41v{QxMRXg>aImEacbKVbE0eXVfYHB4vR; z69_{JT?ez`u=)KRAPA68yksM?vVr1RCoZBDHWb^4`~A|%JRtTt!eylufaa^c>2g5k zT5s(v{hp+j@7=o=)8kHDa`8zTgdwK7PF{%xQS#s+nsi%km^2Rj?8W@-X65bXp-^Yo8 zzyv6q|JBD`h9%A;Fmj;qI+%a~9hi_6DC8sF29;%RSRk?v*l$|oq@E07r;2_lf{tUN z&fZf`fVs>Sa$FJsX85Em zqj}-yv5!DBcG6!wNXP{W1DR~Za(jWEB4_;3SYl-+e^Qe0V8U)9sLZN|6P>)8=_25W z#zGS(xSarZV+D=(xq;w-I#|{K?~&vmdOmD(2G9wDovTovwkQZb0xksI_rkFf$G5SG z40C`)hRuDxCZyr7cna#sOm zk^6H(i+z_JNOZ~qr4Af_5tx&!Fti@1q4owyHvcNed!YflKES1eMr}tpS!l28E+nmon}Zb5T%xlPpvo!8qDNBAP;z-`}ZgTIDcON7o!JS$NL5dkhdvbCE+ zU^GGU52Yl_G;N+MX;U`Lw%<5h`raBzZuw9AWvvRb-DT?lG92@Y{Vgb5ja?r=m*@~* ze3ZGDJe{h9)p``A@{tR?2;u^R(F;3_1*PLl9&KO0@oS=%TR(Mt*%~>9--4k{n-D-_ zS)ZlfUJ|I9VMAe}WPQM#0Ws(I~}zE;WU1>k2;Ssa^mS>P6S48L(_co25EzZ?;f zMHOe}2Mk@Jko+aEEf2Gqn`X`0b3g~cKB8uX<)>YeUzhQsA%Uv`u861WAJi@I9~?kT zeB$+U@fQ&54u^OydndMo@L4ckf_!wG;Q{-K06#uSm%jfYc72p*nR_I_!K}M(V>KYg z$bC}_=H~`L@~QfuyLhQ?A0fZJvQ-7(7QzoVqntti70z$5qQwsyw@W0`8t(WRRJO(cEak6P2ijfT^bH18rT7? z3w|gDMNFZ2;lf{Y!s4wy7lDwy4SQqw7DLZhwYeqvNQ1Q6$TOhBO^$_KM>GHpc5?Cv zJQA=de?fgTKgfp$!1jhe&R(S9F4PctV2e;ciWt z9GBiAYwlQRHDH8f*Y1F7SK^dEDUH3`$R$Qc7LA0dSJQ%P9xwicxZpJVMf%hv_SJtP z$d}AoUSK^!;oQ<8Ts=FBB&|Xi#5xoFjF2F2kBZG@G->sjqKuQyya}CuFV?SzHTda1 zk0h~yvI;@ax>7F!XcUACC_FmGqVDA>QRUiuAw|c_!uq3ZLY7X_tE#g!!>v^nbt_K> zznkZu7jCoXv&2Ns#~_#NGYO~Vi$%=k3lj3G@15@wHNEcsI4|g9OHYi>q~~P(q-Wo- zf7!LskReTizCo#rzCmDHNazL6mWh?JfE8ruzfY`X*XNL-SALxcU449ieLbCT_LZJ& zZ-D%MnMZq)v+R2r683ZRy|dHyPUUluqoRCUI4Wxy>y)QC>Xcxh1|b-q2-DE0iY z9FN&Sibp+Lw*dH#Bq0F-?t%gvJS0AdIoIF0Bl@(WDf=k>%k|FMjkC=iW z0^~5j#QxXEeFO;HfD=RBF(q2T!$F!3J$zn$@y@1KN)Q#<77Poat0sC5f}b!V7Jh;z z^_UaT&LrpR&Pjaz>%rcTDUHuyXKfhdoh}&>Mz?0KIk24DwZ3gYn}lAM2FE zja|1-eqD~gml8LY%BI1`{{kcT_OtbIy^p<@NW5oXWk)?bVIuo5^JeM9XZtr*KSHHmQBB!mpo#@WbqVn}eeTRevP;?w`j z;^l_~7ey`h0@}54`a;v(jjR(){?Xe;qLLZ^Veo?!oc^)f#-dW!h(Gjh|G|%7qzWO)Qv7KoLE?+P_ zy4@-jaAmpYFyCy?@8&*Bi(yuSV`U4sEqK{n&m~-c^;vA!mGIx53jqX9%N<2^S^6>q z+V8lF*#~168aF1Y{OOh4!`w^q+ElHM8sA~;JZvNKKYzD&PU4)mbPGGsP5OF3_(Jk& z1e?VMg%r+=qdmS2<=J6bBif+5&J&`Q8W9ln&3Hto}yNpBXI?sMsXl9O;RM%V%$t_^q z)E~>dv6L|BS9AHF+uJR{ue;;)<+>2UWmK^#ep6!b{z6mGQX!3vQoZxk~BU=UyNUNkSUp_jiznx$H9) z${&ePS_SdWWMv!*Sijh(KtutloC}bunLs3zJ^lBEHERZFf)zxCptxa2WPdIHnB__IYAe zXl}qU2wL7c|M3UlJ4CRka}!;9|7Y@L1rWKY!CtAG$aLA^<;lSipG(Q3u4KZ0nBPFLJhFK=MPP< z3CWl!#Z$P5{yhP2loN5|Pk(mpfq$U_Cir|B2%U7es0k*sXEbA(r<6;dsT*vz`1fvq7;J&Efca!>BMot2?fN+a zw4e1U*BeuO!`BD1wi0CM!NPmeh9Nr=4_V3HtiTMSgiesSz#o0qxT{jiPwx;tp-o2? zF`(cQDBFjetp3TtB#I%=AO$WTibYOga0&0e;^44aU5jJ)>hu2C^|gsh%&3LZd8T)l zRqCHngNm?AgBP|a@3*vIE@Qit1hd7yx! z06CKdGuyNKi<3!I64QG_M;7o+I0XMqgerXanR!_*-%3@lO<&nZPG0!Sre;IFZg|J} zgk4jE$}ZY(1a5GD6jN13a0)n~Z+{*9JcGOiv^x-fpfOaO>|;P2bS3s>W5l4da^k3u zb_`tXoiU7ROSEFU#ZqX6>X$TPVSyVXSP}dzy5Ayl=lpBDX^GDX*3=vIO|%=mN>_h4 zpP{c5)#K(V9H5qZhe9daZZY5S-KAjxFyEp0)10LNy7PpKNjpOO9V*UU2)%Tb#D|PB zj^yn-Fdt653!1yCEO3Tm*0IO{=2T`oBeLtbT+{mb$=H0_txwK_YbYov-^*;LHG41& z3K*Th52}+ovzMN+7pul7(E%(0Qp)GofaL-#TZ}m*onsviV+Do>N>jPXxvy?(qeD&< zaWjU>R*LaW)@ zQl$!>*`ch{#=K|=A;m8NF%Wd0L7r6QMt=(>7RJC}EI(nmB&HK;f|yLUWh&r@zx+d7 zjkE4x4#Zfy62(PhnzzNKr@!uJp~o#sRO*Hn+Ds#cX&+{irU&D9bLQwl(-jN!bb-AS z&lq8M7`~1ELCm;HzR271aA_y*L~o{j+e~I-m|@8C3Lk(N@(~pzpc5C6QETwiniwNY zEVC%H3_921S*ohD%@%tK9!J%77REvk!DOt3sno9ojh#&yE82>!+&4?*wkZry=(V|f zRV##eK!hB7R>Y$6YfzZm8OH|>9rON9Yw}{8M^8J5Wy7#?(uc)?mlJA z2ph;?x&W*rFA_zN+kG%YrEw(u6~CE~8OH30-?y8$obSBt`K>3EnXm;pRVsA^O65k! zZmwD#W86PPx0l$HeEdcOV8p=JbX45DzsqZW=$Ne(fz$s8l+f0)OghNFjikFcKQq!* zgecPNn-0ezbTMc0kmXNtb$zYc=56m1B^yTKFFXn=$AyZpumZL9gH6~cxqHyb&Eqw| z)c{f2S5!FsE*)r;Bry?<1;~<^=(EQ`q%b?n1#zAal95nJnUPC7kFpRla$M?jiFat4 z)pZ*ALS_MtcGg*;y-SvXDVZMJV0Z?nQ=!1+NTF!ZP4x#^z=tg+5$kIqA2~g z_P|$kMb<}NDnD1Q|2crK!t63ZJJ~9y)=ZDLHf09rrI)wcD;LCU-ZvDe0bfA2u zw6!ZH5_7oY-RY5I31`nFtbMo56v6&nXnPJ-6ZyD7J!5(I^)ez(zw?R~aEWFEC5>}n zoT1$vAv9iggyn@VikN&A#sOzhHk1@Ph-@bXM-zR_&`&v!Ko8PHvLRWS^on-Bxj_$R zHqX@7rL2aj)kn-z-0?vZU4V#MGwp-6{n7>c)P#f=zA>O2MNDdm?BPJ92{8HZuON3A z)15`Hq5d~sZPRDgr$tKog2y6E>*<>!^sV~6JOX{eIgs{KWoMke$%iE*ZZs7bq)HzO zPcnn_iT8j#^s2P)L+Y~Gvirk{S> zF46X%2gAX%X~FqPQ0hmO>uP`q+fUW!S8RR?R6q!Z6tSycMXB4J#qLqLv33`xG6^)J z;E}q!y5-P;`aJZohKZJ}i`Jq2k#1$++?kCqND?W^NcMAr{>cnu4JhajIE@v}wkKqa z72UnbV3A>@g%T=E36{?YMu29Q+caS0)HX^l>C^eTJj2$IDQjzhcSHfJ=NS|y5Y&0 zQm;SE-18_ZQ*J>LiAMIw!-t~EMBo|{7+kk5>h@Tax-tFkP6O?p<}aeMpnBtBgb>Va zVkK2sN&Pgyuc54%7>>A2~3E`${{`bX=@)j0%U}`ISsLm2=sG#(ItY+Mfa< z?tK0%y{Rh@MpTDI_j3qg8{#Ayc_vu>?`E|Eol9fu6~%_lK%xPFJ{2mO=Rd?pNFj&ALTOe)uT+ z?9X@4@!Kn=cu$MA>5c307>-CB0K9NZrZ3!lW@xgg&o81)Bx|0L>v=Au-E2qRw&uXH zlZ8^ek}NgtfqI=c!|7N6kOOkVw6p6B4RH|Q>s-VHl|}e`b~}vie_sf?ufcAreVFa2 z7@dy2_&pM_>-C40>eWqx^%vzP>;9dxE?F9xO?gw$%~vg+L)hDOrX?n>*6;1stNS!! zOrg0Wu@)Gk@xvm1a>HqIXlj07s)gSffeK9-|d|vO!#CTYe}#_z$iya zqD5JFk?c|Kt>n7(o>+bPpI10P<4y}ZYY-BM ze{1fGuIX*i&V$Bg+R7|_VNwONZ*@m1JpsFIzW$q1Z?pLKa*F~9UoGl}bqbt$c5lCE zL3LHP`#zuSw&a)9FR>EmPR(?l%*k09!C0SrZbhlzI)*({N*mjwOE~IRwzF735Q;5I zLM9PY+Qw{)*5>~|C0utrRsa9Lwv4Wk6`6%Dvb$N`8=+73u5gWzz4y5GCW-7-wnEA( z*Ua9tOE$R(ab0yUF86o({?6n4dB*#^&+DA$^Zj}~pD({UI2-g62iE6a9wPEu-AwhP z&G(q>e!ML*?9fUYlh%j_gZMLQqW6b=B-@9rWF8N#83@u3USk?$(57~v1=-R4Z`a4$ zloA9q03qgOsOXr{w0e1E^01~*iS)F3X~g)h96rmO@Vndl?8NEU_jY5sd)M#lP{Q+Q zW8|2bBDkkN%E4lu1u)y`B-{uT+CHY>hSF#{_uBN4Y( zeTkKPyErwzDOn}n5-PNA#oJrC-MFiw`Z1lcKRx?KCBD1)QCkx8ayZri^5l#BU`;}?=o zdC4Pi&tAm9tyiYEh)kP_T^mY=7)cs>$l_HHACf4&s`3OQk>gPZma5|8rMz2bHKNBM zZiU2tVa`HIvt%w7yeT~&}nn3P-CcgNK(gvtK$P1)QnieB6 z1)p~9`EI@|CvI6w1A<6$x;DSY319E{5Bz})PF+z;f;$2U4$svk9g7ua7u1fC-(do(T9U~R z4`Du$7Y}(M=BjY2t`i05v1RlYR-}kl{Hixi#0^R&3tAaU?x?F|v;v&WnGj$U1$5-a zYVd5vDge8rO84K46br_vuE&b-ZpFu%to$I!Fp7`U<{{)evR!^+G>5?oY)UxXEMTDZ zB8vdr4~JHgU#LOW!v(RkZZSxe@b!rGZgDsjyfRYgv`%9;1WWl9`CIkU`(qVI|8s;6 z9@Z7Wm_^lcN9HOXB_f^jSesVC6|xIVBzcEJIT8OlIX!P z$$@j-TP!#qQ123vHzYiVPt5YY3P^ZaDs9{)U!?!=`-^SpQgGW95Ow52ayHg-w|Ill z?-V5G_gA^&_7nXDMcVy)yt2waJ&>KplB_0cK02`dd= z?*53iSy2d)IZwtL#0VLx(2DfuAzdenz@o>bn-5ivgjXNxb+l$*PC>qrM^pxM_6D$RlAu*}RgJf0_h*8X zKs=|v#5!C)`tDB6kVpK_vKR|8PFFujy~R-GuYyWF==eRAwm(TLUQaA#k=CSFo|mAn#Yt8;i={EK>@#-=wQiRon_*H>p>;Kl_-3s~ZI z@T(tuBt$juD{0%uItd)<`yJg7n>0Y4|1tZudK=nH@Q#|O_S}5_Zr1ISoGAuP#?Ouk z*!vfn8rol0<`Ig9Fz_oh{@p9_l8+?Mj%0&j4V@CjcRj+|?L{7R?8ePk$w}GpDU9JKt_f{_u5H(xPT4Ziz3_-m=Vhr0p@T>G)(K6cM4p!y#qdn(W^6W(t;@}WZv zMBcwWfiFzTJJz%XIC1B@QDDtS4jL@!6fT>3ny5OqnCtwbf70rDO9c!nV{b;C+Mam{ zl-I}IS2WItI$Ycf^unFyqb?!iRF>~^e|{r$A{Jd0J53;|dEShxJmUVKF6i0(o7w#( zeh^9(M0QPr67s3ahq|WOV|fsWUZ-pe*T}fMX+6&Acs>?!b98-k&gwyW(T~0Nz_1>d zVFDfl#zGTN7qI~pAxGk(>3S;Vk8Acd&9Ivp>`4fBfAuK5t_lrrn2~1o#^J4k-G+YA-09X*TdE@*IL8vlv? za&^^{{M`fPD%a|K@Brk?cQ3``D!U5k`n36B#{du%MbU9wsUgjdsAsA9MmLx_8J{)2 ztEhc_yTMKA@nd?;d((QMX{3AR?1I~gj{;bnnm+r_7K z(zgfFF6?rr0@^g1(30V(?NIHE-iOZf%8|c%1{q@`ZF++lN2q-0F2Awg?#iYfnCTYx zLAlP0ginh*0ZYpa%_qo{Y(J+2*V^lLE7kOQ{vCN@1 zqsk=zZszN(4O%k4M<7}t>0p%J&3BcyAn6D9`#t)jb}FoB=jE~1UgquM$Z{%)_U z)Wr-sd*)N~bWcoRj;9oQO{LAo|8Vm- zDbw=yz5)9uz>@h4wn;h~A4qju`ntS3&w6R;?b`>Uva)kth_33YG!)lZzR!;Xz_LSoceQ=wcSo2t+X2Z^3r#dle8%!2^HH6uTpc8{5Pd^*^nKFaE1<_H6o@~ zFs@67UJaHXab@5|QFwaAvQ)kdeNq)-1~dC4MaewL%TY;Mcx?EQJpY<8flgI~=@BQu zN~=nK&FlU35o%Y&IL-uCiHgM@PcH(#Z<+E&C!POl&GmiCHplXemNjfe=Sr9hZ&c)- zb|79h$Rh3KIssV_YZ@;vtFOu6k%j-Q_hsgT$YpAV^)^oXqns&>X&n>u=@us3dDoVszgzbr=dE~?>9iz&jRw%OBX+~9xnB`3vg}7r ze|JZTfOURU#L9_-sS6z@TI!zak9+`3%d4~eCO?gT-rxQ{u%yOXBh=sXCr_@F`dz%+ z>l|xsq?f{*vz{(Pjs#rcYL#@M(0C^j#}53K7;dP1x9xY_0?tgIWsM2GYm7#qF81xD zzeL-GRySb!2=de*H(ZkVc?W7$5nLjcBdMRJ7TYu;iD{#vuMcgq$!i+%wE!O_wN@x5 z)~cwLG_LZWWq+enONllwbI9%HP`6fFdZehp)=@ueZ&Zl|fNV3%E$gE6jnO)uM_1(w z7fCyJl_o;8_7hIxe7tk)%n4@M9ZCkfd%KciRpN>MTNGJTs!-$3OEzEu7bSkP#RzIf ztKGT*IkUut#X3LzyxuX%&p_#XFLJDpacj;Z)T+8-==O<6J}D{YTx)o=fk$ZWOMsQ4 z*YVBv%kPUPU4JzlyI+^fxgWF5HiSqzFMX}G4+mIJ$R9&JMDPWjY$ws28LGsw@#P0a zNzk)qd#ewOM}|^XU1>UxCf3`il0Q6IVfqv~&h{-a4Xi+d@{4mL1Gk^c+4kIYE-ch+ z%DZWJP$40DtNzuUHjH90?jV35kQL>@)k7!Eo4`Wp9yz8{VssC)ab6R=KUDc|1+?}D zTBZA$J?r^LPlsYP_V`VLv^#q6aDo*Sxa{1k<1XUvDH5AgNV>rURmx7;b_PfEAIONG z7tP$8k`B}8_TL{!bDJK&jMj9f!cnI)zA@ETg%vCIfm5Bh!cX61!G8iIdx_HBTb2X* zuBKpP{N*0m{mY-Pr1>UG)LG^{rt*SM_#R3gd?wkNW(t&kz|punVDLA$jK%-dBycbN zx3s29CyhF56Lj}4v{N<<0a?r|_I;8(8ID8~>IA58ltj)drx5)Qd>7Ig!h$&Zft&6aseGw2)*or#;OV4(YudxP1tQm2>tsQP`aCLm;9j2 zCmYiJ&-F#*ivU79uBZKJpI+igFwLrbnRQVFABE+soY4SAcEl9QYc_6VZ#uf;@~;vy zscQH*#58%OJ@keGcyyQ!RCba>nc5ucO)zpUm>u_aL)4`o?rm`*D%W$0xfhd{Mh~e# z%Ky8A%a7?#l0O?O_|z{LB5R(F?VDv{hyA1B7p4b0mj_A&r=B|lFZrOt)IqNlBD<AZ!Fi@ep8VX+FM_69k6sC%CUcc!-rIU+_D_RG3T++&thk> z4ua$7Cv|jV5BJKn>hm72#-wuA0MpolL+te}s2U6e}?{kGb){YJn*7OOZ&YCcUGpCAo&p=9#r@x-KgsFT0Ig{!o3-kq*2NhCg?;@- zH%7RK`5wfY!M*Aif8p+O@7}Zysul$SQa7QXUVYSt-v*9n6;%;>%pG2F2bz7PM4;VynSwu88H_0t8&ovDA}^(6aKk6rH%PBSGE>L~ry` z#x+*2XcG50;-%bBV_wkz1a9k;@ z*j|6+ww)vg15{|^8;oVF9*%BOl*sLF)|Y{kaboT{zxa`9>q%&@d(%xG>Un&}Wcp+q zjODBzo{J@ul!mPv4|oz=(`#N6+8+u6A)VAxU0%qBeKM6eP+$hMM&!p*&m=-ox8Rwb zL2z|J-r4jd?hi6aRz77Lew>Mc=0CV#z?@&6z&PZHlHpTkp}pMWy2`Ywsu3{C53_TdFoW* zNkKO7SztoG{B^9)fEwzhAe)D*mJ(#6!CHjZe4TEE#7YPB>9mA-j5h7`##x*>_+4#B zNk!k;T^#mK$#jJyQM$_a>_F{d<`oU?VGcf8cRf9yW;GoA>C4T6kB+!HxPHTi=4Uco zBGeNb4}KV*oz%IWIfd0>ac_64XWRL^ZLbv)+Tm5D9u$6ZPiL87aaz0sR`GRwS$3`= z9!|ndj?2BDRIXzc6OGqT&e)uggHoN~h{0Y8^U4wG#%3N7Ha1WUdzy!fYq7k{ZAyme zHN)NNF;#N!@3b>=TKz2zl<#YH$$S=ezrL-Ns94_^*zxUyZ{r3p2zzLTyY*mxEk`bH z98NL8^xx&(kU%5EYp)}#JL3A?3g%->EvLrk|JLo~61=|p98vqTu2j0jQ*O?;(@(2K z?U~xR&RIhd1w1U7A(%?;`fT=_&&W+{+!iY|($ZGWvb@|x%Poy+<~}l)Cl$9Ob*9x% zNH+_duL&EL%Sy8Umh+g3`K~R!N81jCjUVtBQ8&vVLZ1I8EU%R^P9(0B8YB!^_AbsQ z2NEBO@qrA0s&F;#y8PYdu+F;!pT>UkpccH9gyCl=} zubU#lkDll2m&Ww>U#`{<-R-~?(|nvw`p?0?!CG5Xxy-@B?r(>bG&B=Z7zA`fb^SgC ze)p1ax6;SrduJ0C?~*Lj8wPFOKU60o-?2ek@&nDkCJyL)00N(G7Zee`u^?D|d)ep9 zLYoRya@o6JMj`N9o6|_+wM~Lo*fXw3lbo3)0{5De=;+I0f|6`)ZbO*yL{|SQ7?Evz z#n$9dTz;El5{GlsH=2rme%{uSJt5?o@n8ux8APsJn~^{L3!Zg?suDsLEGyiRkPSf& zy6u!CS%rV-O+64^K_Yse3un=hcg4dAuOl5R>&>?G`9Ny%ef*I}vZC4dOxO zGTquFsIHFm|9b6SWrrX42|5_lREoze8e>D&&opKY z3|_X_^aheC*h%i3VsS@Fu0qLc=Qx*y_|}H@gqA`MY+z!!>gXW_e<;>E>uh($;;X|W z7J26Oo3OZKzW1zPIun_4lE2^G!kH50Fz?FTOZ;J`Bjs|}xI{{)GzT_d3aFwI+Ma6p z41%z0aW7X~I`C9FC*NZDLEqJ>L8Fq|+<6bjRJ2!yo_iKgjO%bzcs#`G$bY8Mc09(X zRJK+FgUB_*K1pn9C53@vs!JZI3!&LHzBF2iona9())UF_K4lQ_6%|Ey-sy{06;Y^$ z1{ia2RiGXUe@+9k-s|AjR507#GEI|=rR* z1(pR5W2d|r4mh@#EDQ=^Ymsso#Gb`fpOG}z{IA~O<+qTH&u8@>An$(`V&ArQmvSTS z{aB+_6#ObpLE|L^$JwSCi>47K!y-cSAwl84T*4Hy(9CfVBIli;tIDx}_UT*UR>qHK zFB4+}3j^6aiamQ5?<1nnUr4Dv0Vnp7N;pKNGVBmt9c$l~?X%Mxtqy_C6c93gSr%}9 zy77(7FT~)SQft!NhHsy~ir+*tE$tMf%v%6cT#%A7JdWDOuy0nqJ{+>j21QaQ*wC&1 z-r`}*gGytVJX2fmbf~akgK5oBZUDmgq58u33<={MC!b^wpZ>W{HRJuE;5~X2c?T|E zQ8;@Mv<9a|sfmrQT)a0~I7y>dYbh4$zvN^eJP2=_2)Kk!H zbM8QV@l>IpH!U)dX=vc(`#X|)BohIXY;;s-5Yu9H+Msar#G~>FGY2k}1CD?fGeWY{ zDdH51UvgW2svM!;HqIU^_LTAdd&uoXo9=#{ z{u#97VMe$TX2OWIPKj-OS`$rB=~qQbjF+_*3qg>=`hI=UvRNArHFO(zCN1n&SVq#< z?d`r|)~X*H-PgfhpG~N-E-CSWmc>r$${4&MA^)@{c!SJ>d80q2oDgCX6If`bCBQq|vDCNs@slX#c4u~vWRKY#8`#%sYs1fEp*QU5+dk4Yn?I~jxX!iujA7A5 zy+4IRJ(AOAbL>zJKlV=iu;}j=h{5G56(ebLg4-DEHDhd#4S6pfWwyrkyEHJ91h)Og V`gRq~qyPf8|I~FK)~Q-Y{U4=vt$qLi literal 0 HcmV?d00001 From 1269e0270d6f58e8bbf4d3598d72c1f0f95ec217 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:28 +0800 Subject: [PATCH 1348/1461] Declare the 1.20.6 pack formats --- minecraft-1.20.6/src/main/resources/pack.mcmeta | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 minecraft-1.20.6/src/main/resources/pack.mcmeta diff --git a/minecraft-1.20.6/src/main/resources/pack.mcmeta b/minecraft-1.20.6/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..f782fbab --- /dev/null +++ b/minecraft-1.20.6/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "${mod_name} resources", + "pack_format": 32, + "supported_formats": [32, 41] + } +} From 48c813e871eb6d4eed0a459d81c1b55649abaaf1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:29 +0800 Subject: [PATCH 1349/1461] Add ForgeClient.java to the Forge 1.20.6 glue --- .../jnhprojects/mcheli/forge/ForgeClient.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java diff --git a/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java new file mode 100644 index 00000000..fad19b67 --- /dev/null +++ b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -0,0 +1,121 @@ +package com.jnhprojects.mcheli.forge; + +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.Minecraft; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.client.CameraAngles; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Camera; +import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.client.event.EntityRenderersEvent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.client.event.RegisterParticleProvidersEvent; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.client.event.RenderLivingEvent; +import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.IEventBus; + +/** Client events on Forge. Only loaded on a physical client. */ +final class ForgeClient { + private ForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(effect -> Minecraft.getInstance().gameRenderer.loadEffect(effect)); + modBus.addListener((FMLClientSetupEvent event) -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); + modBus.addListener((EntityRenderersEvent.RegisterRenderers event) -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + modBus.addListener((EntityRenderersEvent.RegisterRenderers event) -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener((RegisterParticleProvidersEvent event) -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + modBus.addListener((RegisterKeyMappingsEvent event) -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener((RegisterClientReloadListenersEvent event) -> + ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); + modBus.addListener((AddGuiOverlayLayersEvent event) -> event.getLayeredDraw() + .add(ForgeLayeredDraw.POST_SLEEP_STACK, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + IEventBus forgeBus = MinecraftForge.EVENT_BUS; + forgeBus.addListener((TickEvent.ClientTickEvent event) -> { + if (event.phase == TickEvent.Phase.END) { + ClientHooks.clientTickEnd(); + } + }); + forgeBus.addListener((ViewportEvent.ComputeCameraAngles event) -> ClientHooks.computeCameraAngles(new Angles(event))); + forgeBus.addListener((ViewportEvent.ComputeFov event) -> event.setFOV(ClientHooks.fov((float) event.getFOV()))); + forgeBus.addListener((RenderHandEvent event) -> event.setCanceled(ClientHooks.hidesHand())); + forgeBus.addListener(EventPriority.NORMAL, false, RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getEntity(), event.getPoseStack(), event.getPartialTick())); + forgeBus.addListener(ForgeClient::onInteractionKey); + forgeBus.addListener((ClientPlayerNetworkEvent.LoggingIn event) -> ClientHooks.loggedIn(event.getPlayer())); + forgeBus.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } + + private record Angles(ViewportEvent.ComputeCameraAngles event) implements CameraAngles { + @Override + public Camera camera() { + return event.getCamera(); + } + + @Override + public float partialTick() { + return (float) event.getPartialTick(); + } + + @Override + public float yaw() { + return event.getYaw(); + } + + @Override + public float pitch() { + return event.getPitch(); + } + + @Override + public float roll() { + return event.getRoll(); + } + + @Override + public void setYaw(float yaw) { + event.setYaw(yaw); + } + + @Override + public void setPitch(float pitch) { + event.setPitch(pitch); + } + + @Override + public void setRoll(float roll) { + event.setRoll(roll); + } + } +} From 34578517776db9581239cc52afbbbf9113a7054a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:29 +0800 Subject: [PATCH 1350/1461] Add ForgeNetwork.java to the Forge 1.20.6 glue --- .../mcheli/forge/ForgeNetwork.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java diff --git a/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java new file mode 100644 index 00000000..883dd91d --- /dev/null +++ b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.network.Channel; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.payload.PayloadFlow; + +/** + * Registers the shared packets on a Forge payload channel. Each packet travels under its own id, + * as on NeoForge and Fabric; the channel only adds Forge's version check. + */ +final class ForgeNetwork { + private static Channel channel; + + private ForgeNetwork() { + } + + static void register() { + PayloadFlow flow = ChannelBuilder + .named(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "main")) + .networkProtocolVersion(ModNetwork.PROTOCOL_VERSION) + .payloadChannel() + .play() + .serverbound(); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + flow = serverbound(flow, packet); + } + flow = flow.clientbound(); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + flow = clientbound(flow, packet); + } + channel = flow.build(); + } + + static Channel channel() { + return channel; + } + + private static PayloadFlow serverbound( + PayloadFlow flow, ModNetwork.Serverbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg, ctx.getSender())); + ctx.setPacketHandled(true); + }); + } + + private static PayloadFlow clientbound( + PayloadFlow flow, ModNetwork.Clientbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg)); + ctx.setPacketHandled(true); + }); + } +} From bd6a12dc12a4f4d830ddcc9e97a6666d2c87b9df Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:30 +0800 Subject: [PATCH 1351/1461] Add ForgePlatform.java to the Forge 1.20.6 glue --- .../mcheli/forge/ForgePlatform.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java diff --git a/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java new file mode 100644 index 00000000..d8b9d11c --- /dev/null +++ b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.network.PacketDistributor; + +import java.nio.file.Path; + +final class ForgePlatform implements Platform { + @Override + public String loaderName() { + return "forge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.SERVER.noArg()); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.PLAYER.with(player)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From fd68e2b22e3fc0541f2c34f9e1ac5746dff3eef8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:30 +0800 Subject: [PATCH 1352/1461] Add ForgeVehicleIngredient.java to the Forge 1.20.6 glue --- .../mcheli/forge/ForgeVehicleIngredient.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java diff --git a/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java new file mode 100644 index 00000000..057d0061 --- /dev/null +++ b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java @@ -0,0 +1,80 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.common.crafting.ingredients.AbstractIngredient; +import net.minecraftforge.common.crafting.ingredients.IIngredientSerializer; +import org.jspecify.annotations.Nullable; + +/** {@link VehicleIngredient} as a Forge ingredient, read from the {@code type} key. */ +final class ForgeVehicleIngredient extends AbstractIngredient { + static final MapCodec CODEC = VehicleIngredient.codec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final StreamCodec STREAM_CODEC = + VehicleIngredient.streamCodec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final IIngredientSerializer SERIALIZER = new IIngredientSerializer<>() { + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + public void write(RegistryFriendlyByteBuf buffer, ForgeVehicleIngredient value) { + STREAM_CODEC.encode(buffer, value); + } + + @Override + public ForgeVehicleIngredient read(RegistryFriendlyByteBuf buffer) { + return STREAM_CODEC.decode(buffer); + } + }; + + private final String vehicle; + + private ForgeVehicleIngredient(String vehicle) { + this.vehicle = vehicle; + } + + String vehicle() { + return vehicle; + } + + @Override + public boolean test(@Nullable ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + /** Read when shown, so a vehicle loaded after the recipes still appears. */ + @Override + public ItemStack[] getItems() { + return VehicleIngredient.displayStacks(vehicle).toArray(ItemStack[]::new); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IIngredientSerializer serializer() { + return SERIALIZER; + } + + @Override + public boolean equals(Object o) { + return o instanceof ForgeVehicleIngredient other && vehicle.equals(other.vehicle); + } + + @Override + public int hashCode() { + return vehicle.hashCode(); + } +} From f14c9b882148ee577fd19c4916a2d31cafffa8d2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:31 +0800 Subject: [PATCH 1353/1461] Replace the Forge 1.20.6 placeholder entry point with the real one --- .../jnhprojects/mcheli/forge/McheliForge.java | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java index 4225f85c..c878e1b4 100644 --- a/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java +++ b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java @@ -1,19 +1,76 @@ package com.jnhprojects.mcheli.forge; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegisterEvent; -/** - * Placeholder entry point for Minecraft 1.20.6. It only proves that the module builds and loads; - * the Forge glue from forge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +/** The Forge entry point: everything here only connects Forge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliForge(FMLJavaModLoadingContext context) { + IEventBus modBus = context.getModEventBus(); + Platform.install(new ForgePlatform()); + MCHeli.init(); - public McheliForge() { - LOGGER.info("MCHeli Rework placeholder loaded on Forge for Minecraft 1.20.6; no content yet"); + modBus.addListener((RegisterEvent event) -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(ForgeRegistries.Keys.INGREDIENT_SERIALIZERS, + helper -> helper.register(VehicleIngredient.ID, ForgeVehicleIngredient.SERIALIZER)); + }); + ForgeNetwork.register(); + modBus.addListener((FMLCommonSetupEvent event) -> event.enqueueWork(CommonHooks::commonSetup)); + modBus.addListener((AddPackFindersEvent event) -> event.addRepositorySource(CommonHooks.packSource())); + + IEventBus forgeBus = MinecraftForge.EVENT_BUS; + forgeBus.addListener((ServerAboutToStartEvent event) -> CommonHooks.serverAboutToStart(event.getServer())); + forgeBus.addListener((ServerStoppedEvent event) -> CommonHooks.serverStopped()); + forgeBus.addListener((TickEvent.ServerTickEvent event) -> { + if (event.phase == TickEvent.Phase.END) { + CommonHooks.serverTickEnd(event.getServer()); + } + }); + forgeBus.addListener((RegisterCommandsEvent event) -> CommonHooks.registerCommands(event.getDispatcher())); + forgeBus.addListener((PlayerEvent.PlayerLoggedInEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + forgeBus.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + ForgeClient.init(modBus); + } + } + + /** Only once Forge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } } } From 0df4dd7bf5c6c61b80f85336f6ae7ad19073440b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:31 +0800 Subject: [PATCH 1354/1461] Drop the placeholder wording from the Forge 1.20.6 metadata --- forge-1.20.6/src/main/resources/META-INF/mods.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/forge-1.20.6/src/main/resources/META-INF/mods.toml b/forge-1.20.6/src/main/resources/META-INF/mods.toml index 47ec16fb..1fdc3b48 100644 --- a/forge-1.20.6/src/main/resources/META-INF/mods.toml +++ b/forge-1.20.6/src/main/resources/META-INF/mods.toml @@ -13,7 +13,6 @@ authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft Forge. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. From 79af164fcfc5d005d073c0f5ddccee953ce53099 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:32 +0800 Subject: [PATCH 1355/1461] Add water.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/buckets/water.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/buckets/water.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/buckets/water.json b/forge-1.20.6/src/main/resources/data/c/tags/items/buckets/water.json new file mode 100644 index 00000000..165009c1 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/buckets/water.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:water_bucket" + ] +} From cac005e0a03a99ab8b5aa9d0fced7b3378e30b94 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:32 +0800 Subject: [PATCH 1356/1461] Add wooden.json to the Forge 1.20.6 glue --- .../main/resources/data/c/tags/items/chests/wooden.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/chests/wooden.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/chests/wooden.json b/forge-1.20.6/src/main/resources/data/c/tags/items/chests/wooden.json new file mode 100644 index 00000000..75795e06 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/chests/wooden.json @@ -0,0 +1,7 @@ +{ + "values": [ + "minecraft:chest", + "minecraft:trapped_chest", + {"id": "#forge:chests/wooden", "required": false} + ] +} From a57c0a27a37dba7cc24ffdc2c80644de8f41525e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:32 +0800 Subject: [PATCH 1357/1461] Add redstone.json to the Forge 1.20.6 glue --- .../main/resources/data/c/tags/items/dusts/redstone.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dusts/redstone.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dusts/redstone.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dusts/redstone.json new file mode 100644 index 00000000..ebac6ec1 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dusts/redstone.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:redstone", + {"id": "#forge:dusts/redstone", "required": false} + ] +} From 74e0e586cd2ded017a12e010d2ce46118563ac2f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:33 +0800 Subject: [PATCH 1358/1461] Add black.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/black.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/black.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/black.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/black.json new file mode 100644 index 00000000..371f5e88 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/black.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:black_dye", + {"id": "#forge:dyes/black", "required": false} + ] +} From 0b6980341151a18b2b9ad98352524d9d818dddaa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:33 +0800 Subject: [PATCH 1359/1461] Add blue.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/blue.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/blue.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/blue.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/blue.json new file mode 100644 index 00000000..708dffc5 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/blue.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:blue_dye", + {"id": "#forge:dyes/blue", "required": false} + ] +} From c7a21b8fb3aaf5427156fa26877e597276ee5ffd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:34 +0800 Subject: [PATCH 1360/1461] Add brown.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/brown.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/brown.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/brown.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/brown.json new file mode 100644 index 00000000..9231d54a --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/brown.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:brown_dye", + {"id": "#forge:dyes/brown", "required": false} + ] +} From eaaa63d4ff61f7a0ae91cdc10770fb43cd5ff0f7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:34 +0800 Subject: [PATCH 1361/1461] Add cyan.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/cyan.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/cyan.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/cyan.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/cyan.json new file mode 100644 index 00000000..a42059cb --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/cyan.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:cyan_dye", + {"id": "#forge:dyes/cyan", "required": false} + ] +} From 131843302c389b67ac2a9d132857ca389783e990 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:35 +0800 Subject: [PATCH 1362/1461] Add gray.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/gray.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/gray.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/gray.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/gray.json new file mode 100644 index 00000000..caa1a665 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/gray.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:gray_dye", + {"id": "#forge:dyes/gray", "required": false} + ] +} From eb5f6fa1cd6b4bd88764abd8a04499ca1386b9bd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:35 +0800 Subject: [PATCH 1363/1461] Add green.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/green.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/green.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/green.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/green.json new file mode 100644 index 00000000..31f63302 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/green.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:green_dye", + {"id": "#forge:dyes/green", "required": false} + ] +} From 8fa9590093977a6a4bc7d6101e924032555284d5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:36 +0800 Subject: [PATCH 1364/1461] Add light_blue.json to the Forge 1.20.6 glue --- .../main/resources/data/c/tags/items/dyes/light_blue.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/light_blue.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/light_blue.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/light_blue.json new file mode 100644 index 00000000..bd6cdedd --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/light_blue.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:light_blue_dye", + {"id": "#forge:dyes/light_blue", "required": false} + ] +} From ec4c29a923b6606c405750a758d724efdc2568bf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:36 +0800 Subject: [PATCH 1365/1461] Add light_gray.json to the Forge 1.20.6 glue --- .../main/resources/data/c/tags/items/dyes/light_gray.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/light_gray.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/light_gray.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/light_gray.json new file mode 100644 index 00000000..8b3b4af9 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/light_gray.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:light_gray_dye", + {"id": "#forge:dyes/light_gray", "required": false} + ] +} From 8892d920d2b4ac2d89df570a9248b1fccb357855 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:37 +0800 Subject: [PATCH 1366/1461] Add lime.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/lime.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/lime.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/lime.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/lime.json new file mode 100644 index 00000000..7be0a5ad --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/lime.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:lime_dye", + {"id": "#forge:dyes/lime", "required": false} + ] +} From 3fc1c519d0ba93a1ae833d40226553e4509ee03c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:37 +0800 Subject: [PATCH 1367/1461] Add magenta.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/magenta.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/magenta.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/magenta.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/magenta.json new file mode 100644 index 00000000..0cadd40a --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/magenta.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:magenta_dye", + {"id": "#forge:dyes/magenta", "required": false} + ] +} From 1c5a24e235d05c7fdf9a59ac5d0d1d99dcd5c266 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:37 +0800 Subject: [PATCH 1368/1461] Add orange.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/orange.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/orange.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/orange.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/orange.json new file mode 100644 index 00000000..6a9410ca --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/orange.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:orange_dye", + {"id": "#forge:dyes/orange", "required": false} + ] +} From 9c8fe9ad9d4cdf1a8a7c6bea0fe74677d3b0270f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:38 +0800 Subject: [PATCH 1369/1461] Add pink.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/pink.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/pink.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/pink.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/pink.json new file mode 100644 index 00000000..a39c8e24 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/pink.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:pink_dye", + {"id": "#forge:dyes/pink", "required": false} + ] +} From d857c141b8775b29250c29b349e2c2a23ac6a0e6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:38 +0800 Subject: [PATCH 1370/1461] Add purple.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/purple.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/purple.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/purple.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/purple.json new file mode 100644 index 00000000..28e42ec8 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/purple.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:purple_dye", + {"id": "#forge:dyes/purple", "required": false} + ] +} From b074847ea9aea8552a274a8d14b93aea9babbbbd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:39 +0800 Subject: [PATCH 1371/1461] Add red.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/red.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/red.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/red.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/red.json new file mode 100644 index 00000000..e273b27b --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/red.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:red_dye", + {"id": "#forge:dyes/red", "required": false} + ] +} From dca5114805415aaf80578330e2a1e181a0a2036d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:39 +0800 Subject: [PATCH 1372/1461] Add white.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/white.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/white.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/white.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/white.json new file mode 100644 index 00000000..78dc1bd9 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/white.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:white_dye", + {"id": "#forge:dyes/white", "required": false} + ] +} From 5bd515b7d4bebbb17db59e0d978e036b5d5dfd98 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:40 +0800 Subject: [PATCH 1373/1461] Add yellow.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/dyes/yellow.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/dyes/yellow.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/yellow.json b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/yellow.json new file mode 100644 index 00000000..3ce69377 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/dyes/yellow.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:yellow_dye", + {"id": "#forge:dyes/yellow", "required": false} + ] +} From a84efd4bd933e360ca910022987fbe5a5641035e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:40 +0800 Subject: [PATCH 1374/1461] Add feathers.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/feathers.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/feathers.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/feathers.json b/forge-1.20.6/src/main/resources/data/c/tags/items/feathers.json new file mode 100644 index 00000000..cd043bdb --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/feathers.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:feather", + {"id": "#forge:feathers", "required": false} + ] +} From 2f372e649ae7a6a5d298bd1e7824975d7449920b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:41 +0800 Subject: [PATCH 1375/1461] Add raw_fish.json to the Forge 1.20.6 glue --- .../main/resources/data/c/tags/items/foods/raw_fish.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/foods/raw_fish.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/foods/raw_fish.json b/forge-1.20.6/src/main/resources/data/c/tags/items/foods/raw_fish.json new file mode 100644 index 00000000..04d42afb --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/foods/raw_fish.json @@ -0,0 +1,8 @@ +{ + "values": [ + "minecraft:cod", + "minecraft:salmon", + "minecraft:tropical_fish", + "minecraft:pufferfish" + ] +} From 5909d68def1e79d241f08cf39a1f3e63ab699584 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:41 +0800 Subject: [PATCH 1376/1461] Add diamond.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/gems/diamond.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/gems/diamond.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/gems/diamond.json b/forge-1.20.6/src/main/resources/data/c/tags/items/gems/diamond.json new file mode 100644 index 00000000..3904df1c --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/gems/diamond.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:diamond", + {"id": "#forge:gems/diamond", "required": false} + ] +} From 281a6b34e3e3bd677283a95f8fb5cd3b6339f035 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:42 +0800 Subject: [PATCH 1377/1461] Add colorless.json to the Forge 1.20.6 glue --- .../resources/data/c/tags/items/glass_blocks/colorless.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/glass_blocks/colorless.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/glass_blocks/colorless.json b/forge-1.20.6/src/main/resources/data/c/tags/items/glass_blocks/colorless.json new file mode 100644 index 00000000..ed0d10ef --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/glass_blocks/colorless.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:glass", + {"id": "#forge:glass/colorless", "required": false} + ] +} From 3a91054058df8ba0c14663b09eb5f3126aac4ef9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:42 +0800 Subject: [PATCH 1378/1461] Add colorless.json to the Forge 1.20.6 glue --- .../resources/data/c/tags/items/glass_panes/colorless.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/glass_panes/colorless.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/glass_panes/colorless.json b/forge-1.20.6/src/main/resources/data/c/tags/items/glass_panes/colorless.json new file mode 100644 index 00000000..346ef2a2 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/glass_panes/colorless.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:glass_pane", + {"id": "#forge:glass_panes/colorless", "required": false} + ] +} From 2796664c6343838d9412c386ab03e1fcd8d48236 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:42 +0800 Subject: [PATCH 1379/1461] Add gunpowders.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/gunpowders.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/gunpowders.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/gunpowders.json b/forge-1.20.6/src/main/resources/data/c/tags/items/gunpowders.json new file mode 100644 index 00000000..955b2de9 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/gunpowders.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:gunpowder", + {"id": "#forge:gunpowder", "required": false} + ] +} From ea0b205130f3ebc0288e3128261af6dee4eea0b0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:43 +0800 Subject: [PATCH 1380/1461] Add gold.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/ingots/gold.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/ingots/gold.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/ingots/gold.json b/forge-1.20.6/src/main/resources/data/c/tags/items/ingots/gold.json new file mode 100644 index 00000000..44db933c --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/ingots/gold.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:gold_ingot", + {"id": "#forge:ingots/gold", "required": false} + ] +} From 9c60ea4c9c9fe187cc9e6259ed096158cef5f627 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:43 +0800 Subject: [PATCH 1381/1461] Add iron.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/ingots/iron.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/ingots/iron.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/ingots/iron.json b/forge-1.20.6/src/main/resources/data/c/tags/items/ingots/iron.json new file mode 100644 index 00000000..52bf3cf7 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/ingots/iron.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:iron_ingot", + {"id": "#forge:ingots/iron", "required": false} + ] +} From 12c0c34f4dcd421e7634b633a9e123edadec8766 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:44 +0800 Subject: [PATCH 1382/1461] Add wooden.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/rods/wooden.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/rods/wooden.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/rods/wooden.json b/forge-1.20.6/src/main/resources/data/c/tags/items/rods/wooden.json new file mode 100644 index 00000000..fa97361e --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/rods/wooden.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:stick", + {"id": "#forge:rods/wooden", "required": false} + ] +} From b61dbbd328a277fba312aeb2fcdd4ee9eeb6de2d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:44 +0800 Subject: [PATCH 1383/1461] Add coal.json to the Forge 1.20.6 glue --- .../resources/data/c/tags/items/storage_blocks/coal.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/coal.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/coal.json b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/coal.json new file mode 100644 index 00000000..5255a483 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/coal.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:coal_block", + {"id": "#forge:storage_blocks/coal", "required": false} + ] +} From 9add9d5515e6f54bce9e7025f23c202d3c233d49 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:45 +0800 Subject: [PATCH 1384/1461] Add diamond.json to the Forge 1.20.6 glue --- .../resources/data/c/tags/items/storage_blocks/diamond.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/diamond.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/diamond.json b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/diamond.json new file mode 100644 index 00000000..c7aac185 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/diamond.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:diamond_block", + {"id": "#forge:storage_blocks/diamond", "required": false} + ] +} From 2a3893e88b3e407fb30bf7cb717b922a7281eb1a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:45 +0800 Subject: [PATCH 1385/1461] Add gold.json to the Forge 1.20.6 glue --- .../resources/data/c/tags/items/storage_blocks/gold.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/gold.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/gold.json b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/gold.json new file mode 100644 index 00000000..42f874a1 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/gold.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:gold_block", + {"id": "#forge:storage_blocks/gold", "required": false} + ] +} From ab49e26cb90e5ba69d751ff225a82f521b9644d8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:46 +0800 Subject: [PATCH 1386/1461] Add iron.json to the Forge 1.20.6 glue --- .../resources/data/c/tags/items/storage_blocks/iron.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/iron.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/iron.json b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/iron.json new file mode 100644 index 00000000..7d0543b7 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/iron.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:iron_block", + {"id": "#forge:storage_blocks/iron", "required": false} + ] +} From 5e296cc89804e24f445a3c8a6c2b97bd1aa24581 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:46 +0800 Subject: [PATCH 1387/1461] Add redstone.json to the Forge 1.20.6 glue --- .../data/c/tags/items/storage_blocks/redstone.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/redstone.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/redstone.json b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/redstone.json new file mode 100644 index 00000000..a8f29d91 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/storage_blocks/redstone.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:redstone_block", + {"id": "#forge:storage_blocks/redstone", "required": false} + ] +} From 27368fa20431f3445c6d534ab3a15ad8edd74b0f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:47 +0800 Subject: [PATCH 1388/1461] Add strings.json to the Forge 1.20.6 glue --- .../src/main/resources/data/c/tags/items/strings.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 forge-1.20.6/src/main/resources/data/c/tags/items/strings.json diff --git a/forge-1.20.6/src/main/resources/data/c/tags/items/strings.json b/forge-1.20.6/src/main/resources/data/c/tags/items/strings.json new file mode 100644 index 00000000..7a042c57 --- /dev/null +++ b/forge-1.20.6/src/main/resources/data/c/tags/items/strings.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:string", + {"id": "#forge:string", "required": false} + ] +} From 20e2d29ed369e8c90b43537b0d9b64e0e4552028 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:47 +0800 Subject: [PATCH 1389/1461] Set up the Forge 1.20.6 runs --- forge-1.20.6/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/forge-1.20.6/build.gradle b/forge-1.20.6/build.gradle index 51b53eb9..17131ee9 100644 --- a/forge-1.20.6/build.gradle +++ b/forge-1.20.6/build.gradle @@ -7,12 +7,17 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + minecraft { mappings channel: 'official', version: minecraft_version runs { configureEach { workingDir = rootProject.layout.projectDirectory.dir("run-${minecraft_version}") + systemProperty 'mcheli.legacyZip', legacyZip } register('client') From 581309ffe65b30aaa197663311f869f1f1ef02bd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:48 +0800 Subject: [PATCH 1390/1461] Stop treating Forge 1.20.6 as a placeholder --- forge-1.20.6/gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/forge-1.20.6/gradle.properties b/forge-1.20.6/gradle.properties index 33de24a2..079c1d8a 100644 --- a/forge-1.20.6/gradle.properties +++ b/forge-1.20.6/gradle.properties @@ -1,8 +1,7 @@ -# Minecraft 1.20.6 on Forge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 1.20.6 on Forge loader=forge minecraft_version=1.20.6 minecraft_version_range=[1.20.6,1.20.7) forge_version=50.2.14 loader_version_range=[50,) java_version=21 -scaffold=true From 2225a8b750e9ce4cf618f4dc7452606d1e55dfba Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:48 +0800 Subject: [PATCH 1391/1461] Replace the NeoForge 1.20.6 placeholder entry point with the real one --- .../mcheli/neoforge/McheliNeoForge.java | 96 ++++++++++++++++--- 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java index 19b0ef8d..a1a02c70 100644 --- a/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java +++ b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java @@ -1,19 +1,93 @@ package com.jnhprojects.mcheli.neoforge; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Placeholder entry point for Minecraft 1.20.6. It only proves that the module builds and loads; - * the NeoForge glue from neoforge-26.3 replaces it when this version is ported for a final release. - */ -@Mod("mcheli") +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.AddPackFindersEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import net.neoforged.neoforge.registries.RegisterEvent; + +/** The NeoForge entry point: everything here only connects NeoForge to the shared code. */ +@Mod(MCHeli.MOD_ID) public final class McheliNeoForge { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); + public McheliNeoForge(IEventBus modBus) { + Platform.install(new NeoForgePlatform()); + MCHeli.init(); + + modBus.addListener(RegisterEvent.class, event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(NeoForgeRegistries.Keys.INGREDIENT_TYPES, + helper -> helper.register(VehicleIngredient.ID, NeoForgeVehicleIngredient.TYPE)); + }); + modBus.addListener(RegisterPayloadHandlersEvent.class, McheliNeoForge::registerPackets); + modBus.addListener(FMLCommonSetupEvent.class, event -> event.enqueueWork(CommonHooks::commonSetup)); + modBus.addListener(AddPackFindersEvent.class, event -> event.addRepositorySource(CommonHooks.packSource())); + + NeoForge.EVENT_BUS.addListener(ServerAboutToStartEvent.class, event -> CommonHooks.serverAboutToStart(event.getServer())); + NeoForge.EVENT_BUS.addListener(ServerStoppedEvent.class, event -> CommonHooks.serverStopped()); + NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> CommonHooks.serverTickEnd(event.getServer())); + NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> CommonHooks.registerCommands(event.getDispatcher())); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedInEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedOutEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + NeoForgeClient.init(modBus); + } + } + + /** Only once NeoForge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } + } + + /** Handlers run on the main thread, which is NeoForge's default. */ + private static void registerPackets(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(Integer.toString(ModNetwork.PROTOCOL_VERSION)); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(registrar, packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(registrar, packet); + } + } + + private static void serverbound(PayloadRegistrar registrar, ModNetwork.Serverbound packet) { + registrar.playToServer(packet.type(), packet.codec(), + (msg, context) -> packet.handler().accept(msg, (ServerPlayer) context.player())); + } - public McheliNeoForge() { - LOGGER.info("MCHeli Rework placeholder loaded on NeoForge for Minecraft 1.20.6; no content yet"); + private static void clientbound(PayloadRegistrar registrar, ModNetwork.Clientbound packet) { + registrar.playToClient(packet.type(), packet.codec(), (msg, context) -> packet.handler().accept(msg)); } } From 54be9a589fa05462956adcf7f73ab2ce879d9732 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:49 +0800 Subject: [PATCH 1392/1461] Add the NeoForge client glue for 1.20.6, using its camera angle event --- .../mcheli/neoforge/NeoForgeClient.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java diff --git a/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java new file mode 100644 index 00000000..5e0a2e14 --- /dev/null +++ b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -0,0 +1,121 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.client.CameraAngles; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.neoforge.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; +import net.neoforged.neoforge.client.event.InputEvent; +import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; +import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent; +import net.neoforged.neoforge.client.event.RenderHandEvent; +import net.neoforged.neoforge.client.event.RenderLivingEvent; +import net.neoforged.neoforge.client.event.ViewportEvent; +import net.neoforged.neoforge.client.gui.VanillaGuiLayers; +import net.neoforged.neoforge.common.NeoForge; + +/** + * Client events on NeoForge. Only loaded on a physical client. The camera hook is a mixin + * ({@code GameRendererMixin}): NeoForge's camera event fires before vanilla positions the camera, + * so a cockpit eye set from it would be overwritten. + */ +final class NeoForgeClient { + private NeoForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$loadEffect(effect)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + modBus.addListener(RegisterMenuScreensEvent.class, event -> ClientHooks.registerMenuScreens(event::register)); + modBus.addListener(RegisterParticleProvidersEvent.class, event -> { + event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); + event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + }); + modBus.addListener(RegisterKeyMappingsEvent.class, event -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener(RegisterClientReloadListenersEvent.class, + event -> ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); + modBus.addListener(RegisterGuiLayersEvent.class, + event -> event.registerAbove(VanillaGuiLayers.SLEEP_OVERLAY, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post.class, event -> ClientHooks.clientTickEnd()); + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeCameraAngles.class, event -> ClientHooks.computeCameraAngles(new Angles(event))); + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeFov.class, event -> event.setFOV(ClientHooks.fov((float) event.getFOV()))); + NeoForge.EVENT_BUS.addListener(RenderHandEvent.class, event -> event.setCanceled(ClientHooks.hidesHand())); + NeoForge.EVENT_BUS.addListener(RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getEntity(), event.getPoseStack(), event.getPartialTick())); + NeoForge.EVENT_BUS.addListener(InputEvent.InteractionKeyMappingTriggered.class, NeoForgeClient::onInteractionKey); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingIn.class, event -> ClientHooks.loggedIn(event.getPlayer())); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingOut.class, event -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } + + private record Angles(ViewportEvent.ComputeCameraAngles event) implements CameraAngles { + @Override + public Camera camera() { + return event.getCamera(); + } + + @Override + public float partialTick() { + return (float) event.getPartialTick(); + } + + @Override + public float yaw() { + return event.getYaw(); + } + + @Override + public float pitch() { + return event.getPitch(); + } + + @Override + public float roll() { + return event.getRoll(); + } + + @Override + public void setYaw(float yaw) { + event.setYaw(yaw); + } + + @Override + public void setPitch(float pitch) { + event.setPitch(pitch); + } + + @Override + public void setRoll(float roll) { + event.setRoll(roll); + } + } +} From abf23bc3c4e8956c8c5082ec4436d4819d5260f6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:49 +0800 Subject: [PATCH 1393/1461] Add NeoForgePlatform.java to the NeoForge 1.20.6 glue --- .../mcheli/neoforge/NeoForgePlatform.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java diff --git a/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java new file mode 100644 index 00000000..ab353589 --- /dev/null +++ b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.network.PacketDistributor; + +import java.nio.file.Path; + +final class NeoForgePlatform implements Platform { + @Override + public String loaderName() { + return "neoforge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + PacketDistributor.sendToServer(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From 009cfc5fb568c21fb9fa99d12ba39cfb7f17524d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:49 +0800 Subject: [PATCH 1394/1461] Add NeoForgeVehicleIngredient.java to the NeoForge 1.20.6 glue --- .../neoforge/NeoForgeVehicleIngredient.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java diff --git a/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java new file mode 100644 index 00000000..0e7ddc96 --- /dev/null +++ b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.common.crafting.ICustomIngredient; +import net.neoforged.neoforge.common.crafting.IngredientType; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a NeoForge ingredient, read from the {@code neoforge:ingredient_type} key. */ +record NeoForgeVehicleIngredient(String vehicle) implements ICustomIngredient { + static final IngredientType TYPE = new IngredientType<>( + VehicleIngredient.codec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle), + VehicleIngredient.streamCodec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle)); + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream getItems() { + return VehicleIngredient.displayStacks(vehicle); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IngredientType getType() { + return TYPE; + } +} From 876d65f0b9adad3bb8b5017fb987ea5d2195d1af Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:50 +0800 Subject: [PATCH 1395/1461] Invoke the private post effect loader on NeoForge 1.20.6 --- .../mcheli/neoforge/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java diff --git a/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..ee6b727f --- /dev/null +++ b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#loadEffect} private on 1.20.6. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("loadEffect") + void mcheli$loadEffect(ResourceLocation effect); +} From 70c01f56e97fcae6a8fb41bc13675d2ffe8d9815 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:50 +0800 Subject: [PATCH 1396/1461] Drop the placeholder wording from the NeoForge 1.20.6 metadata --- .../src/main/resources/META-INF/neoforge.mods.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/neoforge-1.20.6/src/main/resources/META-INF/neoforge.mods.toml b/neoforge-1.20.6/src/main/resources/META-INF/neoforge.mods.toml index 2585f0b9..ecb64dfe 100644 --- a/neoforge-1.20.6/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge-1.20.6/src/main/resources/META-INF/neoforge.mods.toml @@ -13,12 +13,14 @@ authors="${mod_authors}" credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." description=''' Helicopters, planes, tanks and vehicles for modern Minecraft. -This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the "mcheli" folder of the game directory. ''' +[[mixins]] +config="mcheli.mixins.json" + [[dependencies.${mod_id}]] modId="neoforge" type="required" From ea1f60578602b0950295cb99a6e8b96107ce45a0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:51 +0800 Subject: [PATCH 1397/1461] List the NeoForge 1.20.6 mixins, which no longer hook the camera --- .../src/main/resources/mcheli.mixins.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 neoforge-1.20.6/src/main/resources/mcheli.mixins.json diff --git a/neoforge-1.20.6/src/main/resources/mcheli.mixins.json b/neoforge-1.20.6/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..ef8e0cae --- /dev/null +++ b/neoforge-1.20.6/src/main/resources/mcheli.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.neoforge.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "GameRendererInvoker" + ], + "injectors": { + "defaultRequire": 1 + } +} From a147cd9b335a6f1fa49067161757bca6a3b73e6e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:51 +0800 Subject: [PATCH 1398/1461] Set up the NeoForge 1.20.6 runs --- neoforge-1.20.6/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neoforge-1.20.6/build.gradle b/neoforge-1.20.6/build.gradle index 6dded114..60829c34 100644 --- a/neoforge-1.20.6/build.gradle +++ b/neoforge-1.20.6/build.gradle @@ -7,12 +7,17 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + neoForge { version = neoforge_version runs { configureEach { gameDirectory = rootProject.file("run-${minecraft_version}") + systemProperty 'mcheli.legacyZip', legacyZip } client { client() From f7543acfb94c7d061bdb46da190928de7bbcc23c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:52 +0800 Subject: [PATCH 1399/1461] Stop treating NeoForge 1.20.6 as a placeholder --- neoforge-1.20.6/gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/neoforge-1.20.6/gradle.properties b/neoforge-1.20.6/gradle.properties index 14fb2f7c..3e067ad4 100644 --- a/neoforge-1.20.6/gradle.properties +++ b/neoforge-1.20.6/gradle.properties @@ -1,8 +1,7 @@ -# Minecraft 1.20.6 on NeoForge (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 1.20.6 on NeoForge loader=neoforge minecraft_version=1.20.6 minecraft_version_range=[1.20.6,1.20.7) neoforge_version=20.6.144 loader_version_range=[20.6,) java_version=21 -scaffold=true From c17486e5f4a307c7a998269da99674ffd1fecfac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:52 +0800 Subject: [PATCH 1400/1461] Add FabricPlatform.java to the Fabric 1.20.6 glue --- .../mcheli/fabric/FabricPlatform.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java new file mode 100644 index 00000000..a7114f13 --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; + +import java.nio.file.Path; + +final class FabricPlatform implements Platform { + @Override + public String loaderName() { + return "fabric"; + } + + @Override + public Path gameDir() { + return FabricLoader.getInstance().getGameDir(); + } + + @Override + public Path configDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + public String modVersion() { + return FabricLoader.getInstance().getModContainer(MCHeli.MOD_ID) + .map(c -> c.getMetadata().getVersion().getFriendlyString()).orElse(""); + } + + @Override + public boolean isClient() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPlayNetworking.send(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ServerPlayNetworking.send(player, payload); + } + + /** Fabric orders tabs by registration, which already puts ours one after another. */ + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return FabricItemGroup.builder(); + } +} From ddb74c17e2b5720291070ad4045880ed075266ac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:53 +0800 Subject: [PATCH 1401/1461] Add FabricVehicleIngredient.java to the Fabric 1.20.6 glue --- .../fabric/FabricVehicleIngredient.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java new file mode 100644 index 00000000..5ad9f821 --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** {@link VehicleIngredient} as a Fabric ingredient, read from the {@code fabric:type} key. */ +record FabricVehicleIngredient(String vehicle) implements CustomIngredient { + static final CustomIngredientSerializer SERIALIZER = new CustomIngredientSerializer<>() { + private final MapCodec codec = + VehicleIngredient.codec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + private final StreamCodec streamCodec = + VehicleIngredient.streamCodec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + + @Override + public ResourceLocation getIdentifier() { + return VehicleIngredient.ID; + } + + @Override + public MapCodec getCodec(boolean allowEmpty) { + return codec; + } + + @Override + public StreamCodec getPacketCodec() { + return streamCodec; + } + }; + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public List getMatchingStacks() { + return VehicleIngredient.displayStacks(vehicle).toList(); + } + + @Override + public boolean requiresTesting() { + return true; + } + + @Override + public CustomIngredientSerializer getSerializer() { + return SERIALIZER; + } +} From c7aab0aad19bbd938b996e69e8f40515f03ad50c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:53 +0800 Subject: [PATCH 1402/1461] Replace the Fabric 1.20.6 placeholder entry point with the real one --- .../mcheli/fabric/McheliFabric.java | 69 +++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java index f30386b9..16bc7b1d 100644 --- a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java @@ -1,19 +1,74 @@ package com.jnhprojects.mcheli.fabric; +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; import net.fabricmc.api.ModInitializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; /** - * Placeholder entry point for Minecraft 1.20.6. It only proves that the module builds and loads; - * the Fabric glue from fabric-26.3 replaces it when this version is ported for a final release. + * The Fabric entry point: everything here only connects Fabric to the shared code. The imported + * content pack is added by {@code PackRepositoryMixin}, since Fabric has no pack finder event. */ public final class McheliFabric implements ModInitializer { - private static final Logger LOGGER = LoggerFactory.getLogger("MCHeli"); - @Override public void onInitialize() { - LOGGER.info("MCHeli Rework placeholder loaded on Fabric for Minecraft 1.20.6; no content yet"); + Platform.install(new FabricPlatform()); + MCHeli.init(); + + for (ResourceKey> registry : Registration.registries()) { + register(Registration.typed(registry)); + } + CustomIngredientSerializer.register(FabricVehicleIngredient.SERIALIZER); + registerPackets(); + CommonHooks.commonSetup(); + + ServerLifecycleEvents.SERVER_STARTING.register(CommonHooks::serverAboutToStart); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> CommonHooks.serverStopped()); + ServerTickEvents.END_SERVER_TICK.register(CommonHooks::serverTickEnd); + CommandRegistrationCallback.EVENT.register((dispatcher, context, selection) -> CommonHooks.registerCommands(dispatcher)); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> CommonHooks.playerLoggedIn(handler.getPlayer())); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> CommonHooks.playerLoggedOut(handler.getPlayer())); + } + + @SuppressWarnings("unchecked") + private static void register(ResourceKey> key) { + Registry registry = (Registry) BuiltInRegistries.REGISTRY.get(key.location()); + if (registry == null) { + throw new IllegalStateException("No registry " + key.location()); + } + Registration.registerAll(key, (id, value) -> Registry.register(registry, id, value)); + } + + /** Client handlers are registered by {@link McheliFabricClient}; handlers run on the main thread. */ + private static void registerPackets() { + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + } + + private static void clientbound(ModNetwork.Clientbound packet) { + PayloadTypeRegistry.playS2C().register(packet.type(), packet.codec()); + } + + private static void serverbound(ModNetwork.Serverbound packet) { + PayloadTypeRegistry.playC2S().register(packet.type(), packet.codec()); + ServerPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg, context.player())); } } From 197d8dbdb2959787d1a6054b857b7cb4c289eefd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:54 +0800 Subject: [PATCH 1403/1461] Add McheliFabricClient.java to the Fabric 1.20.6 glue --- .../mcheli/fabric/McheliFabricClient.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java new file mode 100644 index 00000000..b012a4bb --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java @@ -0,0 +1,77 @@ +package com.jnhprojects.mcheli.fabric; + +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.Minecraft; +import com.jnhprojects.mcheli.fabric.mixin.GameRendererInvoker; +import com.jnhprojects.mcheli.client.particle.SmokeParticle; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * Client side of the Fabric entry point. The camera, hand, key and rider hooks are mixins (see + * {@code com.jnhprojects.mcheli.fabric.mixin}), since Fabric API has no events for them. + */ +public final class McheliFabricClient implements ClientModInitializer { + + @Override + public void onInitializeClient() { + ClientHooks.init(effect -> ((GameRendererInvoker) Minecraft.getInstance().gameRenderer).mcheli$loadEffect(effect)); + ClientHooks.registerEntityRenderers(EntityRendererRegistry::register); + ClientHooks.registerBlockEntityRenderers(BlockEntityRenderers::register); + ClientHooks.registerMenuScreens(MenuScreens::register); + ParticleFactoryRegistry.getInstance().register(ModParticles.GLOW.get(), GlowParticle.Provider::new); + ParticleFactoryRegistry.getInstance().register(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); + ClientHooks.keyMappings().forEach(KeyBindingHelper::registerKeyBinding); + ResourceManagerHelper resources = ResourceManagerHelper.get(PackType.CLIENT_RESOURCES); + ClientHooks.reloadListeners().forEach((id, listener) -> resources.registerReloadListener(new Identified(id, listener))); + // Drawn after the vanilla HUD, which is where Forge's post sleep layer ends up too. + HudRenderCallback.EVENT.register(ClientHooks::drawHud); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + + ClientTickEvents.END_CLIENT_TICK.register(client -> ClientHooks.clientTickEnd()); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> ClientHooks.loggedIn(client.player)); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ClientHooks.loggedOut()); + } + + private static void clientbound(ModNetwork.Clientbound packet) { + ClientPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg)); + } + + /** Fabric wants an id on every reload listener. */ + private record Identified(ResourceLocation id, PreparableReloadListener listener) implements IdentifiableResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return id; + } + + @Override + public CompletableFuture reload(PreparationBarrier barrier, ResourceManager manager, ProfilerFiller preparations, + ProfilerFiller reload, Executor background, Executor main) { + return listener.reload(barrier, manager, preparations, reload, background, main); + } + } +} From 9dd22a64afeee8e84c7d5ae8d0a37409ab8f5e3b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:54 +0800 Subject: [PATCH 1404/1461] Add RollableCamera.java to the Fabric 1.20.6 glue --- .../java/com/jnhprojects/mcheli/fabric/RollableCamera.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java new file mode 100644 index 00000000..ff30f0ff --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/RollableCamera.java @@ -0,0 +1,7 @@ +package com.jnhprojects.mcheli.fabric; + +/** Implemented on {@code Camera} by {@code CameraMixin}. Outside the mixin package, which Mixin refuses to load classes from. */ +public interface RollableCamera { + /** Like vanilla's {@code setRotation}, with a roll in degrees as Forge applies it. */ + void mcheli$setRotation(float yRot, float xRot, float roll); +} From e94869891a1c6ac2bd3c76adf72525053185742e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:54 +0800 Subject: [PATCH 1405/1461] Reach Camera.setRotation from the Fabric camera hook on 1.20.6 --- .../mcheli/fabric/mixin/CameraMixin.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java new file mode 100644 index 00000000..67e65ec3 --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -0,0 +1,21 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +/** + * Lets the camera hook reach {@code Camera#setRotation}. The view matrix on 1.20.6 is built from the + * camera's angles alone, so the roll is not for this class: {@code GameRendererMixin} applies it. + */ +@Mixin(Camera.class) +abstract class CameraMixin implements RollableCamera { + @Shadow + protected abstract void setRotation(float yRot, float xRot); + + @Override + public void mcheli$setRotation(float yRot, float xRot, float roll) { + setRotation(yRot, xRot); + } +} From f0613a98695d547f13f0122ba54593ef54683aaa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:55 +0800 Subject: [PATCH 1406/1461] Invoke the private post effect loader on Fabric 1.20.6 --- .../mcheli/fabric/mixin/GameRendererInvoker.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java new file mode 100644 index 00000000..8e1f0568 --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererInvoker.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** Vanilla keeps {@code GameRenderer#loadEffect} private on 1.20.6. */ +@Mixin(GameRenderer.class) +public interface GameRendererInvoker { + @Invoker("loadEffect") + void mcheli$loadEffect(ResourceLocation effect); +} From e2bb18fab58123d94e6d352b2fc368363572d5b0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:55 +0800 Subject: [PATCH 1407/1461] Hook the camera before the view matrix on Fabric 1.20.6 and stack the roll on it --- .../fabric/mixin/GameRendererMixin.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java new file mode 100644 index 00000000..c566a389 --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.fabric.RollableCamera; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.util.Mth; +import org.joml.Matrix4f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * The camera hook, where Forge fires its camera angle event on 1.20.6 (the camera is placed for + * the frame, and its rotation is about to become the view matrix), and the first person hand, + * which is hidden in some seats. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Unique + private float mcheli$viewRoll; + + @Inject(method = "renderLevel", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/GameRenderer;resetProjectionMatrix(Lorg/joml/Matrix4f;)V", shift = At.Shift.AFTER)) + private void mcheli$beforeViewRotation(CallbackInfo ci) { + mcheli$viewRoll = 0.0F; + ClientHooks.computeCameraAngles(mainCamera, mainCamera.getPartialTickTime(), (camera, yaw, pitch, roll) -> { + setRotation(camera, yaw, pitch, roll); + mcheli$viewRoll = roll; + }); + } + + /** Vanilla's view matrix has no roll; it stacks on top of the other two, as NeoForge does it. */ + @Redirect(method = "renderLevel", at = @At(value = "INVOKE", target = "Lorg/joml/Matrix4f;rotationXYZ(FFF)Lorg/joml/Matrix4f;")) + private Matrix4f mcheli$withRoll(Matrix4f matrix, float x, float y, float z) { + return new Matrix4f().rotationZ(mcheli$viewRoll * Mth.DEG_TO_RAD).mul(matrix.rotationXYZ(x, y, z)); + } + + @Inject(method = "renderItemInHand", at = @At("HEAD"), cancellable = true) + private void mcheli$renderItemInHand(CallbackInfo ci) { + if (ClientHooks.hidesHand()) { + ci.cancel(); + } + } + + /** Zoom through a sight, where Forge fires its field of view event. */ + @Inject(method = "getFov", at = @At("RETURN"), cancellable = true) + private void mcheli$getFov(Camera camera, float partialTicks, boolean useFovSetting, CallbackInfoReturnable cir) { + cir.setReturnValue((double) ClientHooks.fov((float) cir.getReturnValueD())); + } + + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((RollableCamera) camera).mcheli$setRotation(yaw, pitch, roll); + } +} From 2e52b42b4b209fac5a2786288d55f347ae4c715e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:56 +0800 Subject: [PATCH 1408/1461] Add LivingEntityRendererMixin.java to the Fabric 1.20.6 glue --- .../mixin/LivingEntityRendererMixin.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java new file mode 100644 index 00000000..2cd967b3 --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java @@ -0,0 +1,23 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.world.entity.LivingEntity; +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.callback.CallbackInfo; + +/** Tilts riders with their vehicle, where Forge fires its living entity render event. */ +@Mixin(LivingEntityRenderer.class) +abstract class LivingEntityRendererMixin { + + @Inject(method = "render(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;" + + "Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At("HEAD")) + private void mcheli$beforeRender(LivingEntity entity, float yaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, + int light, CallbackInfo ci) { + ClientHooks.beforeRenderLiving(entity, pose, partialTicks); + } +} From 7750b3b4f4441ccfe9fc2aee613daf250d28a4fc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:56 +0800 Subject: [PATCH 1409/1461] Add MinecraftMixin.java to the Fabric 1.20.6 glue --- .../mcheli/fabric/mixin/MinecraftMixin.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java new file mode 100644 index 00000000..b4685f81 --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.HitResult; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** The use and pick block keys, which Forge and NeoForge let mods take through an input event. */ +@Mixin(Minecraft.class) +abstract class MinecraftMixin { + @Shadow + public @Nullable LocalPlayer player; + @Shadow + public @Nullable HitResult hitResult; + @Shadow + private int rightClickDelay; + + @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) + private void mcheli$startUseItem(CallbackInfo ci) { + if (player == null) { + return; + } + ClientHooks.UseKey result = ClientHooks.onUseKey(InteractionHand.MAIN_HAND); + if (result != ClientHooks.UseKey.PASS) { + // Vanilla sets the same delay before it looks at the hands. + rightClickDelay = 4; + if (result == ClientHooks.UseKey.CONSUME_AND_SWING) { + player.swing(InteractionHand.MAIN_HAND); + } + ci.cancel(); + } + } + + @Inject(method = "pickBlock", at = @At("HEAD"), cancellable = true) + private void mcheli$pickBlock(CallbackInfo ci) { + if (hitResult != null && hitResult.getType() != HitResult.Type.MISS && ClientHooks.onPickBlockKey()) { + ci.cancel(); + } + } +} From 10511c89f0342cfc3c3e860e025b460c3b1cd8a2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:57 +0800 Subject: [PATCH 1410/1461] Add PackRepositoryMixin.java to the Fabric 1.20.6 glue --- .../fabric/mixin/PackRepositoryMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java new file mode 100644 index 00000000..965719b9 --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.CommonHooks; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.Arrays; + +/** + * Adds the imported original content to every pack repository, client resources and server data + * alike, which is what Forge and NeoForge do through their pack finder event. + */ +@Mixin(PackRepository.class) +abstract class PackRepositoryMixin { + + @ModifyVariable(method = "", at = @At("HEAD"), argsOnly = true) + private static RepositorySource[] mcheli$addImportedContent(RepositorySource[] sources) { + RepositorySource[] withOurs = Arrays.copyOf(sources, sources.length + 1); + withOurs[sources.length] = CommonHooks.packSource(); + return withOurs; + } +} From 5a72da1c8047139673de25359181cd7a734d9db0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:57 +0800 Subject: [PATCH 1411/1461] Add ParticleEngineMixin.java to the Fabric 1.20.6 glue --- .../fabric/mixin/ParticleEngineMixin.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java new file mode 100644 index 00000000..87ebfeb7 --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/ParticleEngineMixin.java @@ -0,0 +1,32 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.google.common.collect.ImmutableList; +import com.jnhprojects.mcheli.client.render.Glow; +import net.minecraft.client.particle.ParticleEngine; +import net.minecraft.client.particle.ParticleRenderType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +/** + * Vanilla only draws the particle render types on its own list, which leaves our glow and smoke + * particles invisible. Forge and NeoForge draw every render type in use. + */ +@Mixin(ParticleEngine.class) +abstract class ParticleEngineMixin { + @Shadow + @Final + @Mutable + private static List RENDER_ORDER; + + @Inject(method = "", at = @At("RETURN")) + private static void mcheli$addRenderTypes(CallbackInfo ci) { + RENDER_ORDER = ImmutableList.builder().addAll(RENDER_ORDER).add(Glow.PARTICLE_LAYER, Glow.SMOKE_LAYER).build(); + } +} From e714c5e0d86ed5f9dec3516483590c50ba035e58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:58 +0800 Subject: [PATCH 1412/1461] Add RuntimeSoundInstanceMixin.java to the Fabric 1.20.6 glue --- .../mixin/RuntimeSoundInstanceMixin.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java new file mode 100644 index 00000000..35f4032b --- /dev/null +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java @@ -0,0 +1,20 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.concurrent.CompletableFuture; + +/** Streams content pack sounds from disk through Fabric's sound instance hook. */ +@Mixin(RuntimeSoundInstance.class) +abstract class RuntimeSoundInstanceMixin implements FabricSoundInstance { + + @Override + public CompletableFuture getAudioStream(SoundBufferLibrary loader, ResourceLocation id, boolean repeatInstantly) { + return ((RuntimeSoundInstance) (Object) this).openStream(loader, id, repeatInstantly); + } +} From 318ec14300dcbefe9cb6fc462487d70ac06542e1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:58 +0800 Subject: [PATCH 1413/1461] Add feathers.json to the Fabric 1.20.6 glue --- .../src/main/resources/data/c/tags/items/feathers.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fabric-1.20.6/src/main/resources/data/c/tags/items/feathers.json diff --git a/fabric-1.20.6/src/main/resources/data/c/tags/items/feathers.json b/fabric-1.20.6/src/main/resources/data/c/tags/items/feathers.json new file mode 100644 index 00000000..acc0cf54 --- /dev/null +++ b/fabric-1.20.6/src/main/resources/data/c/tags/items/feathers.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:feather" + ] +} From a1d8968af16fa77c7374e088da18b36971d1c26c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:59 +0800 Subject: [PATCH 1414/1461] Add raw_fish.json to the Fabric 1.20.6 glue --- .../main/resources/data/c/tags/items/foods/raw_fish.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 fabric-1.20.6/src/main/resources/data/c/tags/items/foods/raw_fish.json diff --git a/fabric-1.20.6/src/main/resources/data/c/tags/items/foods/raw_fish.json b/fabric-1.20.6/src/main/resources/data/c/tags/items/foods/raw_fish.json new file mode 100644 index 00000000..04d42afb --- /dev/null +++ b/fabric-1.20.6/src/main/resources/data/c/tags/items/foods/raw_fish.json @@ -0,0 +1,8 @@ +{ + "values": [ + "minecraft:cod", + "minecraft:salmon", + "minecraft:tropical_fish", + "minecraft:pufferfish" + ] +} From 6d279e7572e139224732d3f2bb37c4d8aa358096 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:59 +0800 Subject: [PATCH 1415/1461] Add colorless.json to the Fabric 1.20.6 glue --- .../resources/data/c/tags/items/glass_blocks/colorless.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fabric-1.20.6/src/main/resources/data/c/tags/items/glass_blocks/colorless.json diff --git a/fabric-1.20.6/src/main/resources/data/c/tags/items/glass_blocks/colorless.json b/fabric-1.20.6/src/main/resources/data/c/tags/items/glass_blocks/colorless.json new file mode 100644 index 00000000..8aeeecb0 --- /dev/null +++ b/fabric-1.20.6/src/main/resources/data/c/tags/items/glass_blocks/colorless.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:glass" + ] +} From 86bde9fa912eb140a609698e6ff9939f60901d6b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:30:59 +0800 Subject: [PATCH 1416/1461] Add colorless.json to the Fabric 1.20.6 glue --- .../resources/data/c/tags/items/glass_panes/colorless.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fabric-1.20.6/src/main/resources/data/c/tags/items/glass_panes/colorless.json diff --git a/fabric-1.20.6/src/main/resources/data/c/tags/items/glass_panes/colorless.json b/fabric-1.20.6/src/main/resources/data/c/tags/items/glass_panes/colorless.json new file mode 100644 index 00000000..6bae2d7f --- /dev/null +++ b/fabric-1.20.6/src/main/resources/data/c/tags/items/glass_panes/colorless.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:glass_pane" + ] +} From 5078066673c0e502ddc6f453774ca071706128e6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:00 +0800 Subject: [PATCH 1417/1461] Add gunpowders.json to the Fabric 1.20.6 glue --- .../src/main/resources/data/c/tags/items/gunpowders.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fabric-1.20.6/src/main/resources/data/c/tags/items/gunpowders.json diff --git a/fabric-1.20.6/src/main/resources/data/c/tags/items/gunpowders.json b/fabric-1.20.6/src/main/resources/data/c/tags/items/gunpowders.json new file mode 100644 index 00000000..d2e7016f --- /dev/null +++ b/fabric-1.20.6/src/main/resources/data/c/tags/items/gunpowders.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:gunpowder" + ] +} From dbb013715a3c4b23fe9500e6d36d1dc7e27f2189 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:00 +0800 Subject: [PATCH 1418/1461] Drop the placeholder wording from the Fabric 1.20.6 metadata --- fabric-1.20.6/src/main/resources/fabric.mod.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fabric-1.20.6/src/main/resources/fabric.mod.json b/fabric-1.20.6/src/main/resources/fabric.mod.json index 1b87b793..8346f4c2 100644 --- a/fabric-1.20.6/src/main/resources/fabric.mod.json +++ b/fabric-1.20.6/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "${mod_id}", "version": "${mod_version}", "name": "${mod_name}", - "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. This build is a placeholder: the port to this Minecraft version is not done yet. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", + "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", "authors": ["JayMar921 (rework)", "EMB4 (original author)", "Fureniku (CurseForge maintainer)"], "contact": { "homepage": "https://github.com/JnH-Projects/MCHeli-Rework-Forge", @@ -14,8 +14,10 @@ "icon": "mcheli_logo.png", "environment": "*", "entrypoints": { - "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"] + "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"], + "client": ["com.jnhprojects.mcheli.fabric.McheliFabricClient"] }, + "mixins": ["mcheli.mixins.json"], "depends": { "fabricloader": "${loader_version_range}", "fabric-api": "*", From 81c779a482c600cbe54ad4c1e56f304201006222 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:01 +0800 Subject: [PATCH 1419/1461] List the Fabric 1.20.6 mixins --- .../src/main/resources/mcheli.mixins.json | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 fabric-1.20.6/src/main/resources/mcheli.mixins.json diff --git a/fabric-1.20.6/src/main/resources/mcheli.mixins.json b/fabric-1.20.6/src/main/resources/mcheli.mixins.json new file mode 100644 index 00000000..22cab45a --- /dev/null +++ b/fabric-1.20.6/src/main/resources/mcheli.mixins.json @@ -0,0 +1,21 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "PackRepositoryMixin" + ], + "client": [ + "CameraMixin", + "GameRendererInvoker", + "GameRendererMixin", + "LivingEntityRendererMixin", + "MinecraftMixin", + "ParticleEngineMixin", + "RuntimeSoundInstanceMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From 588bc239a5f0e678fbf0054da6a70cffc6ba12d0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:01 +0800 Subject: [PATCH 1420/1461] Set up the Fabric 1.20.6 runs --- fabric-1.20.6/build.gradle | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fabric-1.20.6/build.gradle b/fabric-1.20.6/build.gradle index 35a0c27b..3b06b6c2 100644 --- a/fabric-1.20.6/build.gradle +++ b/fabric-1.20.6/build.gradle @@ -7,10 +7,15 @@ plugins { apply from: rootProject.file('gradle/loader.gradle') +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + loom { runs { configureEach { runDir = "../run-${minecraft_version}" + vmArg "-Dmcheli.legacyZip=${legacyZip}" } server { programArgs '--nogui' @@ -20,7 +25,7 @@ loom { dependencies { minecraft "com.mojang:minecraft:${minecraft_version}" - // 1.20.6 is obfuscated; Mojang's names keep the code the same as on Forge and NeoForge. + // 1.20.6 is obfuscated; Mojang's names keep the shared code the same as on Forge and NeoForge. mappings loom.officialMojangMappings() modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" From d80a869c48caaac40068d79b1bb6d1acac7469a6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:02 +0800 Subject: [PATCH 1421/1461] Stop treating Fabric 1.20.6 as a placeholder --- fabric-1.20.6/gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fabric-1.20.6/gradle.properties b/fabric-1.20.6/gradle.properties index 7e86c3cb..6ec4bb9d 100644 --- a/fabric-1.20.6/gradle.properties +++ b/fabric-1.20.6/gradle.properties @@ -1,4 +1,4 @@ -# Minecraft 1.20.6 on Fabric (placeholder until the port, see docs/DEVELOPMENT.md) +# Minecraft 1.20.6 on Fabric loader=fabric minecraft_version=1.20.6 minecraft_version_range=~1.20.6 @@ -6,4 +6,3 @@ fabric_loader_version=0.20.1 loader_version_range=>=0.16.0 fabric_api_version=0.100.8+1.20.6 java_version=21 -scaffold=true From 4a1b367182bc14ed0490c9c9c9cfd40d35296032 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:02 +0800 Subject: [PATCH 1422/1461] Build the 1.20.6 modules --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f1948a43..008a79ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mod_license=MCHeli Rework Licence 1.0 (see LICENSE) # Platform modules built by this project, one per Minecraft version. Each has its own # gradle.properties with the Minecraft, loader and Java versions. -platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-26.2,neoforge-26.2,fabric-26.2,forge-26.1,neoforge-26.1,fabric-26.1,forge-1.21.11,neoforge-1.21.11,fabric-1.21.11,forge-1.21.5,neoforge-1.21.5,fabric-1.21.5,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 +platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-26.2,neoforge-26.2,fabric-26.2,forge-26.1,neoforge-26.1,fabric-26.1,forge-1.21.11,neoforge-1.21.11,fabric-1.21.11,forge-1.21.5,neoforge-1.21.5,fabric-1.21.5,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1,forge-1.20.6,neoforge-1.20.6,fabric-1.20.6 # Libraries the common module compiles against. Every supported Minecraft version ships them. slf4j_version=2.0.17 From 9d23129d6a7e7164d9bfd1f14cf02ab70c1a1e4a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:03 +0800 Subject: [PATCH 1423/1461] Note the 1.20.6 port in the changelog --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d318f5c..aa2a2c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,13 +15,14 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). the vehicle screen, the drafting table, team manager, UAV stations and the workshop. On 1.21.1 the PVP button of the team manager switches the server's PVP setting until a restart, since that version has no game rule for it, and target markers ignore view bobbing. -- Minecraft 26.2, 26.1, 1.21.11 and 1.21.5 on Forge, NeoForge and Fabric, with everything 26.3 has. On 26.1, - 1.21.11 and 1.21.5 teams pick from the sixteen chat colours, since those versions have no separate team - colours. On 1.21.5 the PVP button of the team manager switches the server's PVP setting until a restart. +- Minecraft 26.2, 26.1, 1.21.11, 1.21.5 and 1.20.6 on Forge, NeoForge and Fabric, with everything 26.3 has. On + 26.1 and older, teams pick from the sixteen chat colours, since those versions have no separate team + colours. On 1.21.5 and 1.20.6 the PVP button of the team manager switches the server's PVP setting until a + restart. ### Changed -- Forge, NeoForge and Fabric on 26.3, 26.2, 26.1, 1.21.11, 1.21.5 and 1.21.1 are all built on every pull request. +- Forge, NeoForge and Fabric on 26.3, 26.2, 26.1, 1.21.11, 1.21.5, 1.21.1 and 1.20.6 are all built on every pull request. - Clients and servers on different loaders cannot play together, which the roadmap used to promise. Each loader refuses the other at its own handshake, before the mod runs. A pack zip still works on every loader when copied by hand. From 0e79c1edc9c21084a5b26804ebd356fbcc7f5e93 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:03 +0800 Subject: [PATCH 1424/1461] List Minecraft 1.20.6 in the README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bf70014d..34664916 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ MCHeli Rework icon Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3**, **26.2**, **26.1**, -**1.21.11**, **1.21.5** and **1.21.1**, on **Forge**, **NeoForge** and **Fabric**. +**1.21.11**, **1.21.5**, **1.21.1** and **1.20.6**, on **Forge**, **NeoForge** and **Fabric**. MCHeli Rework is a ground-up port of MCHeli 1.0.3, a mod that stopped at Minecraft 1.7.10. Every system is rewritten for the modern game, while the original vehicle definitions, models, @@ -92,12 +92,13 @@ Reserved original, the code is source available rather than open source. | 1.21.11 | 61.2.4 or newer in 61.x | 21.11.48 or newer | Loader 0.16 and Fabric API | 21 | | 1.21.5 | 55.1.18 or newer in 55.x | 21.5.101 or newer | Loader 0.16 and Fabric API | 21 | | 1.21.1 | 52.1.0 or newer | 21.1.200 or newer | Loader 0.16 and Fabric API | 21 | +| 1.20.6 | 50.2.14 or newer in 50.x | 20.6.144 or newer | Loader 0.16 and Fabric API | 21 | The launcher bundles the right Java for each version. ## Installing -1. Install Forge, NeoForge or Fabric for Minecraft 26.3, 26.2, 26.1, 1.21.11, 1.21.5 or 1.21.1. On Fabric, also install +1. Install Forge, NeoForge or Fabric for Minecraft 26.3, 26.2, 26.1, 1.21.11, 1.21.5, 1.21.1 or 1.20.6. On Fabric, also install [Fabric API](https://modrinth.com/mod/fabric-api). 2. Download the jar for your loader from the [releases](https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases): From 20d182b40b7d7a58e299b10635ba5ed0589c75d0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:04 +0800 Subject: [PATCH 1425/1461] Tick the 1.20.6 port on the roadmap --- docs/ROADMAP.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7c4b661b..51a391dc 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -309,12 +309,13 @@ added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest - [x] Every resource the repository holds ships in the jar (betas 4 to 8 lacked recipes, the thermal shader, the smoke particle and the drafting table model) - [x] Phases 8 to 12 ported to all three loaders on 1.21.1 -- [ ] Every module in `platforms` (26.3, 26.2, 26.1, 1.21.11, 1.21.5 and 1.21.1 are in) +- [ ] Every module in `platforms` (26.3, 26.2, 26.1, 1.21.11, 1.21.5, 1.21.1 and 1.20.6 are in) - [x] `minecraft-26.2` with its Forge, NeoForge and Fabric glue - [x] `minecraft-26.1` with its Forge, NeoForge and Fabric glue - [x] `minecraft-1.21.11` with its Forge, NeoForge and Fabric glue - [x] `minecraft-1.21.5` with its Forge, NeoForge and Fabric glue -- [ ] `minecraft-1.20.6`, `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its +- [x] `minecraft-1.20.6` with its Forge, NeoForge and Fabric glue +- [ ] `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its placeholder modules with real glue - [ ] `common` compiles for Java 17 (no pattern matching `switch`, no `SequencedCollection` methods) From 3f166727f68a60c629d33da2f6073de90a8e9325 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:04 +0800 Subject: [PATCH 1426/1461] Describe what differs on 1.20.6 --- docs/ARCHITECTURE.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2e287851..4814d9ca 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -51,7 +51,7 @@ loader. The top layer is a thin module per loader and Minecraft version, which c lower layers together with its own glue into one jar. See [DEVELOPMENT.md](DEVELOPMENT.md#three-layers). -Only 26.3, 26.2, 26.1, 1.21.11, 1.21.5 and 1.21.1 have the middle layer so far. The other supported versions have placeholder +Only 26.3, 26.2, 26.1, 1.21.11, 1.21.5, 1.21.1 and 1.20.6 have the middle layer so far. The other supported versions have placeholder loader modules (`scaffold=true`) that compile neither lower layer: just an entry point that logs, against that version's loader and toolchain. Porting a version means adding its `minecraft-` folder, copying the loader glue in and dropping the flag. @@ -197,6 +197,23 @@ the betas it is not kept in step; each final release brings it, and every other - **Teams.** There is no PVP game rule or `TeamColor`. The team manager switches the server's PVP setting, which lasts until a restart, and picks from the `ChatFormatting` colours. +### Minecraft 1.20.6 + +`minecraft-1.20.6` is `minecraft-1.21.1` with these differences: + +- **Data folders.** They still have plural names (`recipes`, `loot_tables`, `tags/items`), so + `ImportTarget.V1_20_6` writes recipes to `data/mcheli/recipes`, and the jar guard in + `gradle/loader.gradle` keeps EMB4's out of both spellings. +- **Vertices.** The older builder: `vertex`, `color`, `uv`, `overlayCoords`, `uv2`, `normal`, and an + `endVertex` to finish each one. +- **Particle render types.** `begin` is handed a builder and `end` a tesselator. +- **Camera.** The view matrix comes from the camera's angles rather than its quaternion. NeoForge's + `ComputeCameraAngles` carries the roll, so it uses the event as Forge does and has no camera mixin. + Fabric hooks after `resetProjectionMatrix` and wraps `Matrix4f.rotationXYZ` to stack the roll on top. +- **Conventional tags.** Forge has none of the `c:` tags, so `forge-1.20.6` defines all our recipes use, + from the vanilla items and any matching `forge:` tag. Fabric lacks five, defined in `fabric-1.20.6`. +- **Respawn.** `TeamManager` finds the respawn point itself, since there is no `DimensionTransition`. + `client` is only referenced from the loader modules behind a physical client check, so a dedicated server never loads a client class. From 4b81d37470f33e8094aafa3c3c14aabb0787a8d7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:05 +0800 Subject: [PATCH 1427/1461] List the 1.20.6 modules and say to count unmatched ingredients --- docs/DEVELOPMENT.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 346827f5..4c271ff5 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -27,6 +27,9 @@ | Forge | 52.1.20 for Minecraft 1.21.1 | Built with ForgeGradle 7 and the official mappings. | | NeoForge | 21.1.265 for Minecraft 1.21.1 | | | Fabric | Fabric API 0.116.17+1.21.1 | Loom remaps the jar, which is obfuscated on 1.21.1. | +| Forge | 50.2.14 for Minecraft 1.20.6 | Built with ForgeGradle 7 and the official mappings. | +| NeoForge | 20.6.144 for Minecraft 1.20.6 | | +| Fabric | Fabric API 0.100.8+1.20.6 | Loom remaps the jar, which is obfuscated on 1.20.6. | All three use Mojang's official names, so the shared code reads the same on every loader. The loader versions of the [placeholder modules](#placeholder-modules) are in their own @@ -53,7 +56,7 @@ Shared properties live in the root `gradle.properties`: `mod_version`, the mod m of loader modules (`platforms`) and the library versions `common` compiles against. Each loader module has its own `gradle.properties` with its loader, Minecraft and Java versions. During the betas up to beta.8, `platforms` listed only `forge-26.3`. Roadmap phase 13 adds modules back as each -one catches up, currently the three each on 26.3, 26.2, 26.1, 1.21.11, 1.21.5 and 1.21.1; the others stay in the repository +one catches up, currently the three each on 26.3, 26.2, 26.1, 1.21.11, 1.21.5, 1.21.1 and 1.20.6; the others stay in the repository until they are ported. `mods.toml`, `neoforge.mods.toml`, `fabric.mod.json` and `pack.mcmeta` are filled in from them at build time. What every loader module shares (source folders, jar contents, manifest, file @@ -82,6 +85,8 @@ minecraft-1.21.5/ the same shared code on Minecraft 1.21.5 forge-1.21.5/, neoforge-1.21.5/, fabric-1.21.5/ the loader glue for 1.21.5 minecraft-1.21.1/ the same shared code, backported to Minecraft 1.21.1 forge-1.21.1/, neoforge-1.21.1/, fabric-1.21.1/ the loader glue for 1.21.1 +minecraft-1.20.6/ the same shared code on Minecraft 1.20.6 +forge-1.20.6/, neoforge-1.20.6/, fabric-1.20.6/ the loader glue for 1.20.6 -/ placeholders for the other supported versions (see below) gradle/loader.gradle what every loader module shares docs/ documentation and content format guides @@ -126,8 +131,7 @@ of them on the command line. After pulling the split, re-import the Gradle proje ### Placeholder modules Every [supported version](ROADMAP.md#supported-versions) has a loader module, but only 26.3, 26.2, -26.1, 1.21.11, 1.21.5 and 1.21.1 are ported. The other 7 (`forge-`, `neoforge-` and `fabric-` for -1.20.6, plus `forge-` and `fabric-` for 1.20.2 and 1.19.4) are placeholders: each pins +26.1, 1.21.11, 1.21.5, 1.21.1 and 1.20.6 are ported. The other 4 (`forge-` and `fabric-` for 1.20.2 and 1.19.4) are placeholders: each pins its loader, mappings and Java version, and holds only an entry point that logs a line at startup and the mod metadata. Their `gradle.properties` sets `scaffold=true`, which tells `gradle/loader.gradle` to leave out `common` and `minecraft-`, so they build before that @@ -146,7 +150,8 @@ version), copy the loader glue from the ported module of the same loader, remove and add the module to `platforms`. A clean compile proves little about a port. Compare what the loaders report as well: a recipe that names an -undefined tag is logged and dropped on one loader while the others load it. Mixin targets, the Forge access transformer, shaders and +undefined tag is logged and dropped on one loader while the others load it, and on 1.21.1 and older it +loads with an ingredient nothing matches, so count those too. Mixin targets, the Forge access transformer, shaders and data files only fail at runtime, so start a client and a dedicated server on every loader of that version, and check a built jar. A loader can also pin a library more strictly than the version the root `gradle.properties` asks for; override it in that module's `gradle.properties`, as NeoForge 26.2 From b7e30bb9a678cd5fcf42dd1d5dc4662ce9d53f4c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:31:05 +0800 Subject: [PATCH 1428/1461] Add 1.20.6 checks for jars, camera roll, recipes and respawn --- docs/TESTING.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/TESTING.md b/docs/TESTING.md index b707e62f..ecc6a935 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -48,6 +48,15 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi another loader is refused by the loaders themselves, with a message naming the one it needs. - [ ] A `serverconfig/mcheli-server.toml` written by 1.1.0-beta.2 (Forge) keeps its values. +### 1.20.6 only + +- [ ] From the built jars on a launcher profile: all three loaders start, and thermal turns on and off. +- [ ] In a banked aircraft, heading east and then north, the horizon tilts the same way on all three + loaders. Forge's camera event applies the roll before the other two rotations, unlike NeoForge's. +- [ ] Sight zoom works on all three loaders. +- [ ] A vehicle's recipe crafts on each loader, including one with gunpowder or feathers. +- [ ] "Respawn all" in the team manager sends a player to their bed, and to world spawn without one. + ### 1.21.5 only - [ ] From the built jars on a launcher profile: all three loaders start, and thermal turns on and off. From e96f222c58ded1dd4e5f06021ffce20351248cc1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:34:44 +0800 Subject: [PATCH 1429/1461] Take 1.20.2 and 1.19.4 off the 1.1.0 plan on the roadmap --- docs/ROADMAP.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 51a391dc..5d60468c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -12,7 +12,7 @@ tagged pre-release that installs and runs on its own. | 3 | `1.0.0-alpha.3` | Weapons and combat | Implemented, in-game test in progress | | 4 | `1.0.0-beta.1` | HUD and polish | Done | | | `1.0.0` | Release: code-only jar | Released | -| 5-13 | `1.1.0-beta.1` to `beta.9` | Content packs, NeoForge and Fabric, equipment, workshop, every release version from 1.19.4 to 26.3 ([plan](#road-to-110)) | In progress | +| 5-13 | `1.1.0-beta.1` to `beta.9` | Content packs, NeoForge and Fabric, equipment, workshop, every release version from 1.20.6 to 26.3 ([plan](#road-to-110)) | In progress | | | `1.1.0` | Release on CurseForge | Planned | ## Phase 1: foundation and flyable helicopters @@ -140,7 +140,7 @@ Rules that hold for every beta: | 11 | `1.1.0-beta.7` | Workshop: model, parts and tuning (Forge 26.3 only) | Released | | 12 | `1.1.0-beta.8` | Workshop: textures, sounds and sharing (Forge 26.3 only) | Released | | | `1.1.0-beta.8.1` | Hotfix: the files missing from the released jars | Released | -| 13 | `1.1.0-beta.9` | Every loader on every supported version, 1.19.4 to 26.3 | Planned | +| 13 | `1.1.0-beta.9` | Every loader on every supported version, 1.20.6 to 26.3 | Planned | | | `1.1.0` | Release on CurseForge | Planned | The order is deliberate. The registry and the source split come first because everything else @@ -166,13 +166,12 @@ only. | 1.21.5 | 21 | yes | yes | yes | | 1.21.1 | 21 | yes | yes | yes | | 1.20.6 | 21 | yes | yes | yes | -| 1.20.2 | 17 | yes | no | yes | -| 1.19.4 | 17 | yes | no | yes | -That is 25 jars per final release. NeoForge starts at 1.20.6 (ModDevGradle, which builds the -NeoForge jars, cannot set up NeoForge 1.20.2), and 1.19.4 is the oldest version. -Older versions are not frozen: every final release ships on the whole list again. Every pair -already has a Gradle module; the ones not ported yet are placeholders that only load and log +That is 21 jars per final release. Older versions are not frozen: every final release ships on the +whole list again. + +1.20.2 and 1.19.4 (Forge and Fabric) are on hold and not part of 1.1.0: they need Java 17 and vehicle +items without data components. Their Gradle modules remain as placeholders that only load and log (see [DEVELOPMENT.md](DEVELOPMENT.md#placeholder-modules)). ## Phase 5: groundwork (`1.1.0-beta.1`) @@ -301,7 +300,7 @@ built here syncs to other players with no extra work. Betas 4 to 8 ship only the Forge 26.3 jar, to keep builds fast. This phase brings the features added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest of the -[supported versions](#supported-versions), down to 1.19.4. All 25 jars ship from here on. +[supported versions](#supported-versions), down to 1.20.6. All 21 jars ship from here on. - [x] A Gradle module for every version and loader pair, the missing ones as placeholders that download their loader and only load and log @@ -309,18 +308,12 @@ added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest - [x] Every resource the repository holds ships in the jar (betas 4 to 8 lacked recipes, the thermal shader, the smoke particle and the drafting table model) - [x] Phases 8 to 12 ported to all three loaders on 1.21.1 -- [ ] Every module in `platforms` (26.3, 26.2, 26.1, 1.21.11, 1.21.5, 1.21.1 and 1.20.6 are in) +- [x] Every supported version's modules in `platforms` - [x] `minecraft-26.2` with its Forge, NeoForge and Fabric glue - [x] `minecraft-26.1` with its Forge, NeoForge and Fabric glue - [x] `minecraft-1.21.11` with its Forge, NeoForge and Fabric glue - [x] `minecraft-1.21.5` with its Forge, NeoForge and Fabric glue - [x] `minecraft-1.20.6` with its Forge, NeoForge and Fabric glue -- [ ] `minecraft-1.20.2` and `minecraft-1.19.4`, each replacing its - placeholder modules with real glue -- [ ] `common` compiles for Java 17 (no pattern matching `switch`, no `SequencedCollection` - methods) -- [ ] Vehicle items keep their vehicle in NBT on 1.20.2 and 1.19.4, since data components only - arrived in 1.20.5 - [ ] Legacy importer and content packs checked on every version - [ ] The phase 7 smoke tests for every jar From c69b4a186dc6599f07fddfabb9b1b0fb9c92b63e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:34:44 +0800 Subject: [PATCH 1430/1461] Say 1.1.0 ships from 1.20.6 to 26.3 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 34664916..678c74f6 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ textures and sounds load unchanged. > equipment are in, down to UAV stations, the drafting table and the team manager. The vehicle > workshop builds new vehicles in game from a Blockbench or Blender model, with their own textures > and sounds, and sends them to a server for its operators to approve. Betas ship the -> Forge 26.3 jar only; 1.1.0 ships on Forge and Fabric for every version from 1.19.4 to 26.3, and -> on NeoForge from 1.20.6 (see [supported versions](docs/ROADMAP.md#supported-versions)). See the +> Forge 26.3 jar only; 1.1.0 ships on Forge, NeoForge and Fabric for every version from +> 1.20.6 to 26.3 (see [supported versions](docs/ROADMAP.md#supported-versions)). See the > [roadmap](docs/ROADMAP.md). ## Showcase video From c77ef00ba45db6df9345f2bfe5a6ef2e68af38f4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:34:45 +0800 Subject: [PATCH 1431/1461] Describe the 1.20.2 and 1.19.4 modules as on hold --- docs/DEVELOPMENT.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 4c271ff5..04eedffe 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4,7 +4,7 @@ | Tool | Version | Notes | |------|---------|-------| -| JDK | 25 (21 for 1.20.6 to 1.21.11, 17 for 1.19.4 and 1.20.2) | Gradle provisions them through the Foojay resolver if none is installed. | +| JDK | 25 (21 for 1.20.6 to 1.21.11, 17 for the placeholders on 1.19.4 and 1.20.2) | Gradle provisions them through the Foojay resolver if none is installed. | | Gradle | 9.8.2 | Use the wrapper (`./gradlew`). Loom 1.19 needs 9.8. | | ForgeGradle | 7.x | Forge modules. Sets up Minecraft through the Mavenizer on first build. | | ModDevGradle | 2.0.x | NeoForge modules. | @@ -130,8 +130,8 @@ of them on the command line. After pulling the split, re-import the Gradle proje ### Placeholder modules -Every [supported version](ROADMAP.md#supported-versions) has a loader module, but only 26.3, 26.2, -26.1, 1.21.11, 1.21.5, 1.21.1 and 1.20.6 are ported. The other 4 (`forge-` and `fabric-` for 1.20.2 and 1.19.4) are placeholders: each pins +Every [supported version](ROADMAP.md#supported-versions) is ported. Four more modules, `forge-` and +`fabric-` for 1.20.2 and 1.19.4, are on hold and not part of 1.1.0. They are placeholders: each pins its loader, mappings and Java version, and holds only an entry point that logs a line at startup and the mod metadata. Their `gradle.properties` sets `scaffold=true`, which tells `gradle/loader.gradle` to leave out `common` and `minecraft-`, so they build before that @@ -159,7 +159,7 @@ and 26.1 do with `jspecify_version`. A first run in a new run folder stops at va screen instead of the title screen, which matters only to anything that automates a client. There is no `neoforge-1.20.2`: ModDevGradle needs a "moddev bundle" that NeoForge 20.2 does not -publish, so NeoForge support starts at 1.20.6, the next supported version. +publish, so NeoForge could not join those placeholders. `Mod/` holds the original `MC1.7.10_mcheli_1.0.3.zip`. It is git-ignored. Download it from the [MCHeli CurseForge page](https://www.curseforge.com/minecraft/mc-mods/mcheli-minecraft-helicopter-mod/files) From 721b23cc6fea14a89434d85db16470d7b75da7ad Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 00:34:45 +0800 Subject: [PATCH 1432/1461] Note in the changelog that 1.20.2 and 1.19.4 are not part of 1.1.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2a2c55..8d5b5908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). ### Changed - Forge, NeoForge and Fabric on 26.3, 26.2, 26.1, 1.21.11, 1.21.5, 1.21.1 and 1.20.6 are all built on every pull request. +- 1.1.0 ships on 26.3, 26.2, 26.1, 1.21.11, 1.21.5, 1.21.1 and 1.20.6 on all three loaders (21 jars). + 1.20.2 and 1.19.4 are on hold and not part of it. - Clients and servers on different loaders cannot play together, which the roadmap used to promise. Each loader refuses the other at its own handshake, before the mod runs. A pack zip still works on every loader when copied by hand. From 5101c579e03550b3675a6cf49e2fe54da15790e0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:13 +0800 Subject: [PATCH 1433/1461] Let code outside the sound instance stream a pack sound on 26.3 --- .../mcheli/client/sound/RuntimeSoundInstance.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java index 6c873144..274b8ac7 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -55,8 +55,9 @@ protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSourc } /** - * Forge and NeoForge call this through their sound instance extension, which it overrides - * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + * NeoForge calls this through its sound instance extension, which it overrides without saying + * so. Fabric has no such method and calls {@link #openStream} from a mixin. Forge declares it + * too but never calls it on this version, see {@code PackSoundBuffers} in its module. */ public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { return openStream(buffers, sound.getPath(), looping); @@ -68,6 +69,11 @@ public CompletableFuture openStream(SoundBufferLibrary buffers, Ide if (source == null) { return buffers.getStream(path, looping); } + return streamFile(source, looping); + } + + /** Streams an OGG file from disk, read as it plays and never cached as a buffer. */ + public static CompletableFuture streamFile(Path source, boolean looping) { return CompletableFuture.supplyAsync(() -> { try { InputStream in = new BufferedInputStream(Files.newInputStream(source)); From 7f7733d96a59ed28efb62271d4ab03cc917652fd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:32 +0800 Subject: [PATCH 1434/1461] Apply the Forge 26.3 access transformer --- forge-26.3/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-26.3/build.gradle b/forge-26.3/build.gradle index c0a19a24..9ce2c73b 100644 --- a/forge-26.3/build.gradle +++ b/forge-26.3/build.gradle @@ -12,6 +12,8 @@ apply from: rootProject.file('gradle/loader.gradle') def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath minecraft { + useDefaultAccessTransformer() + runs { configureEach { workingDir = rootProject.layout.projectDirectory.dir('run') From eefc06f844617d631bfc44acd9024ac901749a3e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:33 +0800 Subject: [PATCH 1435/1461] Open the sound engine's buffer library on Forge 26.3 --- forge-26.3/src/main/resources/META-INF/accesstransformer.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 forge-26.3/src/main/resources/META-INF/accesstransformer.cfg diff --git a/forge-26.3/src/main/resources/META-INF/accesstransformer.cfg b/forge-26.3/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 00000000..f48fac70 --- /dev/null +++ b/forge-26.3/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,2 @@ +# Forge's sound engine reads pack sounds through this library; see PackSoundBuffers. +public-f net.minecraft.client.sounds.SoundEngine soundBuffers From 079dd4b18791410ac1fd6cf95108730dd1dc0e9a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:34 +0800 Subject: [PATCH 1436/1461] Stream pack sounds from the buffer library on Forge 26.3 --- .../mcheli/forge/PackSoundBuffers.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java new file mode 100644 index 00000000..6bf4f891 --- /dev/null +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.minecraft.client.Minecraft; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundEngine; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.ResourceProvider; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +/** + * Streams sounds that only exist in a runtime root. Forge still declares + * {@code SoundInstance.getStream} on this Minecraft version, but its sound engine goes straight to + * the buffer library and never calls it, so {@code RuntimeSoundInstance} has no say here and a pack + * sound would fail to open, with nothing in the log. The field holding the library is opened by an + * access transformer. + */ +final class PackSoundBuffers extends SoundBufferLibrary { + private static final String FOLDER = "sounds/"; + private static final String EXTENSION = ".ogg"; + + private final ResourceProvider resources; + + private PackSoundBuffers(ResourceProvider resources) { + super(resources); + this.resources = resources; + } + + /** Fires on every engine (re)load, so only the first one after construction replaces anything. */ + static void install(SoundEngine engine) { + if (!(engine.soundBuffers instanceof PackSoundBuffers)) { + engine.soundBuffers = new PackSoundBuffers(Minecraft.getInstance().getResourceManager()); + } + } + + @Override + public CompletableFuture getStream(Identifier location, boolean looping) { + Path file = runtimeFile(location); + return file != null ? RuntimeSoundInstance.streamFile(file, looping) : super.getStream(location, looping); + } + + /** The disk file for {@code mcheli:sounds/.ogg}, unless a resource pack has that sound. */ + private @Nullable Path runtimeFile(Identifier location) { + String path = location.getPath(); + if (!location.getNamespace().equals(MCHeli.MOD_ID) || !path.startsWith(FOLDER) || !path.endsWith(EXTENSION) + || resources.getResource(location).isPresent()) { + return null; + } + return RuntimeAssets.soundFile(path.substring(FOLDER.length(), path.length() - EXTENSION.length())); + } +} From acf61188bd6c87d8cf97b2de021b6d472b05c686 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:34 +0800 Subject: [PATCH 1437/1461] Install the pack sound library when the Forge 26.3 sound engine loads --- .../src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java index 41e3115b..2a211ca7 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -17,6 +17,7 @@ import net.minecraftforge.client.event.RenderHandEvent; import net.minecraftforge.client.event.RenderLivingEvent; import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.event.sound.SoundEngineLoadEvent; import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.bus.BusGroup; @@ -32,6 +33,7 @@ static void init(BusGroup modBus) { FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + SoundEngineLoadEvent.BUS.addListener(event -> PackSoundBuffers.install(event.getEngine())); RegisterParticleProvidersEvent.BUS.addListener(event -> { event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); From 6f948c0c802ee2cdf223f9be9bff6d6c59153f61 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:35 +0800 Subject: [PATCH 1438/1461] Let code outside the sound instance stream a pack sound on 26.2 --- .../mcheli/client/sound/RuntimeSoundInstance.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java index fb3ee2e3..e42d8cbd 100644 --- a/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java +++ b/minecraft-26.2/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -54,8 +54,9 @@ protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSourc } /** - * Forge and NeoForge call this through their sound instance extension, which it overrides - * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + * NeoForge calls this through its sound instance extension, which it overrides without saying + * so. Fabric has no such method and calls {@link #openStream} from a mixin. Forge declares it + * too but never calls it on this version, see {@code PackSoundBuffers} in its module. */ public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { return openStream(buffers, sound.getPath(), looping); @@ -67,6 +68,11 @@ public CompletableFuture openStream(SoundBufferLibrary buffers, Ide if (source == null) { return buffers.getStream(path, looping); } + return streamFile(source, looping); + } + + /** Streams an OGG file from disk, read as it plays and never cached as a buffer. */ + public static CompletableFuture streamFile(Path source, boolean looping) { return CompletableFuture.supplyAsync(() -> { try { InputStream in = new BufferedInputStream(Files.newInputStream(source)); From 26546cd67f1e661e5378acd34f22c2cd1f89ce9f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:35 +0800 Subject: [PATCH 1439/1461] Open the sound engine's buffer library on Forge 26.2 --- forge-26.2/src/main/resources/META-INF/accesstransformer.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-26.2/src/main/resources/META-INF/accesstransformer.cfg b/forge-26.2/src/main/resources/META-INF/accesstransformer.cfg index c9ffbad2..94e38b38 100644 --- a/forge-26.2/src/main/resources/META-INF/accesstransformer.cfg +++ b/forge-26.2/src/main/resources/META-INF/accesstransformer.cfg @@ -1,2 +1,4 @@ # Vanilla keeps the post effect setter private on 26.2; the thermal view needs it. public net.minecraft.client.renderer.GameRenderer setPostEffect(Lnet/minecraft/resources/Identifier;)V +# Forge's sound engine reads pack sounds through this library; see PackSoundBuffers. +public-f net.minecraft.client.sounds.SoundEngine soundBuffers From 3508defe3727229079c366bf744bbb6a212bb804 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:36 +0800 Subject: [PATCH 1440/1461] Stream pack sounds from the buffer library on Forge 26.2 --- .../mcheli/forge/PackSoundBuffers.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java diff --git a/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java new file mode 100644 index 00000000..6bf4f891 --- /dev/null +++ b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.minecraft.client.Minecraft; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundEngine; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.ResourceProvider; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +/** + * Streams sounds that only exist in a runtime root. Forge still declares + * {@code SoundInstance.getStream} on this Minecraft version, but its sound engine goes straight to + * the buffer library and never calls it, so {@code RuntimeSoundInstance} has no say here and a pack + * sound would fail to open, with nothing in the log. The field holding the library is opened by an + * access transformer. + */ +final class PackSoundBuffers extends SoundBufferLibrary { + private static final String FOLDER = "sounds/"; + private static final String EXTENSION = ".ogg"; + + private final ResourceProvider resources; + + private PackSoundBuffers(ResourceProvider resources) { + super(resources); + this.resources = resources; + } + + /** Fires on every engine (re)load, so only the first one after construction replaces anything. */ + static void install(SoundEngine engine) { + if (!(engine.soundBuffers instanceof PackSoundBuffers)) { + engine.soundBuffers = new PackSoundBuffers(Minecraft.getInstance().getResourceManager()); + } + } + + @Override + public CompletableFuture getStream(Identifier location, boolean looping) { + Path file = runtimeFile(location); + return file != null ? RuntimeSoundInstance.streamFile(file, looping) : super.getStream(location, looping); + } + + /** The disk file for {@code mcheli:sounds/.ogg}, unless a resource pack has that sound. */ + private @Nullable Path runtimeFile(Identifier location) { + String path = location.getPath(); + if (!location.getNamespace().equals(MCHeli.MOD_ID) || !path.startsWith(FOLDER) || !path.endsWith(EXTENSION) + || resources.getResource(location).isPresent()) { + return null; + } + return RuntimeAssets.soundFile(path.substring(FOLDER.length(), path.length() - EXTENSION.length())); + } +} From 5afde3328e958d2ca86ae0163b5f35dfcad74055 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:36 +0800 Subject: [PATCH 1441/1461] Install the pack sound library when the Forge 26.2 sound engine loads --- .../src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java index fb861c5c..369690c6 100644 --- a/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java +++ b/forge-26.2/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -18,6 +18,7 @@ import net.minecraftforge.client.event.RenderHandEvent; import net.minecraftforge.client.event.RenderLivingEvent; import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.event.sound.SoundEngineLoadEvent; import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.bus.BusGroup; @@ -33,6 +34,7 @@ static void init(BusGroup modBus) { FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + SoundEngineLoadEvent.BUS.addListener(event -> PackSoundBuffers.install(event.getEngine())); RegisterParticleProvidersEvent.BUS.addListener(event -> { event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); From b1a08b5fa9aa73ffcaad7f1f17adbcba6381e225 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:37 +0800 Subject: [PATCH 1442/1461] Let code outside the sound instance stream a pack sound on 26.1 --- .../mcheli/client/sound/RuntimeSoundInstance.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java index fb3ee2e3..e42d8cbd 100644 --- a/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java +++ b/minecraft-26.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -54,8 +54,9 @@ protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSourc } /** - * Forge and NeoForge call this through their sound instance extension, which it overrides - * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + * NeoForge calls this through its sound instance extension, which it overrides without saying + * so. Fabric has no such method and calls {@link #openStream} from a mixin. Forge declares it + * too but never calls it on this version, see {@code PackSoundBuffers} in its module. */ public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { return openStream(buffers, sound.getPath(), looping); @@ -67,6 +68,11 @@ public CompletableFuture openStream(SoundBufferLibrary buffers, Ide if (source == null) { return buffers.getStream(path, looping); } + return streamFile(source, looping); + } + + /** Streams an OGG file from disk, read as it plays and never cached as a buffer. */ + public static CompletableFuture streamFile(Path source, boolean looping) { return CompletableFuture.supplyAsync(() -> { try { InputStream in = new BufferedInputStream(Files.newInputStream(source)); From f36ed6808a369efd986be9a244bb6d4a72f1eb98 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:37 +0800 Subject: [PATCH 1443/1461] Open the sound engine's buffer library on Forge 26.1 --- forge-26.1/src/main/resources/META-INF/accesstransformer.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-26.1/src/main/resources/META-INF/accesstransformer.cfg b/forge-26.1/src/main/resources/META-INF/accesstransformer.cfg index df469780..881256a4 100644 --- a/forge-26.1/src/main/resources/META-INF/accesstransformer.cfg +++ b/forge-26.1/src/main/resources/META-INF/accesstransformer.cfg @@ -1,2 +1,4 @@ # Vanilla keeps the post effect setter private on 26.1; the thermal view needs it. public net.minecraft.client.renderer.GameRenderer setPostEffect(Lnet/minecraft/resources/Identifier;)V +# Forge's sound engine reads pack sounds through this library; see PackSoundBuffers. +public-f net.minecraft.client.sounds.SoundEngine soundBuffers From 7cc905d916f6ae4f4dcc14aea0f5ed0830adf804 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:38 +0800 Subject: [PATCH 1444/1461] Stream pack sounds from the buffer library on Forge 26.1 --- .../mcheli/forge/PackSoundBuffers.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java diff --git a/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java new file mode 100644 index 00000000..6bf4f891 --- /dev/null +++ b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.minecraft.client.Minecraft; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundEngine; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.ResourceProvider; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +/** + * Streams sounds that only exist in a runtime root. Forge still declares + * {@code SoundInstance.getStream} on this Minecraft version, but its sound engine goes straight to + * the buffer library and never calls it, so {@code RuntimeSoundInstance} has no say here and a pack + * sound would fail to open, with nothing in the log. The field holding the library is opened by an + * access transformer. + */ +final class PackSoundBuffers extends SoundBufferLibrary { + private static final String FOLDER = "sounds/"; + private static final String EXTENSION = ".ogg"; + + private final ResourceProvider resources; + + private PackSoundBuffers(ResourceProvider resources) { + super(resources); + this.resources = resources; + } + + /** Fires on every engine (re)load, so only the first one after construction replaces anything. */ + static void install(SoundEngine engine) { + if (!(engine.soundBuffers instanceof PackSoundBuffers)) { + engine.soundBuffers = new PackSoundBuffers(Minecraft.getInstance().getResourceManager()); + } + } + + @Override + public CompletableFuture getStream(Identifier location, boolean looping) { + Path file = runtimeFile(location); + return file != null ? RuntimeSoundInstance.streamFile(file, looping) : super.getStream(location, looping); + } + + /** The disk file for {@code mcheli:sounds/.ogg}, unless a resource pack has that sound. */ + private @Nullable Path runtimeFile(Identifier location) { + String path = location.getPath(); + if (!location.getNamespace().equals(MCHeli.MOD_ID) || !path.startsWith(FOLDER) || !path.endsWith(EXTENSION) + || resources.getResource(location).isPresent()) { + return null; + } + return RuntimeAssets.soundFile(path.substring(FOLDER.length(), path.length() - EXTENSION.length())); + } +} From 79a3048c628abfdea0549c3358a2e474fb5c701d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:38 +0800 Subject: [PATCH 1445/1461] Install the pack sound library when the Forge 26.1 sound engine loads --- .../src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java index fb861c5c..369690c6 100644 --- a/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java +++ b/forge-26.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -18,6 +18,7 @@ import net.minecraftforge.client.event.RenderHandEvent; import net.minecraftforge.client.event.RenderLivingEvent; import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.event.sound.SoundEngineLoadEvent; import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.bus.BusGroup; @@ -33,6 +34,7 @@ static void init(BusGroup modBus) { FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + SoundEngineLoadEvent.BUS.addListener(event -> PackSoundBuffers.install(event.getEngine())); RegisterParticleProvidersEvent.BUS.addListener(event -> { event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); From 1f1aedafac2a862a652fac02a947dc09ccfda8eb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:39 +0800 Subject: [PATCH 1446/1461] Let code outside the sound instance stream a pack sound on 1.21.11 --- .../mcheli/client/sound/RuntimeSoundInstance.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java index b3327f16..4166ffb8 100644 --- a/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java +++ b/minecraft-1.21.11/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -54,8 +54,9 @@ protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSourc } /** - * Forge and NeoForge call this through their sound instance extension, which it overrides - * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + * NeoForge calls this through its sound instance extension, which it overrides without saying + * so. Fabric has no such method and calls {@link #openStream} from a mixin. Forge declares it + * too but never calls it on this version, see {@code PackSoundBuffers} in its module. */ public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { return openStream(buffers, sound.getPath(), looping); @@ -67,6 +68,11 @@ public CompletableFuture openStream(SoundBufferLibrary buffers, Ide if (source == null) { return buffers.getStream(path, looping); } + return streamFile(source, looping); + } + + /** Streams an OGG file from disk, read as it plays and never cached as a buffer. */ + public static CompletableFuture streamFile(Path source, boolean looping) { return CompletableFuture.supplyAsync(() -> { try { InputStream in = new BufferedInputStream(Files.newInputStream(source)); From 7a3c2aa1dc1cb584c0a6c113b8818668110686f6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:39 +0800 Subject: [PATCH 1447/1461] Open the sound engine's buffer library on Forge 1.21.11 --- forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg b/forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg index 8fa0e652..ba580467 100644 --- a/forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg +++ b/forge-1.21.11/src/main/resources/META-INF/accesstransformer.cfg @@ -1,2 +1,4 @@ # Vanilla keeps the post effect setter private on 1.21.11; the thermal view needs it. public net.minecraft.client.renderer.GameRenderer setPostEffect(Lnet/minecraft/resources/Identifier;)V +# Forge's sound engine reads pack sounds through this library; see PackSoundBuffers. +public-f net.minecraft.client.sounds.SoundEngine soundBuffers From ae9d4f66169ad27ec2e879c95d187dac4c1aac4a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:40 +0800 Subject: [PATCH 1448/1461] Stream pack sounds from the buffer library on Forge 1.21.11 --- .../mcheli/forge/PackSoundBuffers.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java diff --git a/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java new file mode 100644 index 00000000..6bf4f891 --- /dev/null +++ b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/PackSoundBuffers.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.minecraft.client.Minecraft; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundEngine; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.ResourceProvider; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +/** + * Streams sounds that only exist in a runtime root. Forge still declares + * {@code SoundInstance.getStream} on this Minecraft version, but its sound engine goes straight to + * the buffer library and never calls it, so {@code RuntimeSoundInstance} has no say here and a pack + * sound would fail to open, with nothing in the log. The field holding the library is opened by an + * access transformer. + */ +final class PackSoundBuffers extends SoundBufferLibrary { + private static final String FOLDER = "sounds/"; + private static final String EXTENSION = ".ogg"; + + private final ResourceProvider resources; + + private PackSoundBuffers(ResourceProvider resources) { + super(resources); + this.resources = resources; + } + + /** Fires on every engine (re)load, so only the first one after construction replaces anything. */ + static void install(SoundEngine engine) { + if (!(engine.soundBuffers instanceof PackSoundBuffers)) { + engine.soundBuffers = new PackSoundBuffers(Minecraft.getInstance().getResourceManager()); + } + } + + @Override + public CompletableFuture getStream(Identifier location, boolean looping) { + Path file = runtimeFile(location); + return file != null ? RuntimeSoundInstance.streamFile(file, looping) : super.getStream(location, looping); + } + + /** The disk file for {@code mcheli:sounds/.ogg}, unless a resource pack has that sound. */ + private @Nullable Path runtimeFile(Identifier location) { + String path = location.getPath(); + if (!location.getNamespace().equals(MCHeli.MOD_ID) || !path.startsWith(FOLDER) || !path.endsWith(EXTENSION) + || resources.getResource(location).isPresent()) { + return null; + } + return RuntimeAssets.soundFile(path.substring(FOLDER.length(), path.length() - EXTENSION.length())); + } +} From a9eceff5675adab735358dbefdfb0046c6a7cf70 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:40 +0800 Subject: [PATCH 1449/1461] Install the pack sound library when the Forge 1.21.11 sound engine loads --- .../src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java index fb861c5c..369690c6 100644 --- a/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java +++ b/forge-1.21.11/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -18,6 +18,7 @@ import net.minecraftforge.client.event.RenderHandEvent; import net.minecraftforge.client.event.RenderLivingEvent; import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.event.sound.SoundEngineLoadEvent; import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.bus.BusGroup; @@ -33,6 +34,7 @@ static void init(BusGroup modBus) { FMLClientSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(() -> ClientHooks.registerMenuScreens(MenuScreens::register))); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerBlockEntityRenderers(event::registerBlockEntityRenderer)); + SoundEngineLoadEvent.BUS.addListener(event -> PackSoundBuffers.install(event.getEngine())); RegisterParticleProvidersEvent.BUS.addListener(event -> { event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new); event.registerSpriteSet(ModParticles.SMOKE.get(), SmokeParticle.Provider::new); From 83c34d864429c46d97c0be05ad7becaf7ae45234 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:41 +0800 Subject: [PATCH 1450/1461] Work out the angles that stack a camera roll on Forge 1.20.6's view matrix --- .../mcheli/physics/AircraftRotation.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/common/src/main/java/com/jnhprojects/mcheli/physics/AircraftRotation.java b/common/src/main/java/com/jnhprojects/mcheli/physics/AircraftRotation.java index 45cba216..16853082 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/physics/AircraftRotation.java +++ b/common/src/main/java/com/jnhprojects/mcheli/physics/AircraftRotation.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli.physics; +import org.joml.Matrix3d; import org.joml.Quaternionf; import org.joml.Vector3f; @@ -12,6 +13,9 @@ * {@code rotY(-yaw) * rotX(pitch) * rotZ(roll)}. */ public final class AircraftRotation { + /** Below this cosine of the middle angle, {@link #rollInXyzOrder} treats the first and last as one axis. */ + private static final double GIMBAL_LOCK = 1.0E-9; + private AircraftRotation() { } @@ -69,6 +73,39 @@ public static float cameraRoll(float yaw, float pitch, float roll, float viewYaw return (float) Math.toDegrees(Math.atan2(onLeft, onUp)); } + /** + * Angles for a view matrix built as {@code rotationXYZ(pitch, yaw + PI, roll)}, the way Forge + * builds it on Minecraft 1.20.6, so that the matrix comes out as a camera roll stacked on top of + * the other two rotations. Fed the plain angles that matrix applies the roll around the world's + * Z axis instead: a bank would look like a bank going south, the opposite one going north, and a + * change of pitch going east or west. + * + * @param yaw the camera yaw, in degrees + * @param pitch the camera pitch, in degrees + * @param roll the roll as {@link #cameraRoll} returns it, in degrees + * @return {@code {yaw, pitch, roll}} in degrees, to pass on to that matrix + */ + public static float[] rollInXyzOrder(float yaw, float pitch, float roll) { + Matrix3d view = new Matrix3d() + .rotationZ(Math.toRadians(-roll)) + .rotateX(Math.toRadians(pitch)) + .rotateY(Math.toRadians(yaw) + Math.PI); + // The matrix above equals rotX(x) * rotY(y) * rotZ(z); read those three back from it. + double cosY = Math.hypot(view.m00, view.m10); + double x; + double z; + if (cosY > GIMBAL_LOCK) { + x = Math.atan2(-view.m21, view.m22); + z = Math.atan2(-view.m10, view.m00); + } else { + // Looking along X: the first and last rotation turn around the same axis, so Z can stay 0. + x = Math.atan2(view.m12, view.m11); + z = 0.0; + } + double y = Math.atan2(view.m20, cosY); + return new float[]{(float) Math.toDegrees(y - Math.PI), (float) Math.toDegrees(x), (float) Math.toDegrees(z)}; + } + public static float wrap(float degrees) { float d = degrees % 360.0F; if (d >= 180.0F) { From 5e498a497cfae874dc4aa3583e02cd42e9d224b1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:41 +0800 Subject: [PATCH 1451/1461] Check the 1.20.6 view angles against the view of the other versions --- .../mcheli/physics/AircraftRotationTest.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/common/src/test/java/com/jnhprojects/mcheli/physics/AircraftRotationTest.java b/common/src/test/java/com/jnhprojects/mcheli/physics/AircraftRotationTest.java index 2e8c3839..2a9bceb7 100644 --- a/common/src/test/java/com/jnhprojects/mcheli/physics/AircraftRotationTest.java +++ b/common/src/test/java/com/jnhprojects/mcheli/physics/AircraftRotationTest.java @@ -1,10 +1,12 @@ package com.jnhprojects.mcheli.physics; +import org.joml.Matrix4f; import org.joml.Quaternionf; import org.joml.Vector3f; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Checks the camera tilt against the hull. The camera rotation is rebuilt the way @@ -68,4 +70,56 @@ void cameraUpStaysOnTheHullsSideOfTheView() { assertEquals(expected.y, cameraUp.y, EPSILON); assertEquals(expected.z, cameraUp.z, EPSILON); } + + /** The view matrix Forge builds on Minecraft 1.20.6 from what its camera angle event returns. */ + private static Matrix4f forgeView(float[] angles) { + return new Matrix4f().rotationXYZ((float) Math.toRadians(angles[1]), (float) (Math.toRadians(angles[0]) + Math.PI), (float) Math.toRadians(angles[2])); + } + + /** The inverse of {@link #camera}: what every other version builds as its view matrix. */ + private static Matrix4f expectedView(float yaw, float pitch, float roll) { + return new Matrix4f().rotation(camera(yaw, pitch, roll).conjugate()); + } + + private static void assertSameRotation(Matrix4f expected, Matrix4f actual) { + Vector3f[] axes = {new Vector3f(1.0F, 0.0F, 0.0F), new Vector3f(0.0F, 1.0F, 0.0F), new Vector3f(0.0F, 0.0F, 1.0F)}; + for (Vector3f axis : axes) { + Vector3f a = expected.transformDirection(new Vector3f(axis)); + Vector3f b = actual.transformDirection(new Vector3f(axis)); + assertEquals(a.x, b.x, EPSILON); + assertEquals(a.y, b.y, EPSILON); + assertEquals(a.z, b.z, EPSILON); + } + } + + @Test + void rollInXyzOrderBuildsTheSameViewOnEveryHeading() { + for (float yaw = -180.0F; yaw <= 180.0F; yaw += 15.0F) { + for (float pitch = -80.0F; pitch <= 80.0F; pitch += 20.0F) { + for (float roll = -170.0F; roll <= 170.0F; roll += 34.0F) { + assertSameRotation(expectedView(yaw, pitch, roll), forgeView(AircraftRotation.rollInXyzOrder(yaw, pitch, roll))); + } + } + } + } + + @Test + void rollInXyzOrderSurvivesGimbalLock() { + // Due east or west and level, a hair of roll leaves the middle angle at its limit. + for (float yaw : new float[]{-90.0F, 90.0F}) { + for (float roll : new float[]{0.0F, 1.0E-4F, -1.0E-4F, 1.0F}) { + assertSameRotation(expectedView(yaw, 0.0F, roll), forgeView(AircraftRotation.rollInXyzOrder(yaw, 0.0F, roll))); + } + } + } + + @Test + void forgesPlainAnglesWouldBeWrong() { + // Why rollInXyzOrder exists: going east, the plain angles do not give the same view at all. + Matrix4f expected = expectedView(-90.0F, 0.0F, -30.0F); + Matrix4f plain = forgeView(new float[]{-90.0F, 0.0F, -30.0F}); + Vector3f expectedUp = expected.transformDirection(new Vector3f(0.0F, 1.0F, 0.0F)); + Vector3f plainUp = plain.transformDirection(new Vector3f(0.0F, 1.0F, 0.0F)); + assertTrue(expectedUp.distance(plainUp) > 0.1F); + } } From aaf850c60018265ffa54505a807340f07a7e2468 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:42 +0800 Subject: [PATCH 1452/1461] Stack the camera roll on the view on Forge 1.20.6 --- .../jnhprojects/mcheli/forge/ForgeClient.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java index fad19b67..e9d93fa5 100644 --- a/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java +++ b/forge-1.20.6/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -7,6 +7,7 @@ import com.jnhprojects.mcheli.client.CameraAngles; import com.jnhprojects.mcheli.client.ClientHooks; import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.physics.AircraftRotation; import com.jnhprojects.mcheli.registry.ModParticles; import net.minecraft.client.Camera; import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; @@ -51,7 +52,10 @@ static void init(IEventBus modBus) { ClientHooks.clientTickEnd(); } }); - forgeBus.addListener((ViewportEvent.ComputeCameraAngles event) -> ClientHooks.computeCameraAngles(new Angles(event))); + forgeBus.addListener((ViewportEvent.ComputeCameraAngles event) -> { + ClientHooks.computeCameraAngles(new Angles(event)); + stackRoll(event); + }); forgeBus.addListener((ViewportEvent.ComputeFov event) -> event.setFOV(ClientHooks.fov((float) event.getFOV()))); forgeBus.addListener((RenderHandEvent event) -> event.setCanceled(ClientHooks.hidesHand())); forgeBus.addListener(EventPriority.NORMAL, false, RenderLivingEvent.Pre.class, @@ -61,6 +65,20 @@ static void init(IEventBus modBus) { forgeBus.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> ClientHooks.loggedOut()); } + /** + * Forge puts the event's roll straight into {@code rotationXYZ} on 1.20.6, so it turns around the + * world's Z axis rather than the view's. The camera's look vectors were set before the event and + * stay as they are, so this only changes the view matrix of this frame. + */ + private static void stackRoll(ViewportEvent.ComputeCameraAngles event) { + if (event.getRoll() != 0.0F) { + float[] angles = AircraftRotation.rollInXyzOrder(event.getYaw(), event.getPitch(), event.getRoll()); + event.setYaw(angles[0]); + event.setPitch(angles[1]); + event.setRoll(angles[2]); + } + } + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { if (event.isUseItem()) { switch (ClientHooks.onUseKey(event.getHand())) { From 1fe1a498dea1b68705cc8f033a8cccea05c71120 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:42 +0800 Subject: [PATCH 1453/1461] Roll the camera the right way on NeoForge 1.20.6 --- .../java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java index 5e0a2e14..42819997 100644 --- a/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java +++ b/neoforge-1.20.6/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -77,6 +77,7 @@ private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered e } } + /** NeoForge turns the camera the other way round its roll axis than {@link CameraAngles} means, as on every version. */ private record Angles(ViewportEvent.ComputeCameraAngles event) implements CameraAngles { @Override public Camera camera() { @@ -100,7 +101,7 @@ public float pitch() { @Override public float roll() { - return event.getRoll(); + return -event.getRoll(); } @Override @@ -115,7 +116,7 @@ public void setPitch(float pitch) { @Override public void setRoll(float roll) { - event.setRoll(roll); + event.setRoll(-roll); } } } From 9ceb2af97757e57f5d4ced55648a190f3b5c25c6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 13:32:43 +0800 Subject: [PATCH 1454/1461] Roll the camera the right way on Fabric 1.20.6 --- .../jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java index c566a389..ac25dab7 100644 --- a/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java +++ b/fabric-1.20.6/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -40,10 +40,13 @@ abstract class GameRendererMixin { }); } - /** Vanilla's view matrix has no roll; it stacks on top of the other two, as NeoForge does it. */ + /** + * Vanilla's view matrix has no roll; it stacks on top of the other two, as NeoForge does it. This + * is the inverse of the camera's rotation, hence the negated roll. + */ @Redirect(method = "renderLevel", at = @At(value = "INVOKE", target = "Lorg/joml/Matrix4f;rotationXYZ(FFF)Lorg/joml/Matrix4f;")) private Matrix4f mcheli$withRoll(Matrix4f matrix, float x, float y, float z) { - return new Matrix4f().rotationZ(mcheli$viewRoll * Mth.DEG_TO_RAD).mul(matrix.rotationXYZ(x, y, z)); + return new Matrix4f().rotationZ(-mcheli$viewRoll * Mth.DEG_TO_RAD).mul(matrix.rotationXYZ(x, y, z)); } @Inject(method = "renderItemInHand", at = @At("HEAD"), cancellable = true) From b6870f4a2c4ac5db483af4ee92b2ef2294a3362f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 14:24:58 +0800 Subject: [PATCH 1455/1461] Start the 1.21.11 pack format range where both pack types accept it --- minecraft-1.21.11/src/main/resources/pack.mcmeta | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft-1.21.11/src/main/resources/pack.mcmeta b/minecraft-1.21.11/src/main/resources/pack.mcmeta index d0514452..f49105ce 100644 --- a/minecraft-1.21.11/src/main/resources/pack.mcmeta +++ b/minecraft-1.21.11/src/main/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { "description": "${mod_name} resources", - "min_format": 75, + "min_format": 82, "max_format": 94 } } From 7a671faa8a030050810b952d9ae23dba0b2f8602 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 14:24:58 +0800 Subject: [PATCH 1456/1461] Describe the Forge sound library, the 1.20.6 camera roll and the 1.21.11 pack format --- docs/ARCHITECTURE.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4814d9ca..d86edf6d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -78,10 +78,15 @@ fires its event, so the shared code sees the same thing on every loader: | Use and pick block keys | `InputEvent.InteractionKeyMappingTriggered` | the same | mixin on `Minecraft` | | Rider tilt | `RenderLivingEvent.Pre` | `RenderLivingEvent.Pre` | mixin on `LivingEntityRenderer.submit` | | Imported content pack | `AddPackFindersEvent` | `AddPackFindersEvent` | mixin on the `PackRepository` constructor | -| Streamed sounds | `SoundInstance.getStream` | `SoundInstance.getStream` | mixin adding `getAudioStream` to `RuntimeSoundInstance` | +| Streamed sounds | `SoundInstance.getStream` up to 1.21.5; `PackSoundBuffers` from 1.21.11 | `SoundInstance.getStream` | mixin adding `getAudioStream` to `RuntimeSoundInstance` | NeoForge rolls the camera the other way round from Forge, so its mixin negates the roll. +Forge 1.21.11 and later still declare `SoundInstance.getStream`, but their `SoundEngine` calls +`SoundBufferLibrary.getStream` directly, and a failed stream leaves no trace in the log. So +`forge-` swaps the library for its own subclass on `SoundEngineLoadEvent`, through an access +transformer on the field. The subclass only steps in for an `mcheli` sound no resource pack has. + ### Minecraft 26.2 `minecraft-26.2` is `minecraft-26.3` with these differences: @@ -134,6 +139,10 @@ Forge sets the official mappings and Fabric uses the remapping Loom plugin. - **Interaction.** A click on the hull arrives through `interactAt`, as on 1.21.1. - **Camera.** `GameRenderer.updateCamera` calls `Camera.setup`, and NeoForge and Fabric hook after that and at the head of `extractCamera`. Field of view is `GameRenderer.getFov`. +- **Pack metadata.** One `pack.mcmeta` is read both as client resources and as server data, and each has + its own limit: resources refuse `supported_formats` above format 64, data requires it up to 81. So a range + can only start at 82, above the game's own resource format of 75. All three loaders still load the mod's + resources, since mod packs are not refused for their format. - **Fabric API.** The older names: `KeyBindingHelper`, `ParticleFactoryRegistry`, `FabricItemGroup`, `PayloadTypeRegistry.playS2C`, and on `CustomIngredient` `getMatchingItems` and `toDisplay`. - **Conventional tags.** Forge on 1.21.x has `forge:gunpowder` and `forge:feathers` but not the `c:` @@ -207,9 +216,12 @@ the betas it is not kept in step; each final release brings it, and every other - **Vertices.** The older builder: `vertex`, `color`, `uv`, `overlayCoords`, `uv2`, `normal`, and an `endVertex` to finish each one. - **Particle render types.** `begin` is handed a builder and `end` a tesselator. -- **Camera.** The view matrix comes from the camera's angles rather than its quaternion. NeoForge's - `ComputeCameraAngles` carries the roll, so it uses the event as Forge does and has no camera mixin. - Fabric hooks after `resetProjectionMatrix` and wraps `Matrix4f.rotationXYZ` to stack the roll on top. +- **Camera.** The view matrix comes from the camera's angles rather than its quaternion, and each loader + gets the roll in differently. NeoForge's `ComputeCameraAngles` stacks it on top of the view with the + opposite sign, so its glue negates it and needs no camera mixin. Fabric hooks after + `resetProjectionMatrix` and wraps `Matrix4f.rotationXYZ` to do the same. Forge passes the event's roll + into `rotationXYZ` itself, where it turns around the world's Z axis, so `ForgeClient` replaces all three + angles with `AircraftRotation.rollInXyzOrder`, which builds the same matrix. - **Conventional tags.** Forge has none of the `c:` tags, so `forge-1.20.6` defines all our recipes use, from the vanilla items and any matching `forge:` tag. Fabric lacks five, defined in `fabric-1.20.6`. - **Respawn.** `TeamManager` finds the respawn point itself, since there is no `DimensionTransition`. From ef3adbfe1e7824e6eb0108942d481dbea62e5a2f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 14:24:59 +0800 Subject: [PATCH 1457/1461] Say how to test a pack sound and what a banked view on 1.20.6 should look like --- docs/TESTING.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index ecc6a935..cdc2f174 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -31,7 +31,8 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi - [ ] Fire a missile from a helicopter or jet: the use key fires without swinging the arm, the pick block key switches weapons, the weapon HUD draws, the missile hits and explodes. - [ ] Right-clicking the hull outside the main hit box boards the vehicle (the arm swings). -- [ ] Engine and rotor sounds play, including a content pack sound (the example pack). +- [ ] Engine and rotor sounds play. Also a content pack sound: the example pack has none, so give a + workshop vehicle an OGG for its engine or a weapon and listen to it. - [ ] `config/mcheli-client.toml` exists; setting `nightGlow = false` and restarting removes the glow. - [ ] Join a dedicated server running the same jar: the server log has no `mcheli` errors, the @@ -51,8 +52,9 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi ### 1.20.6 only - [ ] From the built jars on a launcher profile: all three loaders start, and thermal turns on and off. -- [ ] In a banked aircraft, heading east and then north, the horizon tilts the same way on all three - loaders. Forge's camera event applies the roll before the other two rotations, unlike NeoForge's. +- [ ] In a flight with real turns, banked right, the cockpit stays upright on screen and the horizon's right + side rises, on every heading and all three loaders. Held at a fixed bank in a driver this already + matches 1.21.1 (see `AircraftRotation.rollInXyzOrder`); nobody has flown it. - [ ] Sight zoom works on all three loaders. - [ ] A vehicle's recipe crafts on each loader, including one with gunpowder or feathers. - [ ] "Respawn all" in the team manager sends a player to their bed, and to world spawn without one. From 8b107030fd3f1a944da98069f32e6587a211587c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 14:24:59 +0800 Subject: [PATCH 1458/1461] Mark phase 13 released and move the hands-on smoke tests to 1.1.0 --- docs/ROADMAP.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5d60468c..821a9eb7 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -140,7 +140,7 @@ Rules that hold for every beta: | 11 | `1.1.0-beta.7` | Workshop: model, parts and tuning (Forge 26.3 only) | Released | | 12 | `1.1.0-beta.8` | Workshop: textures, sounds and sharing (Forge 26.3 only) | Released | | | `1.1.0-beta.8.1` | Hotfix: the files missing from the released jars | Released | -| 13 | `1.1.0-beta.9` | Every loader on every supported version, 1.20.6 to 26.3 | Planned | +| 13 | `1.1.0-beta.9` | Every loader on every supported version, 1.20.6 to 26.3 | Released | | | `1.1.0` | Release on CurseForge | Planned | The order is deliberate. The registry and the source split come first because everything else @@ -314,13 +314,21 @@ added since beta.3 to NeoForge, Fabric and 1.21.1, and ports the mod to the rest - [x] `minecraft-1.21.11` with its Forge, NeoForge and Fabric glue - [x] `minecraft-1.21.5` with its Forge, NeoForge and Fabric glue - [x] `minecraft-1.20.6` with its Forge, NeoForge and Fabric glue -- [ ] Legacy importer and content packs checked on every version -- [ ] The phase 7 smoke tests for every jar +- [x] Legacy importer and content packs checked on every version: a scripted client on each of the 21 + modules loads the imported recipes and the example pack, spawns its vehicle and plays a pack + sound from the server. Server sync of packs moved to the smoke tests below +- [x] Pack sounds play on every Forge version (silent on 1.21.11 and later before this phase) +- [x] The camera roll on 1.20.6 follows the hull on all three loaders + +Scripted clients found the bugs above, but nobody has played these jars, so the phase 7 smoke tests moved +to the 1.1.0 release. Only the Forge 26.3 jar has been started from a launcher profile. ## 1.1.0 release No new features after beta.9; a `1.1.0-rc.N` follows only if beta.9 testing finds problems. +- [ ] The phase 7 smoke tests for every jar ([TESTING.md](TESTING.md#smoke-test-every-jar)), each from a + launcher profile, with a real flight, weapons, sound by ear and a pack download from a server - [ ] Every phase checklist done on every supported Minecraft version - [ ] A 1.0.0 world and the beta worlds load with their vehicles and items intact - [ ] A pack made in the workshop on one version loads on every other supported version From f085b6a053fa65e023095b726402229cd5cc8e5c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 14:25:00 +0800 Subject: [PATCH 1459/1461] Say the project is at phase 13 and ships every jar --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 678c74f6..ca4e18c0 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,14 @@ MCHeli Rework is a ground-up port of MCHeli 1.0.3, a mod that stopped at Minecra system is rewritten for the modern game, while the original vehicle definitions, models, textures and sounds load unchanged. -> **Status:** on the way to 1.1.0, now at phase 12 (`1.1.0-beta.8.1`). Every helicopter, plane, +> **Status:** on the way to 1.1.0, now at phase 13 (`1.1.0-beta.9`). Every helicopter, plane, > tank, car and ground vehicle can be placed, crewed and driven with the original handling, all > 229 weapons fire, and the scripted HUDs, fuel, the vehicle screen and all of the original's > equipment are in, down to UAV stations, the drafting table and the team manager. The vehicle > workshop builds new vehicles in game from a Blockbench or Blender model, with their own textures -> and sounds, and sends them to a server for its operators to approve. Betas ship the -> Forge 26.3 jar only; 1.1.0 ships on Forge, NeoForge and Fabric for every version from -> 1.20.6 to 26.3 (see [supported versions](docs/ROADMAP.md#supported-versions)). See the -> [roadmap](docs/ROADMAP.md). +> and sounds, and sends them to a server for its operators to approve. From `1.1.0-beta.9` on, +> every jar ships: Forge, NeoForge and Fabric for every version from 1.20.6 to 26.3 (see +> [supported versions](docs/ROADMAP.md#supported-versions)). See the [roadmap](docs/ROADMAP.md). ## Showcase video From 1e334e2711155f82abdab3ebcab66ddae7c9813a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 14:25:00 +0800 Subject: [PATCH 1460/1461] Date 1.1.0-beta.9 and note the pack sound fix on Forge --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5b5908..04a236e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). ## [Unreleased] +## [1.1.0-beta.9] - 2026-11-07 + +Phase 13 of the road to 1.1.0: every loader on every supported version. This beta ships 21 jars, one per +loader and Minecraft version, from 1.20.6 to 26.3. Betas are not uploaded to CurseForge. Worlds from +1.1.0-beta.8.1 load as before, and the network protocol is unchanged (11). Only the Forge 26.3 jar has been +started from a launcher profile so far, and nobody has flown on the new jars yet; see the smoke test in +[docs/TESTING.md](docs/TESTING.md) before relying on one. + ### Added - NeoForge and Fabric on Minecraft 26.3 catch up with Forge: everything from phases 8 to 12 now runs @@ -36,6 +44,10 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). - On 1.21.1 the E-767 recipe failed to load, since its UAV station ingredient used the newer format. - On Forge 1.21.1, recipes that use gunpowder or feathers could not be crafted, since Forge names those tags differently from the conventional ones our recipes use. The Forge modules on 1.21.x now define them. +- Sounds that come from a content pack or the workshop were silent on Forge 26.3, engine loops and + one-shots alike, and left a sound channel open. Forge's sound engine on 1.21.11 and later never asks the + sound for its stream, so the file on disk was never read. Forge 1.21.11, 26.1, 26.2 and 26.3 now read it + from the sound engine's own library. ## [1.1.0-beta.8.1] - 2026-11-06 @@ -748,6 +760,7 @@ Phase 1: foundation and flyable helicopters. - Free look moved from Left Ctrl to Left Alt, because Left Ctrl is now the vanilla sprint key. - Roll damping runs per tick instead of per frame, so handling no longer depends on frame rate. +[1.1.0-beta.9]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.9 [1.1.0-beta.8.1]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.8.1 [1.1.0-beta.8]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.8 [1.1.0-beta.7]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.7 From af5997db994662922798c333e8b4607e00d3c006 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Sat, 7 Nov 2026 14:25:01 +0800 Subject: [PATCH 1461/1461] Set the version to 1.1.0-beta.9 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 008a79ad..60d02c3c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ net.minecraftforge.gradle.merge-source-sets=true mod_id=mcheli mod_name=MCHeli Rework mod_group=com.jnhprojects.mcheli -mod_version=1.1.0-beta.8.1 +mod_version=1.1.0-beta.9 mod_authors=JayMar921 (rework), EMB4 (original author), Fureniku (CurseForge maintainer) mod_license=MCHeli Rework Licence 1.0 (see LICENSE)