Загрузка данных


package com.fanisdeop.minezone.features.weapon.client.render;

import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

import javax.annotation.Nullable;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL20;
import org.lwjgl.util.glu.Project;

import com.fanisdeop.minezone.client.render.mzrender.backend.GLStateSanitizer;
import com.fanisdeop.minezone.common.item.ItemWeaponAttachment;
import com.fanisdeop.minezone.core.util.WeaponLayoutLoader;
import com.fanisdeop.minezone.features.weapon.common.config.AnimationDef;
import com.fanisdeop.minezone.features.weapon.common.config.ItemCustomWeapon;
import com.fanisdeop.minezone.features.weapon.common.config.WeaponData;
import com.fanisdeop.minezone.features.weapon.common.config.WeaponStatCalculator;
import com.modularmods.mcgltf.IGltfModelReceiver;
import com.modularmods.mcgltf.ItemCameraTransformsHelper;
import com.modularmods.mcgltf.RenderedGltfModel;
import com.modularmods.mcgltf.RenderedGltfScene;
import com.modularmods.mcgltf.animation.GltfAnimationCreator;
import com.modularmods.mcgltf.animation.InterpolatedChannel;

import de.javagl.jgltf.model.AnimationModel;
import de.javagl.jgltf.model.GltfModel;
import de.javagl.jgltf.model.NodeModel;
import de.javagl.jgltf.model.io.Buffers;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.block.model.ItemCameraTransforms.TransformType;
import net.minecraft.client.renderer.tileentity.TileEntityItemStackRenderer;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;

@SideOnly(Side.CLIENT)
public class WeaponModelRenderer extends TileEntityItemStackRenderer implements IGltfModelReceiver {

    private static final Logger LOGGER = LogManager.getLogger(WeaponModelRenderer.class);

    private static final float NEAR_PLANE           = 0.01f;
    private static final float FAR_PLANE            = 1000.0f;
    private static final float KICK_BACK_FACTOR     = 1.0f;
    private static final float KICK_VERTICAL_FACTOR = 0.0f;
    private static final float RELOAD_UNAIM_SPEED   = 0.12f;
    
    private static final FloatBuffer EJECT_PROJ_BUF = BufferUtils.createFloatBuffer(16);

    private boolean ejectCacheValid = false;
    private double ejectWorldX, ejectWorldY, ejectWorldZ;
    private double ejectRgtX, ejectRgtY, ejectRgtZ;
    private double ejectUpX,  ejectUpY,  ejectUpZ;
    private double ejectFwdX, ejectFwdY, ejectFwdZ;
    private float ejectLocalSideSign = 1.0f;

    public static class EjectionDebugInfo {
        public boolean valid;
        public String weaponId;
        public String socketName;
        public double originX, originY, originZ;
        public double fwdX, fwdY, fwdZ;
        public double rgtX, rgtY, rgtZ;
        public double upX, upY, upZ;
        public double velX, velY, velZ;
        public float[] baseVel;
        public float spread;
        public long lastUpdateTime;
    }

    public static final EjectionDebugInfo DEBUG_EJECTION_INFO = new EjectionDebugInfo();
    public static boolean DEBUG_SHOW_EJECTION_GIZMO = false;

    public static class AdsDebugInfo {
        public boolean valid;
        public String weaponId;
        public String sightNodeName;
        public float sightWorldX, sightWorldY, sightWorldZ;
        public float adsTx, adsTy, adsTz;
        public boolean isAuto;
    }

    public static final AdsDebugInfo DEBUG_ADS_INFO = new AdsDebugInfo();

    public static final float STALCRAFT_ADS_SCALE = 1.0f;

    public static final class ModelState {
        public final RenderedGltfScene         scene;
        public final GltfModel                 gltfModel;
        public final List<InterpolatedChannel> track;

        ModelState(RenderedGltfScene scene, GltfModel gltfModel, List<InterpolatedChannel> track) {
            this.scene     = scene;
            this.gltfModel = gltfModel;
            this.track     = track;
        }
    }

    public static final class OpticAdsParameters {
        public boolean hasExternalOptic = false;
        public float scale = 1.0f;
        public float rotX = 0f;
        public float rotY = 0f;
        public float rotZ = 0f;
        public float customX = Float.NaN;
        public float customY = Float.NaN;
        public float customZ = Float.NaN;
        public float targetEyeReliefZ = -0.140f;
        public float viewmodelFov = 70.0f;
        public float scopeZoom = 1.0f;
        @Nullable public GltfModel opticModel = null;
        @Nullable public NodeModel opticAnchorNode = null;
        public float[] socketWorldPos = {0f, 0f, 0f};
    }

    private static final class AdsPose {
        float tx, ty, tz;
        float[] quat = {0, 0, 0, 1};
        float fov;
        float targetScale = 1.0f;
    }

    // =========================================================================
    // DTO И СТРУКТУРЫ ЭЖЕКЦИИ ГИЛЬЗ
    // =========================================================================
    public static final String[] EJECTION_NODE_CANDIDATES = {
            "sleeve_spawn", "sleeve",
            "casing_node", "ejection_node", "ejection_port", "shell_eject", "casing_eject", "eject_port", "ejector",
            "socket_eject", "bone_eject", "extractor", "shell_socket", "eject", "ejection",
            "casing", "shell", "gilza", "casing_spawn", "casing_out", "casing_point", "bullet_eject", "socket_casing", "bone_casing",
            "chamber", "patron", "slide", "bolt"
    };

    public static final String[] SIGHT_NODE_CANDIDATES = {
            "sight", "default_sight", "sight_rear", "sight_front", "sight_point",
            "aim", "aim_point", "iron_sight", "ironsight", "iron_sights",
            "mushka", "celik", "pricel", "diopter"
    };

    public static final class EjectionTransformResult {
        public final double posX, posY, posZ;
        public final double velX, velY, velZ;
        public final double axisX, axisY, axisZ;
        public final float  angularSpeed;
        public final float  scale;
        public final float  lightX, lightY;

        public EjectionTransformResult(
                double posX, double posY, double posZ,
                double velX, double velY, double velZ,
                double axisX, double axisY, double axisZ,
                float angularSpeed, float scale,
                float lightX, float lightY) {
            this.posX = posX; this.posY = posY; this.posZ = posZ;
            this.velX = velX; this.velY = velY; this.velZ = velZ;
            this.axisX = axisX; this.axisY = axisY; this.axisZ = axisZ;
            this.angularSpeed = angularSpeed;
            this.scale = scale;
            this.lightX = lightX; this.lightY = lightY;
        }
    }

    private final String           weaponId;
    private final ResourceLocation modelLocation;
    private final ResourceLocation iconLocation;
    private       WeaponData       weaponData;
    public volatile ModelState     modelState = null;

    public ModelState getModelState() {
        return modelState;
    }

    private final WeaponAnimator        animator         = new WeaponAnimator();
    private final WeaponEffectsRenderer effectsRenderer  = new WeaponEffectsRenderer();
    private final WeaponTextureResolver textureResolver;
    private final DetectorRenderer      detectorRenderer = new DetectorRenderer();

    private boolean isAiming          = false;
    private float   adsProgress       = 0f;
    private float   sprintProgress    = 0f;
    private long    lastFrameTime     = System.nanoTime();
    private float   currentShootSpeed = 1.0f;
    private long    lastShootCallTime = 0L;

    private float lastPitch  = 0f;
    private float lastYaw    = 0f;
    private float eyeOffsetX = 0f;
    private float eyeOffsetY = 0f;

    private AdsPose cachedAdsPose = null;
    private int     cachedAdsNbtHash = 0;

    private static final FloatBuffer CURRENT_MODELVIEW_MATRIX = BufferUtils.createFloatBuffer(16);

    public WeaponModelRenderer(WeaponData data) {
        Objects.requireNonNull(data, "WeaponData must not be null");

        String gltfPath = (data.gltfPath != null && !data.gltfPath.isEmpty())
                ? data.gltfPath
                : data.resolveGltfPath();

        if (gltfPath == null || gltfPath.isEmpty()) {
            throw new IllegalArgumentException("Cannot resolve glTF path for weapon/attachment: " + data.id);
        }

        gltfPath = gltfPath.replace('\\', '/').trim();
        if (gltfPath.startsWith("/"))                gltfPath = gltfPath.substring(1);
        if (gltfPath.startsWith("assets/minezone/")) gltfPath = gltfPath.substring("assets/minezone/".length());
        if (gltfPath.startsWith("minezone:"))        gltfPath = gltfPath.substring("minezone:".length());

        this.weaponId        = data.id != null ? data.id : "";
        this.weaponData      = data;
        this.modelLocation   = new ResourceLocation("minezone", gltfPath);
        this.iconLocation    = new ResourceLocation("minezone", "textures/items/weapons/" + this.weaponId + ".png");
        this.textureResolver = new WeaponTextureResolver(this.weaponId, this.modelLocation);
    }

    private boolean forcedAds = false;
    private float forcedAdsValue = 0.0f;

    public float getAdsProgress()          { return adsProgress; }
    public void  setAiming(boolean aiming) {
        if (this.isAiming != aiming) {
            this.isAiming = aiming;
            if (aiming && DEBUG_ADS_INFO.valid) {
                System.out.println("[MZ-ADS-INFO] Вход в прицел (" + this.weaponId + "): кость='" + DEBUG_ADS_INFO.sightNodeName 
                        + "', auto=" + DEBUG_ADS_INFO.isAuto + ", offset=(" + DEBUG_ADS_INFO.adsTx + ", " + DEBUG_ADS_INFO.adsTy + ", " + DEBUG_ADS_INFO.adsTz + ")");
            }
        }
    }

    public void setForcedAds(boolean forced, float progress) {
        this.forcedAds = forced;
        this.forcedAdsValue = progress;
        if (forced) {
            this.adsProgress = progress;
            this.isAiming = (progress > 0.5f);
        }
    }

    public void warmupGpuBuffers() {
        ModelState state = this.modelState;
        if (state == null || state.scene == null) return;
        Minecraft mc = Minecraft.getMinecraft();
        if (mc.world == null) return;
        try {
            GlStateManager.pushMatrix();
            GlStateManager.colorMask(false, false, false, false);
            GlStateManager.depthMask(false);

            state.scene.renderForShaderMod();
        } catch (Throwable ignored) {
        } finally {
            GlStateManager.colorMask(true, true, true, true);
            GlStateManager.depthMask(true);
            GlStateManager.popMatrix();
            GlStateManager.setActiveTexture(GL13.GL_TEXTURE0);
            GlStateManager.enableTexture2D();
            GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f);
        }
    }

    public void notifyEquipped() {
        animator.notifyEquipped();
        lastFrameTime = System.nanoTime();
        this.cachedAdsPose = null;
        WeaponScopeRenderer.hasOpticCenterSample = false;
        WeaponShaderManager.warmupWeapon(this.weaponId, this.textureResolver);
        this.warmupGpuBuffers();

        try {
            Minecraft mc = Minecraft.getMinecraft();
            if (mc.player != null) {
                ItemStack stack = mc.player.getHeldItemMainhand();
                net.minecraft.nbt.NBTTagCompound at = WeaponAttachmentRenderer.getAttachmentsTag(stack);
                if (at != null && !at.hasNoTags()) {
                    for (String slotKey : at.getKeySet()) {
                        String rawId = WeaponAttachmentRenderer.extractRawId(at.getTag(slotKey));
                        if (rawId != null && !rawId.trim().isEmpty()) {
                            String cleanId = rawId.trim();
                            if (cleanId.contains(":")) cleanId = new ResourceLocation(cleanId).getResourcePath();
                            WeaponModelRenderer attR = WeaponAttachmentRenderer.resolveRenderer(rawId, cleanId);
                            if (attR != null) {
                                WeaponShaderManager.warmupWeapon(cleanId, attR.getTextureResolver());
                                attR.warmupGpuBuffers();
                            }
                        }
                    }
                }
            }
        } catch (Throwable ignored) {}
    }

    private boolean isSilenced = false;

    public boolean isSilencedWeapon() {
        Minecraft mc = Minecraft.getMinecraft();
        if (mc.player != null) {
            ItemStack held = mc.player.getHeldItemMainhand();
            if (held != null && !held.isEmpty() && held.getItem() instanceof ItemCustomWeapon) {
                WeaponStatCalculator.CalculatedStats st = WeaponStatCalculator.calculate(held);
                if (st != null) {
                    this.isSilenced = st.isSilenced;
                    return st.isSilenced;
                }
            }
        }
        return this.isSilenced;
    }

    public void triggerMuzzleFlash() {
        if (isSilencedWeapon()) {
            effectsRenderer.clearMuzzleFlash();
            return;
        }
        effectsRenderer.triggerMuzzleFlash();
    }

    public void playAnimation(String animId) { playAnimation(animId, 1.0f); }

    public void playAnimation(String animId, float speed) {
        boolean isDry = animId != null && animId.contains("dry");
        boolean isShoot = animId != null && !isDry && (animId.startsWith("shoot") || animId.contains("shoot") || animId.contains("fire"));
        if (isShoot) {
            this.currentShootSpeed = speed;
            this.lastShootCallTime = System.currentTimeMillis();
            if (!isSilencedWeapon()) {
                triggerMuzzleFlash();
            }
        } else {
            this.currentShootSpeed = 1.0f;
        }
        animator.playAnimation(resolveLiveWeaponData(), animId);
    }

    public void stopAnimation(String slot) {
        if (slot != null) animator.playAnimation(resolveLiveWeaponData(), "");
    }

    public void    resetToIdle()      { animator.resetToIdle(resolveLiveWeaponData()); }
    public boolean isPlayingOneShot() { return animator.isPlayingOneShot(); }
    public WeaponAnimator getAnimator() { return animator; }

    @Nullable public String getActiveSlot() { return animator.getActiveSlot(); }
    public WeaponData getWeaponData() { return weaponData; }
    public void setWeaponData(WeaponData data) { this.weaponData = data; }
    public WeaponTextureResolver getTextureResolver() { return textureResolver; }
    public boolean isModelReady() { return modelState != null && modelState.scene != null; }

    public GltfModel getGltfModel() {
        return modelState != null ? modelState.gltfModel : null;
    }

    @Override
    public ResourceLocation getModelLocation() { return modelLocation; }

    private static final byte[] DUMMY_1X1_PNG = new byte[] {
        (byte)-119, (byte)80, (byte)78, (byte)71, (byte)13, (byte)10, (byte)26, (byte)10,
        (byte)0, (byte)0, (byte)0, (byte)13, (byte)73, (byte)72, (byte)68, (byte)82,
        (byte)0, (byte)0, (byte)0, (byte)1, (byte)0, (byte)0, (byte)0, (byte)1,
        (byte)8, (byte)6, (byte)0, (byte)0, (byte)0, (byte)31, (byte)21, (byte)-60,
        (byte)-119, (byte)0, (byte)0, (byte)0, (byte)11, (byte)73, (byte)68, (byte)65,
        (byte)84, (byte)120, (byte)-38, (byte)99, (byte)-8, (byte)15, (byte)4, (byte)0,
        (byte)9, (byte)-5, (byte)3, (byte)-3, (byte)104, (byte)-6, (byte)28, (byte)-52,
        (byte)0, (byte)0, (byte)0, (byte)0, (byte)73, (byte)69, (byte)78, (byte)68,
        (byte)-82, (byte)66, (byte)96, (byte)-126
    };

    @Override
    public boolean isReceiveSharedModel(GltfModel gltfModel, List<Runnable> gltfRenderDataList) {
        if (gltfModel != null && gltfModel.getImageModels() != null) {
            for (de.javagl.jgltf.model.ImageModel imgModel : gltfModel.getImageModels()) {
                if (imgModel instanceof de.javagl.jgltf.model.impl.DefaultImageModel) {
                    de.javagl.jgltf.model.impl.DefaultImageModel dim = (de.javagl.jgltf.model.impl.DefaultImageModel) imgModel;
                    if (dim.getBufferViewModel() == null) {
                        try {
                            if (dim.getImageData() == null) {
                                dim.setImageData(Buffers.create(DUMMY_1X1_PNG));
                            }
                        } catch (Throwable t) {
                            dim.setImageData(Buffers.create(DUMMY_1X1_PNG));
                        }
                    }
                }
            }
        }
        return true;
    }

    @Override
    public void onReceiveSharedModel(RenderedGltfModel renderedModel) {
        if (renderedModel == null) return;

        Minecraft.getMinecraft().addScheduledTask(() -> {
            try {
                RenderedGltfScene scene = renderedModel.renderedGltfScenes.get(0);
                List<InterpolatedChannel> track = extractFirstTrack(renderedModel);

                WeaponData live = resolveLiveWeaponData();
                if (live != null) live.validateAnimations();

                textureResolver.extractTextureNamesFromGltf(renderedModel.gltfModel);
                forceDoubleSidedForNode(renderedModel.gltfModel, "optika");
                GltfNodeHelper.setNodeVisible(renderedModel.gltfModel, "stencil", false);

                // Патчим текстуры MCglTF для прямой привязки DDS текстур
                try {
                    java.lang.reflect.Field texMapField = RenderedGltfModel.class.getDeclaredField("textureModelToGlTexture");
                    texMapField.setAccessible(true);
                    @SuppressWarnings("unchecked")
                    Map<de.javagl.jgltf.model.TextureModel, Integer> texMap =
                            (Map<de.javagl.jgltf.model.TextureModel, Integer>) texMapField.get(renderedModel);
                    if (texMap != null) {
                        String basePath = getModelLocation().getResourcePath();
                        String dir = basePath.substring(0, basePath.lastIndexOf('/') + 1);

                        for (Map.Entry<de.javagl.jgltf.model.TextureModel, Integer> entry : texMap.entrySet()) {
                            de.javagl.jgltf.model.TextureModel tm = entry.getKey();
                            de.javagl.jgltf.model.ImageModel im = tm != null ? tm.getImageModel() : null;
                            String uri = (im != null) ? (im.getUri() != null ? im.getUri() : im.getName()) : null;

                            if (uri != null && !uri.isEmpty()) {
                                String base = uri.endsWith(".png") ? uri.substring(0, uri.length() - 4) : (uri.endsWith(".dds") ? uri.substring(0, uri.length() - 4) : uri);
                                String unskinnedBase = base.replace("_golden_", "_")
                                                           .replace("_death_flesh_", "_")
                                                           .replace("_green_", "_")
                                                           .replace("_haki_", "_")
                                                           .replace("_newyear", "")
                                                           .replace("_camo_", "_");
                                if (unskinnedBase.endsWith(".002")) unskinnedBase = unskinnedBase.substring(0, unskinnedBase.length() - 5);

                                List<ResourceLocation> candidatesList = new ArrayList<>();
                                if (!unskinnedBase.equals(base)) {
                                    candidatesList.add(new ResourceLocation(getModelLocation().getResourceDomain(), dir + unskinnedBase + ".dds"));
                                    candidatesList.add(new ResourceLocation(getModelLocation().getResourceDomain(), dir + unskinnedBase + ".png"));
                                }
                                candidatesList.add(new ResourceLocation(getModelLocation().getResourceDomain(), dir + base + ".dds"));
                                candidatesList.add(new ResourceLocation(getModelLocation().getResourceDomain(), dir + uri));
                                candidatesList.add(new ResourceLocation(getModelLocation().getResourceDomain(), dir + base + ".png"));

                                String wId = weaponId.contains("/") ? weaponId.substring(weaponId.lastIndexOf('/') + 1) : weaponId;
                                candidatesList.add(new ResourceLocation(getModelLocation().getResourceDomain(), dir + wId + ".dds"));
                                candidatesList.add(new ResourceLocation(getModelLocation().getResourceDomain(), dir + wId + ".png"));
                                candidatesList.add(new ResourceLocation(getModelLocation().getResourceDomain(), "textures/items/weapons/" + base + ".dds"));
                                candidatesList.add(new ResourceLocation(getModelLocation().getResourceDomain(), "textures/items/weapons/" + base + ".png"));

                                for (ResourceLocation cLoc : candidatesList) {
                                    int ddsId = com.fanisdeop.minezone.client.render.DdsTextureLoader.getOrLoadTexture(cLoc);
                                    if (ddsId > 0) {
                                        entry.setValue(ddsId);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                } catch (Throwable t) {
                    LOGGER.warn("[WeaponModelRenderer] Texture map patch error: {}", t.getMessage());
                }

                modelState = new ModelState(scene, renderedModel.gltfModel, track);
                animator.onModelReceived();
                effectsRenderer.onModelReceived();
                WeaponAttachmentRenderer.captureRestTranslations(renderedModel.gltfModel);

                WeaponShaderManager.warmupWeapon(this.weaponId, this.textureResolver);
                this.warmupGpuBuffers();
            } catch (Exception e) {
                LOGGER.error("Failed to load shared glTF model inside WeaponModelRenderer structure", e);
            }
        });
    }

    public static boolean isExcludedName(String name) {
        if (name == null) return false;
        String s = name.toLowerCase(Locale.ROOT);
        if (!s.contains("handle")) {
            if (s.contains("hands") || s.contains("hand.") || s.contains("hand_")
                    || s.contains("_hand") || s.contains("handl") || s.contains("handr")
                    || s.contains("arm") || s.contains("sleeve") || s.contains("player")) {
                return true;
            }
        }
        if (s.contains("ammo") || s.contains("bullet") || s.contains("bulet")
                || s.contains("patron") || s.contains("cartridge") || s.contains("gilza")
                || s.contains("casing") || s.contains("magazine_alpha")) {
            return true;
        }
        if (s.endsWith("_2") && (s.contains("korpus") || s.contains("plane.000")
                || s.contains("body.001") || s.contains("body_2"))) {
            return true;
        }
        if (s.contains("glass") || s.contains("glas") || s.contains("steklo")
                || s.contains("lens") || s.contains("linza") || s.contains("optic")
                || s.contains("reticle") || s.contains("stencil") || s.contains("flash")
                || s.contains("laser") || s.contains("wire") || s.contains("wires")
                || s.contains("provod") || s.contains("cable") || s.contains("cord")
                || s.contains("tube") || s.contains("grass") || s.contains("peq")) {
            return true;
        }
        return false;
    }

    public static boolean isExcludedNode(NodeModel node) {
        return isExcluded(node, null, null);
    }

    public static boolean isExcluded(NodeModel node, de.javagl.jgltf.model.MeshModel mesh, de.javagl.jgltf.model.MeshPrimitiveModel prim) {
        if (node != null) {
            if (isExcludedName(node.getName())) return true;
            if (node.getMeshModels() != null) {
                for (de.javagl.jgltf.model.MeshModel m : node.getMeshModels()) {
                    if (m != null) {
                        if (isExcludedName(m.getName())) return true;
                        if (m.getMeshPrimitiveModels() != null) {
                            for (de.javagl.jgltf.model.MeshPrimitiveModel p : m.getMeshPrimitiveModels()) {
                                if (p != null && p.getMaterialModel() != null) {
                                    if (isExcludedName(p.getMaterialModel().getName())) return true;
                                }
                            }
                        }
                    }
                }
            }
        }
        if (mesh != null) {
            if (isExcludedName(mesh.getName())) return true;
            if (mesh.getMeshPrimitiveModels() != null) {
                for (de.javagl.jgltf.model.MeshPrimitiveModel p : mesh.getMeshPrimitiveModels()) {
                    if (p != null && p.getMaterialModel() != null) {
                        if (isExcludedName(p.getMaterialModel().getName())) return true;
                    }
                }
            }
        }
        if (prim != null && prim.getMaterialModel() != null) {
            if (isExcludedName(prim.getMaterialModel().getName())) return true;
        }
        return false;
    }

    private void wrapExcludedNodeCommands(RenderedGltfModel renderedModel) {
        if (renderedModel == null || renderedModel.renderedGltfScenes == null) return;
        try {
            java.lang.reflect.Field field = RenderedGltfModel.class.getDeclaredField("rootNodeModelToCommands");
            field.setAccessible(true);
            @SuppressWarnings("unchecked")
            Map<NodeModel, org.apache.commons.lang3.tuple.Triple<List<Runnable>, List<Runnable>, List<Runnable>>> map =
                    (Map<NodeModel, org.apache.commons.lang3.tuple.Triple<List<Runnable>, List<Runnable>, List<Runnable>>>) field.get(renderedModel);

            if (map != null) {
                for (Map.Entry<NodeModel, org.apache.commons.lang3.tuple.Triple<List<Runnable>, List<Runnable>, List<Runnable>>> entry : map.entrySet()) {
                    NodeModel node = entry.getKey();
                    if (node != null && isExcludedNode(node)) {
                        org.apache.commons.lang3.tuple.Triple<List<Runnable>, List<Runnable>, List<Runnable>> triple = entry.getValue();
                        if (triple != null) {
                            List<Runnable> shaderCmds = triple.getRight();
                            if (shaderCmds != null && !shaderCmds.isEmpty()) {
                                List<Runnable> orig = new ArrayList<>(shaderCmds);
                                shaderCmds.clear();
                                shaderCmds.add(() -> WeaponShaderManager.setMeshPaintable(false));
                                shaderCmds.addAll(orig);
                                shaderCmds.add(() -> WeaponShaderManager.setMeshPaintable(true));
                            }
                            List<Runnable> vanillaCmds = triple.getMiddle();
                            if (vanillaCmds != null && !vanillaCmds.isEmpty()) {
                                List<Runnable> orig = new ArrayList<>(vanillaCmds);
                                vanillaCmds.clear();
                                vanillaCmds.add(() -> WeaponShaderManager.setMeshPaintable(false));
                                vanillaCmds.addAll(orig);
                                vanillaCmds.add(() -> WeaponShaderManager.setMeshPaintable(true));
                            }
                        }
                    }
                }

                // Re-populate shaderModRenderCommands and vanillaRenderCommands with the wrapped node commands
                for (RenderedGltfScene scene : renderedModel.renderedGltfScenes) {
                    if (scene != null && scene.shaderModRenderCommands != null) {
                        scene.shaderModRenderCommands.clear();
                        if (renderedModel.gltfModel != null && renderedModel.gltfModel.getSceneModels() != null) {
                            for (de.javagl.jgltf.model.SceneModel sceneModel : renderedModel.gltfModel.getSceneModels()) {
                                if (sceneModel != null && sceneModel.getNodeModels() != null) {
                                    for (NodeModel node : sceneModel.getNodeModels()) {
                                        org.apache.commons.lang3.tuple.Triple<List<Runnable>, List<Runnable>, List<Runnable>> triple = map.get(node);
                                        if (triple != null && triple.getRight() != null) {
                                            scene.shaderModRenderCommands.addAll(triple.getRight());
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        } catch (Throwable t) {
            LOGGER.warn("Failed to wrap excluded node commands for weapon: " + weaponId, t);
        }
    }

    private List<InterpolatedChannel> extractFirstTrack(RenderedGltfModel model) {
        List<AnimationModel> anims = model.gltfModel.getAnimationModels();
        if (anims == null || anims.isEmpty()) return Collections.emptyList();
        List<InterpolatedChannel> ch = GltfAnimationCreator.createGltfAnimation(anims.get(0));
        return ch != null ? ch : Collections.emptyList();
    }

    private WeaponData resolveLiveWeaponData() {
        if (weaponData == null) return null;
        WeaponData live = com.fanisdeop.minezone.features.weapon.common.config.WeaponConfigManager
                .getWeapon(weaponData.id);
        if (live != null && live != weaponData) weaponData = live;
        return weaponData;
    }

    @Override
    public void renderByItem(ItemStack stack) {
        if (ScopeCameraManager.isRenderingScopePass()) {
            return;
        }

        TransformType type = com.fanisdeop.minezone.client.render.mzrender.MZRenderState.currentTransform;
        if (type == null) type = ItemCameraTransformsHelper.getCurrentTransformType();
        if (type == null || type == TransformType.NONE) type = TransformType.GUI;

        if (type == TransformType.GUI || type == TransformType.GROUND || type == TransformType.FIXED) {
            render2DIcon(stack);
            return;
        }

        if (type != TransformType.FIRST_PERSON_RIGHT_HAND
                && type != TransformType.THIRD_PERSON_RIGHT_HAND
                && type != TransformType.FIRST_PERSON_LEFT_HAND
                && type != TransformType.THIRD_PERSON_LEFT_HAND) {
            return;
        }

        ModelState state = this.modelState;
        if (state == null) return;

        Minecraft mc = Minecraft.getMinecraft();
        if (mc.world == null) return;

        int prevActiveTexture = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE);

        GL13.glActiveTexture(GL13.GL_TEXTURE0);
        int prevTex0_2D = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
        boolean prevTex0_2D_Enabled = GL11.glIsEnabled(GL11.GL_TEXTURE_2D);

        GL13.glActiveTexture(GL13.GL_TEXTURE1);
        int prevTex1_2D = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
        boolean prevTex1_2D_Enabled = GL11.glIsEnabled(GL11.GL_TEXTURE_2D);

        GLStateSanitizer.saveCurrentProgram();
        GlStateManager.pushMatrix();
        try {
            WeaponShaderManager.bindWeaponShader(stack, weaponId, textureResolver, weaponData, false);

            if (type == TransformType.FIRST_PERSON_RIGHT_HAND
                    || type == TransformType.FIRST_PERSON_LEFT_HAND) {
                renderFirstPerson(stack, mc, state);
            } else {
                com.fanisdeop.minezone.features.weapon.client.render.TpAnimManager.InterpolatedState animState =
                        com.fanisdeop.minezone.features.weapon.client.render.TpAnimManager.currentRenderState;
                if (animState != null) {
                    GlStateManager.translate(animState.weaponOffset[0], animState.weaponOffset[1], animState.weaponOffset[2]);
                    if (animState.weaponRot[2] != 0f) GlStateManager.rotate(animState.weaponRot[2], 0f, 0f, 1f);
                    if (animState.weaponRot[1] != 0f) GlStateManager.rotate(animState.weaponRot[1], 0f, 1f, 0f);
                    if (animState.weaponRot[0] != 0f) GlStateManager.rotate(animState.weaponRot[0], 1f, 0f, 0f);
                }
                renderThirdPerson(mc, state, false, stack);
            }
        } finally {
            GLStateSanitizer.restoreProgram();
            GLStateSanitizer.sanitize();
            
            WeaponShaderManager.hardResetStates();
            WeaponScopeRenderer.cleanGlClientStates();

            GlStateManager.setActiveTexture(GL13.GL_TEXTURE1);
            GlStateManager.bindTexture(prevTex1_2D);
            if (prevTex1_2D_Enabled) GlStateManager.enableTexture2D(); else GlStateManager.disableTexture2D();

            GlStateManager.setActiveTexture(GL13.GL_TEXTURE0);
            GlStateManager.bindTexture(prevTex0_2D);
            if (prevTex0_2D_Enabled) GlStateManager.enableTexture2D(); else GlStateManager.disableTexture2D();

            GlStateManager.setActiveTexture(prevActiveTexture);
            GlStateManager.color(1f, 1f, 1f, 1f);

            GlStateManager.popMatrix();
        }
    }

    private void renderThirdPerson(Minecraft mc, ModelState state,
                                   boolean isShaderPackActive, ItemStack stack) {
        boolean isEmpty = (stack != null) && (stack.getItem() instanceof ItemCustomWeapon)
                && ((ItemCustomWeapon) stack.getItem()).isEmpty(stack);
        boolean hasGL = WeaponAttachmentRenderer.hasUnderbarrelEquipped(stack);
        animator.setWeaponState(isEmpty, hasGL);
        animator.resetPoseToIdle(weaponData, state.track);
        GL11.glTranslatef(weaponData.tpX, weaponData.tpY, weaponData.tpZ);
        GL11.glRotatef(weaponData.tpRotY, 0f, 1f, 0f);
        GL11.glRotatef(weaponData.tpRotX, 1f, 0f, 0f);
        GL11.glRotatef(weaponData.tpRotZ, 0f, 0f, 1f);
        GL11.glScalef(weaponData.tpScale, weaponData.tpScale, weaponData.tpScale);

        WeaponShaderManager.setupLightingForWeapon(
                mc, TransformType.THIRD_PERSON_RIGHT_HAND, isShaderPackActive, weaponData);

        boolean hideEkran = weaponData != null && weaponData.isDetector
                && DetectorRenderer.isDischarged(stack);

        effectsRenderer.hideHandNodes(state.gltfModel);
        if (hideEkran) GltfNodeHelper.setNodeVisible(state.gltfModel, "ekran", false);

        List<String> hiddenDefaults = Collections.emptyList();
        try {
            hiddenDefaults = WeaponAttachmentRenderer.hideDefaultMeshesForAttachments(stack, state.gltfModel, this.weaponId);
            renderGltfScene(state.scene);
            WeaponAttachmentRenderer.renderAttachments(stack, state, false, adsProgress, this.weaponId, eyeOffsetX, eyeOffsetY);
        } finally {
            WeaponAttachmentRenderer.restoreDefaultMeshes(state.gltfModel, hiddenDefaults);
            if (hideEkran) GltfNodeHelper.setNodeVisible(state.gltfModel, "ekran", true);
            effectsRenderer.restoreHandNodes();
        }
    }

    public void renderSceneForInventory(ItemStack stack) {
        ModelState state = this.modelState;
        if (state == null || state.scene == null) return;

        boolean hasGL = WeaponAttachmentRenderer.hasUnderbarrelEquipped(stack);
        // В меню осмотра всегда выставляем нейтральное/закрытое оружие (isEmpty=false),
        // чтобы пистолеты не были на затворной задержке, а переломные ружья не были раскрыты
        animator.setWeaponState(false, hasGL);
        animator.resetPoseToInspect(weaponData, state.track);
        effectsRenderer.hideHandNodes(state.gltfModel);

        GLStateSanitizer.saveCurrentProgram();
        GlStateManager.pushMatrix();
        try {
            WeaponShaderManager.bindWeaponShader(stack, weaponId, textureResolver, weaponData, true);
            WeaponShaderManager.setupLightingForInventory(Minecraft.getMinecraft(), weaponData);

            int prog = WeaponShaderManager.getWeaponShaderProgram();
            if (prog != 0) {
                int locSpecExp = GL20.glGetUniformLocation(prog, "specularExponent");
                if (locSpecExp != -1) GL20.glUniform1f(locSpecExp, 14.0f);
            }

            boolean hideEkran = weaponData != null && weaponData.isDetector
                    && DetectorRenderer.isDischarged(stack);
            if (hideEkran) GltfNodeHelper.setNodeVisible(state.gltfModel, "ekran", false);

            List<String> hiddenDefaults = Collections.emptyList();
            try {
                hiddenDefaults = WeaponAttachmentRenderer.hideDefaultMeshesForAttachments(stack, state.gltfModel, this.weaponId);
                renderGltfScene(state.scene);
                WeaponAttachmentRenderer.renderAttachments(stack, state, true, adsProgress, this.weaponId, eyeOffsetX, eyeOffsetY);
            } finally {
                WeaponAttachmentRenderer.restoreDefaultMeshes(state.gltfModel, hiddenDefaults);
                if (hideEkran) GltfNodeHelper.setNodeVisible(state.gltfModel, "ekran", true);
            }
        } finally {
            GLStateSanitizer.restoreProgram();
            GLStateSanitizer.sanitize();
            GlStateManager.popMatrix();
            effectsRenderer.restoreHandNodes();
        }
    }

    @Deprecated
    public void renderSceneForInventory() { renderSceneForInventory(ItemStack.EMPTY); }

    private void renderGltfScene(RenderedGltfScene scene) {
        int current    = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM);
        int weaponProg = WeaponShaderManager.getWeaponShaderProgram();

        if (weaponProg != 0 && current == weaponProg) {
            GlStateManager.enableDepth();
            GlStateManager.depthMask(true);
            GlStateManager.depthFunc(GL11.GL_LEQUAL);
            scene.renderForShaderMod();
        } else {
            scene.renderForVanilla();
        }
    }

    public void renderRawScene() {
        ModelState state = this.modelState;
        if (state == null || state.scene == null) return;

        int current    = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM);
        int weaponProg = WeaponShaderManager.getWeaponShaderProgram();

        GlStateManager.enableDepth();
        GL11.glEnable(GL11.GL_DEPTH_TEST);
        GlStateManager.depthMask(true);
        GL11.glDepthMask(true);
        GlStateManager.color(1f, 1f, 1f, 1f);
        GL11.glColor4f(1f, 1f, 1f, 1f);

        GL11.glEnable(GL12.GL_RESCALE_NORMAL);
        GlStateManager.enableRescaleNormal();

        if (weaponProg != 0 && current == weaponProg) {
            int oldPass = net.minecraftforge.client.MinecraftForgeClient.getRenderPass();
            try {
                net.minecraftforge.client.ForgeHooksClient.setRenderPass(0);
                GlStateManager.depthMask(true);
                GL11.glDepthMask(true);
                GlStateManager.disableBlend();
                GL11.glDisable(GL11.GL_BLEND);
                state.scene.renderForShaderMod();

                GlStateManager.enableBlend();
                GL11.glEnable(GL11.GL_BLEND);
                GlStateManager.tryBlendFuncSeparate(
                        GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA,
                        GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO);
                GlStateManager.depthMask(false);
                GL11.glDepthMask(false);
                net.minecraftforge.client.ForgeHooksClient.setRenderPass(1);
                state.scene.renderForShaderMod();
                GlStateManager.depthMask(true);
                GL11.glDepthMask(true);
            } finally {
                net.minecraftforge.client.ForgeHooksClient.setRenderPass(oldPass);
                GlStateManager.disableBlend();
                GL11.glDisable(GL11.GL_BLEND);
                GlStateManager.enableDepth();
            }
        } else {
            state.scene.renderForVanilla();
        }
    }

    private void applyRecoilKick(Minecraft mc, float smoothT) {
        float kick = RecoilSystem.getInstance().getVisualKick(weaponData, mc.getRenderPartialTicks());
        if (kick > 0f) {
            float transDamp = lerp(1.0f, 0.15f, smoothT);
            float rotDamp   = lerp(1.0f, 0.02f, smoothT);

            GL11.glTranslatef(0f, kick * KICK_VERTICAL_FACTOR * transDamp, kick * KICK_BACK_FACTOR * transDamp);
            GL11.glRotatef(-kick * 4f * rotDamp, 1f, 0f, 0f);
        }
    }

    private void render2DIcon() {
        render2DIcon(null);
    }

    private void render2DIcon(ItemStack stack) {
        Minecraft mc = Minecraft.getMinecraft();
        ResourceLocation iconToBind = this.iconLocation;

        if (stack != null && stack.getItem() instanceof com.fanisdeop.minezone.common.item.ItemWeaponAttachment) {
            com.fanisdeop.minezone.common.item.ItemWeaponAttachment att =
                    (com.fanisdeop.minezone.common.item.ItemWeaponAttachment) stack.getItem();
            String iconName = att.getIconName();
            if (iconName != null && !iconName.isEmpty()) {
                String cleanIcon = iconName.replace("minezone:", "").replace(".png", "");
                if (cleanIcon.startsWith("textures/items/attachments/")) {
                    cleanIcon = cleanIcon.substring("textures/items/attachments/".length());
                } else if (cleanIcon.startsWith("items/attachments/")) {
                    cleanIcon = cleanIcon.substring("items/attachments/".length());
                }
                iconToBind = new ResourceLocation("minezone", "textures/items/attachments/" + cleanIcon + ".png");
            }
        }

        mc.getTextureManager().bindTexture(iconToBind);

        GlStateManager.pushMatrix();
        GlStateManager.disableLighting();
        GlStateManager.enableAlpha();
        GlStateManager.enableBlend();
        GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA);
        GlStateManager.translate(-0.5f, -0.5f, 0.6f);

        FloatBuffer buf = CURRENT_MODELVIEW_MATRIX;
        buf.clear();
        GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, buf);
        float[] m = new float[16];
        buf.get(m);

        float sX = WeaponTransformHelper.len3(m[0], m[1], m[2]);
        float sY = WeaponTransformHelper.len3(m[4], m[5], m[6]);
        float sZ = WeaponTransformHelper.len3(m[8], m[9], m[10]);
        m[0] = sX; m[1] = 0;   m[2]  = 0;
        m[4] = 0;  m[5] = -sY; m[6]  = 0;
        m[8] = 0;  m[9] = 0;   m[10] = sZ;

        buf.clear();
        buf.put(m).flip();
        GL11.glLoadMatrix(buf);

        Tessellator tess = Tessellator.getInstance();
        BufferBuilder bb = tess.getBuffer();
        bb.begin(GL11.GL_QUADS, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_TEX);
        bb.pos(-0.5, -0.5, 0).tex(0, 1).endVertex();
        bb.pos( 0.5, -0.5, 0).tex(1, 1).endVertex();
        bb.pos( 0.5,  0.5, 0).tex(1, 0).endVertex();
        bb.pos(-0.5,  0.5, 0).tex(0, 0).endVertex();
        tess.draw();

        GlStateManager.enableLighting();
        GlStateManager.popMatrix();
        mc.getTextureManager().bindTexture(net.minecraft.client.renderer.texture.TextureMap.LOCATION_BLOCKS_TEXTURE);
    }

    private static boolean isAimBlockedBySlot(@Nullable String slot) {
        if (slot == null) return false;
        String s = slot.toLowerCase(Locale.ROOT);
        if (s.contains("reload")) return true;
        if (s.contains("draw") || s.contains("equip")) return true;
        if (s.contains("inspect")) return true;
        if (s.contains("melee") || s.contains("bash")) return true;
        if (s.contains("run_start") || s.contains("run_end")) return true;
        if (s.contains("holster") || s.contains("chamber") || s.contains("check")) return true;
        return false;
    }

    private static float lerp(float a, float b, float t) { return a + (b - a) * t; }

    public void dispose() {
        modelState = null;
        animator.resetToIdle(null);
    }

    private void forceDoubleSidedForNode(GltfModel gltfModel, String nodeName) {
        if (gltfModel == null || gltfModel.getNodeModels() == null) return;
        for (NodeModel node : gltfModel.getNodeModels()) {
            if (node.getName() == null) continue;
            String n = node.getName().toLowerCase(Locale.ROOT);
            if (!n.equals(nodeName) && !n.startsWith(nodeName + ".")) continue;
            if (node.getMeshModels() == null) continue;

            for (de.javagl.jgltf.model.MeshModel mesh : node.getMeshModels()) {
                if (mesh.getMeshPrimitiveModels() == null) continue;
                for (de.javagl.jgltf.model.MeshPrimitiveModel prim : mesh.getMeshPrimitiveModels()) {
                    Object material = prim.getMaterialModel();
                    if (material == null) continue;
                    patchDoubleSidedField(material);
                }
            }
        }
    }

    private void patchDoubleSidedField(Object material) {
        Class<?> cls = material.getClass();
        while (cls != null) {
            try {
                java.lang.reflect.Field f = cls.getDeclaredField("doubleSided");
                f.setAccessible(true);
                f.set(material, Boolean.TRUE);
                return;
            } catch (NoSuchFieldException e) {
                cls = cls.getSuperclass();
            } catch (Throwable t) {
                return;
            }
        }
        try {
            java.lang.reflect.Method setter = material.getClass().getMethod("setDoubleSided", boolean.class);
            setter.invoke(material, true);
        } catch (Throwable ignored) {}
    }

    public static final String[] UNIFIED_OPTIC_PRIORITY = {
            "optika", "reticle_plane", "stencil", "ocular", "eyepiece", "lens_rear", "rear_lens", "inner_lens", "aim_node", "lensaglas", "lens", "glass"
    };

    private static boolean hasMeshDirect(NodeModel node) {
        return node != null && node.getMeshModels() != null && !node.getMeshModels().isEmpty();
    }

    public static NodeModel findOpticAnchor(GltfModel attModel) {
        if (attModel == null || attModel.getNodeModels() == null) return null;

        for (String name : UNIFIED_OPTIC_PRIORITY) {
            NodeModel n = WeaponAttachmentRenderer.findGltfNode(attModel, name);
            if (n != null && hasMeshDirect(n)) return n;
        }

        for (NodeModel n : attModel.getNodeModels()) {
            if (n.getName() == null) continue;
            String ln = n.getName().toLowerCase(Locale.ROOT);
            if (ln.contains("mount") || ln.contains("cap") || ln.contains("ring") || ln.contains("clamp")
                    || ln.contains("body") || ln.contains("barrel") || ln.contains("silencer") || ln.contains("mag")) continue;

            if (ln.contains("optika") || ln.contains("reticle") || ln.contains("aim_node")
                    || ln.contains("lens") || ln.contains("ocular") || ln.contains("eyecup") || ln.contains("glass")) {
                if (hasMeshDirect(n)) return n;
            }
        }
        return null;
    }

    private static float[] computeOpticCenterInAttachment(GltfModel attModel, NodeModel lensNode) {
        if (attModel == null || lensNode == null) return new float[]{ 0f, 0.035f, 0f };

        float[] mat = localChainMatrix(attModel, lensNode);
        float[] aabb = scanMeshAabb(lensNode);
        float cx = 0f, cy = 0f, cz = 0f;
        if (aabb != null) {
            cx = 0.5f * (aabb[0] + aabb[3]);
            cy = 0.5f * (aabb[1] + aabb[4]);
            cz = 0.5f * (aabb[2] + aabb[5]);
        }

        float lx = mat[0] * cx + mat[4] * cy + mat[8]  * cz + mat[12];
        float ly = mat[1] * cx + mat[5] * cy + mat[9]  * cz + mat[13];
        float lz = mat[2] * cx + mat[6] * cy + mat[10] * cz + mat[14];

        return new float[]{ lx, ly, lz };
    }

    private static float[] localChainMatrix(GltfModel model, NodeModel node) {
        if (model == null || node == null) return matIdentity();
        List<NodeModel> chain = new ArrayList<>();
        NodeModel cur = node;
        for (int i = 0; i < 32 && cur != null; i++) {
            chain.add(0, cur);
            cur = findParentNode(model, cur);
        }
        float[] out = matIdentity();
        for (NodeModel n : chain) {
            out = matMul(out, nodeLocalMatrix(n));
        }
        return out;
    }

    private static NodeModel findParentNode(GltfModel model, NodeModel child) {
        if (model == null || model.getNodeModels() == null || child == null) return null;
        for (NodeModel n : model.getNodeModels()) {
            List<NodeModel> kids = n.getChildren();
            if (kids != null && kids.contains(child)) return n;
        }
        return null;
    }

    private static float[] nodeLocalMatrix(NodeModel n) {
        float[] m = n.getMatrix();
        if (m != null && m.length >= 16) return m.clone();

        float[] t = n.getTranslation() != null ? n.getTranslation() : new float[]{0f, 0f, 0f};
        float[] q = n.getRotation() != null ? n.getRotation() : new float[]{0f, 0f, 0f, 1f};
        float[] s = n.getScale() != null ? n.getScale().clone() : new float[]{1f, 1f, 1f};
        if (Math.abs(s[0]) < 1e-8f && Math.abs(s[1]) < 1e-8f && Math.abs(s[2]) < 1e-8f) {
            s[0] = s[1] = s[2] = 1f;
        }
        return matFromTrs(t, q, s);
    }

    private static float[] matFromTrs(float[] t, float[] q, float[] s) {
        float x = q[0], y = q[1], z = q[2], w = q[3];
        float xx = x * x, yy = y * y, zz = z * z;
        float xy = x * y, xz = x * z, yz = y * z;
        float wx = w * x, wy = w * y, wz = w * z;
        float[] m = matIdentity();
        m[0] = (1f - 2f * (yy + zz)) * s[0];
        m[1] = (2f * (xy + wz)) * s[0];
        m[2] = (2f * (xz - wy)) * s[0];
        m[4] = (2f * (xy - wz)) * s[1];
        m[5] = (1f - 2f * (xx + zz)) * s[1];
        m[6] = (2f * (yz + wx)) * s[1];
        m[8] = (2f * (xz + wy)) * s[2];
        m[9] = (2f * (yz - wx)) * s[2];
        m[10] = (1f - 2f * (xx + yy)) * s[2];
        m[12] = t[0];
        m[13] = t[1];
        m[14] = t[2];
        return m;
    }

    private static float[] matMul(float[] a, float[] b) {
        float[] r = new float[16];
        for (int c = 0; c < 4; c++) {
            for (int row = 0; row < 4; row++) {
                r[c * 4 + row] =
                        a[0 * 4 + row] * b[c * 4 + 0]
                      + a[1 * 4 + row] * b[c * 4 + 1]
                      + a[2 * 4 + row] * b[c * 4 + 2]
                      + a[3 * 4 + row] * b[c * 4 + 3];
            }
        }
        return r;
    }

    private static float[] getWeaponNodeWorldPos(NodeModel node, GltfModel model) {
        if (node == null || model == null) return null;
        float[] g = WeaponTransformHelper.computeDeterministicGlobalTransform(node, model);
        if (g == null || g.length < 16) {
            g = WeaponTransformHelper.computeRenderedGlobalTransform(node, model);
        }
        float gx = g != null ? g[12] : 0f;
        float gy = g != null ? g[13] : 0f;
        float gz = g != null ? g[14] : 0f;

        float[] aabb = scanMeshAabb(node);
        if (aabb != null) {
            gx += 0.5f * (aabb[0] + aabb[3]);
            gy += 0.5f * (aabb[1] + aabb[4]);
            gz += 0.5f * (aabb[2] + aabb[5]);
        }

        return new float[]{ gx, gy, gz };
    }

    private static final java.util.IdentityHashMap<NodeModel, float[]> AABB_CACHE = new java.util.IdentityHashMap<>();
    private static final float[] AABB_EMPTY = new float[0];

    private static float[] scanMeshAabb(NodeModel node) {
        if (node == null || node.getMeshModels() == null) return null;
        float[] cached = AABB_CACHE.get(node);
        if (cached != null) return cached == AABB_EMPTY ? null : cached;

        float minx = Float.POSITIVE_INFINITY, miny = Float.POSITIVE_INFINITY, minz = Float.POSITIVE_INFINITY;
        float maxx = Float.NEGATIVE_INFINITY, maxy = Float.NEGATIVE_INFINITY, maxz = Float.NEGATIVE_INFINITY;
        boolean any = false;

        for (de.javagl.jgltf.model.MeshModel mesh : node.getMeshModels()) {
            if (mesh.getMeshPrimitiveModels() == null) continue;
            for (de.javagl.jgltf.model.MeshPrimitiveModel prim : mesh.getMeshPrimitiveModels()) {
                de.javagl.jgltf.model.AccessorModel pos = prim.getAttributes() != null
                        ? prim.getAttributes().get("POSITION") : null;
                if (pos == null) continue;

                Number[] minVals = pos.getMin();
                Number[] maxVals = pos.getMax();
                if (minVals != null && maxVals != null && minVals.length >= 3 && maxVals.length >= 3) {
                    minx = Math.min(minx, minVals[0].floatValue());
                    miny = Math.min(miny, minVals[1].floatValue());
                    minz = Math.min(minz, minVals[2].floatValue());
                    maxx = Math.max(maxx, maxVals[0].floatValue());
                    maxy = Math.max(maxy, maxVals[1].floatValue());
                    maxz = Math.max(maxz, maxVals[2].floatValue());
                    any = true;
                    continue;
                }

                de.javagl.jgltf.model.BufferViewModel view = pos.getBufferViewModel();
                if (view != null && view.getBufferViewData() != null) {
                    java.nio.ByteBuffer raw = view.getBufferViewData();
                    java.nio.ByteBuffer src = raw.slice().order(java.nio.ByteOrder.LITTLE_ENDIAN);
                    int byteOffset = pos.getByteOffset();
                    int stride = pos.getByteStride() > 0 ? pos.getByteStride() : 12;
                    int count = Math.min(pos.getCount(), 4096);
                    for (int i = 0; i < count; i++) {
                        int off = byteOffset + i * stride;
                        if (off + 12 > src.capacity()) break;
                        float x = src.getFloat(off);
                        float y = src.getFloat(off + 4);
                        float z = src.getFloat(off + 8);
                        if (x < minx) minx = x; if (y < miny) miny = y; if (z < minz) minz = z;
                        if (x > maxx) maxx = x; if (y > maxy) maxy = y; if (z > maxz) maxz = z;
                        any = true;
                    }
                }
            }
        }
        float[] res = any ? new float[]{ minx, miny, minz, maxx, maxy, maxz } : null;
        AABB_CACHE.put(node, res != null ? res : AABB_EMPTY);
        return res;
    }

    private static float[] matIdentity() {
        return new float[]{
            1f, 0f, 0f, 0f,
            0f, 1f, 0f, 0f,
            0f, 0f, 1f, 0f,
            0f, 0f, 0f, 1f
        };
    }

    private static float smoothstep(float t) {
        t = Math.max(0f, Math.min(1f, t));
        return t * t * (3.0f - 2.0f * t);
    }

    private static boolean isOpticAttachment(String slotKey, @Nullable com.fanisdeop.minezone.client.util.AttachmentData directAttData,
            @Nullable ItemWeaponAttachment attachItem, @Nullable WeaponData attData, @Nullable GltfModel attModel, @Nullable String cleanId) {
        String slot = slotKey != null ? slotKey.toLowerCase(Locale.ROOT) : "";
        String id = cleanId != null ? cleanId.toLowerCase(Locale.ROOT) : "";

        // 1. Строгое исключение креплений, планок, кронштейнов, переходников (они НЕ оптика!)
        if (id.contains("mount") || id.contains("rail") || id.contains("kron") || id.contains("bracket")
                || id.contains("adapter") || id.contains("planka") || id.contains("riser") || id.contains("dovetail")
                || id.contains("lastochka") || id.contains("skrep") || id.contains("kreplen") || id.contains("krep")) {
            return false;
        }

        // 2. Строгое исключение любых не-прицельных аксессуаров и типов
        if (directAttData != null) {
            String at = directAttData.attachmentType != null ? directAttData.attachmentType.toLowerCase(Locale.ROOT) : "";
            if (at.contains("charm") || at.contains("barrel") || at.contains("silencer") || at.contains("mag")
                    || at.contains("grip") || at.contains("stock") || at.contains("forend") || at.contains("tactical") || at.contains("other")) {
                if (!directAttData.isOpticalScope && !directAttData.isCollimator && directAttData.scopeZoom <= 1.05f) {
                    return false;
                }
            }
        }

        if (slot.contains("charm") || slot.contains("brelok") || slot.contains("брелок") || slot.contains("trinket") || slot.contains("keychain")
                || slot.contains("acc") || slot.contains("grip") || slot.contains("рукоят") || slot.contains("ручка")
                || slot.contains("underbarrel") || slot.contains("подствол")
                || slot.contains("muzzle") || slot.contains("silencer") || slot.contains("глушител") || slot.contains("barrel") || slot.contains("ствол") || slot.contains("dtk")
                || slot.contains("mag") || slot.contains("магазин") || slot.contains("patron") || slot.contains("laser") || slot.contains("tactical_side")
                || slot.contains("tactical_bottom") || slot.contains("tactical_left") || slot.contains("tactical_right") || slot.contains("flashlight") || slot.contains("фонар") || slot.contains("bipod") || slot.contains("сошки")
                || slot.contains("stock") || slot.contains("butt") || slot.contains("приклад")
                || slot.contains("forend") || slot.contains("handguard") || slot.contains("цевье")) {
            return false;
        }

        if (id.startsWith("acc_") || id.startsWith("acc1_") || id.contains("charm") || id.contains("brelok") || id.contains("trinket") || id.contains("keychain")
                || id.contains("silencer") || id.contains("suppressor") || id.contains("compensator")
                || id.contains("flash_hider") || id.contains("dtk") || id.contains("grip") || id.contains("laser")
                || id.contains("flashlight") || id.contains("bipod") || id.contains("butt") || id.contains("stock")
                || id.contains("forend") || id.contains("mag") || id.contains("stanag")) {
            return false;
        }

        // 3. Проверка прямых признаков оптики (коллиматор или оптический прицел)
        if (directAttData != null) {
            if (directAttData.isOpticalScope || directAttData.isCollimator) return true;
            if (directAttData.scopeZoom > 1.05f) return true;
        }
        if (attachItem != null) {
            if (attachItem.isOpticalScope() || attachItem.isCollimator() || attachItem.getScopeZoom() > 1.05f) return true;
        }
        if (attData != null) {
            if (attData.hasBuiltInOptic || attData.scopeZoom > 1.05f) return true;
        }

        // 4. Проверка известных ID прицелов
        if (id.contains("barska") || id.contains("eotech") || id.contains("eotec") || id.contains("kobra")
                || id.contains("hamr") || id.contains("pso") || id.contains("acog") || id.contains("elcan")
                || id.contains("specter") || id.contains("leupold") || id.contains("po4") || id.contains("vortex")
                || id.contains("pkas") || id.contains("rakurs") || id.contains("okp") || id.contains("rmr")
                || id.contains("microt1") || id.contains("valday") || id.contains("susat") || id.contains("pulsar")
                || id.contains("mosinscop") || id.contains("rt20_scope") || id.contains("svt_pricel")
                || id.contains("scope_tr20") || id.contains("xm8_scope") || id.contains("1p29") || id.contains("1p63") || id.contains("1p77")
                || id.contains("collimator") || id.contains("aimpoint") || id.contains("pk06") || id.contains("deltapoint")) {
            return true;
        }

        if (attModel != null && findOpticAnchor(attModel) != null) {
            if (directAttData != null && "optic".equalsIgnoreCase(directAttData.attachmentType)) {
                return true;
            }
            if (attachItem != null && "optic".equalsIgnoreCase(attachItem.getAttachmentType())) {
                return true;
            }
        }
        return false;
    }

    public OpticAdsParameters resolveOpticParameters(ItemStack stack, WeaponData liveData, @Nullable GltfModel weaponModel) {
        OpticAdsParameters params = new OpticAdsParameters();
        float baseFov = (liveData != null && liveData.weaponFov > 0f)
                ? liveData.weaponFov : (liveData != null && liveData.fpFov > 0f ? liveData.fpFov : 70.0f);
        float defaultAdsFov = (liveData != null && liveData.adsFov > 0f) ? liveData.adsFov : baseFov;

        float defaultScale = 1.0f;
        if (liveData != null) {
            if (!Float.isNaN(liveData.adsModelScale) && liveData.adsModelScale > 0.01f) {
                defaultScale = liveData.adsModelScale;
            } else if (!Float.isNaN(liveData.fpScale) && liveData.fpScale > 0.01f) {
                defaultScale = liveData.fpScale;
            }
        }

        float defaultRotY = (liveData != null && !Float.isNaN(liveData.adsModelRotY)) ? liveData.adsModelRotY : ((liveData != null && !Float.isNaN(liveData.fpRotY)) ? liveData.fpRotY : 0f);
        float defaultZ = (liveData != null && !Float.isNaN(liveData.adsModelZ)) ? liveData.adsModelZ : ((liveData != null && !Float.isNaN(liveData.adsZ)) ? liveData.adsZ : -2.5f);

        NBTTagCompound at = WeaponAttachmentRenderer.getAttachmentsTag(stack);
        WeaponLayoutLoader.WeaponLayout layout = WeaponLayoutLoader.getLayout(this.weaponId);

        OpticAdsParameters bestParams = null;
        int bestScore = -1;

        if (at != null && !at.hasNoTags() && weaponModel != null) {
            for (String slotKey : at.getKeySet()) {
                String rawId = WeaponAttachmentRenderer.extractRawId(at.getTag(slotKey));
                if (rawId == null || rawId.trim().isEmpty()) continue;

                String cleanId = rawId.trim();
                if (cleanId.contains(":")) cleanId = new ResourceLocation(cleanId).getResourcePath();

                WeaponModelRenderer attR = WeaponAttachmentRenderer.resolveRenderer(rawId, cleanId);
                net.minecraft.item.Item item = net.minecraft.item.Item.REGISTRY.getObject(new ResourceLocation(rawId));
                if (item == null) item = net.minecraft.item.Item.REGISTRY.getObject(new ResourceLocation("minezone", cleanId));
                ItemWeaponAttachment attachItem = (item instanceof ItemWeaponAttachment) ? (ItemWeaponAttachment) item : null;

                WeaponData attData = attR != null ? attR.getWeaponData() : null;
                GltfModel attModel = attR != null ? attR.getGltfModel() : null;

                com.fanisdeop.minezone.client.util.AttachmentData directAttData = 
                        com.fanisdeop.minezone.features.weapon.common.config.AttachmentConfigLoader.getData(cleanId);

                if (!isOpticAttachment(slotKey, directAttData, attachItem, attData, attModel, cleanId)) continue;

                OpticAdsParameters curr = new OpticAdsParameters();
                NodeModel lensNode = (attModel != null) ? findOpticAnchor(attModel) : null;

                curr.hasExternalOptic = true;
                curr.opticModel = attModel;
                curr.opticAnchorNode = lensNode;

                float zoom = 1.0f;
                if (directAttData != null && directAttData.scopeZoom > 0f) zoom = directAttData.scopeZoom;
                else if (attData != null && attData.scopeZoom > 0f) zoom = attData.scopeZoom;
                else if (attachItem != null && attachItem.getScopeZoom() > 0f) zoom = attachItem.getScopeZoom();
                curr.scopeZoom = zoom;

                float scale = Float.NaN;
                if (directAttData != null && !Float.isNaN(directAttData.adsModelScale) && directAttData.adsModelScale > 0.01f) scale = directAttData.adsModelScale;
                else if (attData != null && !Float.isNaN(attData.adsModelScale) && attData.adsModelScale > 0.01f) scale = attData.adsModelScale;
                else if (attachItem != null && !Float.isNaN(attachItem.getAdsModelScale()) && attachItem.getAdsModelScale() > 0.01f) scale = attachItem.getAdsModelScale();
                else if (directAttData != null && directAttData.scopeScale != null && directAttData.scopeScale > 0.01f) scale = directAttData.scopeScale;
                curr.scale = (!Float.isNaN(scale) && scale > 0.01f) ? scale : defaultScale;

                if (directAttData != null && !Float.isNaN(directAttData.adsModelRotX)) curr.rotX = directAttData.adsModelRotX;
                else if (attData != null && !Float.isNaN(attData.adsModelRotX)) curr.rotX = attData.adsModelRotX;
                else if (attachItem != null && !Float.isNaN(attachItem.getAdsModelRotX())) curr.rotX = attachItem.getAdsModelRotX();
                else curr.rotX = (liveData != null && !Float.isNaN(liveData.adsModelRotX)) ? liveData.adsModelRotX : 0f;

                if (directAttData != null && !Float.isNaN(directAttData.adsModelRotY)) curr.rotY = directAttData.adsModelRotY;
                else if (attData != null && !Float.isNaN(attData.adsModelRotY)) curr.rotY = attData.adsModelRotY;
                else if (attachItem != null && !Float.isNaN(attachItem.getAdsModelRotY())) curr.rotY = attachItem.getAdsModelRotY();
                else curr.rotY = defaultRotY;

                if (directAttData != null && !Float.isNaN(directAttData.adsModelRotZ)) curr.rotZ = directAttData.adsModelRotZ;
                else if (attData != null && !Float.isNaN(attData.adsModelRotZ)) curr.rotZ = attData.adsModelRotZ;
                else if (attachItem != null && !Float.isNaN(attachItem.getAdsModelRotZ())) curr.rotZ = attachItem.getAdsModelRotZ();
                else curr.rotZ = (liveData != null && !Float.isNaN(liveData.adsModelRotZ)) ? liveData.adsModelRotZ : 0f;

                if (directAttData != null && !Float.isNaN(directAttData.adsModelX)) curr.customX = directAttData.adsModelX;
                else if (attData != null && !Float.isNaN(attData.adsModelX)) curr.customX = attData.adsModelX;
                else if (attachItem != null && !Float.isNaN(attachItem.getAdsModelX())) curr.customX = attachItem.getAdsModelX();

                if (directAttData != null && !Float.isNaN(directAttData.adsModelY)) curr.customY = directAttData.adsModelY;
                else if (attData != null && !Float.isNaN(attData.adsModelY)) curr.customY = attData.adsModelY;
                else if (attachItem != null && !Float.isNaN(attachItem.getAdsModelY())) curr.customY = attachItem.getAdsModelY();

                if (directAttData != null && !Float.isNaN(directAttData.adsModelZ)) curr.customZ = directAttData.adsModelZ;
                else if (attData != null && !Float.isNaN(attData.adsModelZ)) curr.customZ = attData.adsModelZ;
                else if (attachItem != null && !Float.isNaN(attachItem.getAdsModelZ())) curr.customZ = attachItem.getAdsModelZ();

                if (!Float.isNaN(curr.customZ) && curr.customZ != 0f) {
                    curr.targetEyeReliefZ = curr.customZ;
                } else if (directAttData != null && directAttData.eyeRelief != null && directAttData.eyeRelief != 0f) {
                    curr.targetEyeReliefZ = directAttData.eyeRelief;
                } else if (attData != null && attData.eyeRelief != 0f) {
                    curr.targetEyeReliefZ = attData.eyeRelief;
                } else {
                    curr.targetEyeReliefZ = defaultZ;
                }

                float attAdsFov = Float.NaN;
                if (directAttData != null && !Float.isNaN(directAttData.adsFov) && directAttData.adsFov > 4.0f) {
                    attAdsFov = directAttData.adsFov;
                } else if (attachItem != null && !Float.isNaN(attachItem.getAdsFov()) && attachItem.getAdsFov() > 4.0f) {
                    attAdsFov = attachItem.getAdsFov();
                } else if (attData != null && !Float.isNaN(attData.adsFov) && attData.adsFov > 4.0f) {
                    attAdsFov = attData.adsFov;
                }

                curr.viewmodelFov = computeViewmodelAdsFov(curr.scopeZoom, attAdsFov, defaultAdsFov, baseFov);

                NodeModel socketNode = null;
                boolean computedPos = false;
                if (layout != null && layout.slots != null) {
                    WeaponLayoutLoader.ModuleSlot layoutSlot = layout.slots.get(slotKey);
                    String socketName = WeaponAttachmentRenderer.resolveSocketName(layoutSlot, slotKey);
                    if (socketName != null) socketNode = WeaponAttachmentRenderer.findGltfNode(weaponModel, socketName);

                    // If socket is on a parent slot attachment (e.g. tactical_top on forend):
                    if (socketNode == null && layoutSlot != null && layoutSlot.parentSlot != null) {
                        String parentRawId = WeaponAttachmentRenderer.extractRawId(at.getTag(layoutSlot.parentSlot));
                        if (parentRawId != null) {
                            String parentCleanId = parentRawId.trim();
                            if (parentCleanId.contains(":")) parentCleanId = new ResourceLocation(parentCleanId).getResourcePath();
                            WeaponModelRenderer parentR = WeaponAttachmentRenderer.resolveRenderer(parentRawId, parentCleanId);
                            if (parentR != null && parentR.getGltfModel() != null) {
                                NodeModel parentSocketNode = WeaponAttachmentRenderer.findGltfNode(parentR.getGltfModel(), socketName);
                                if (parentSocketNode != null) {
                                    WeaponLayoutLoader.ModuleSlot parentLayoutSlot = layout.slots.get(layoutSlot.parentSlot);
                                    String pSocketName = WeaponAttachmentRenderer.resolveSocketName(parentLayoutSlot, layoutSlot.parentSlot);
                                    NodeModel pNode = WeaponAttachmentRenderer.findGltfNode(weaponModel, pSocketName);
                                    if (pNode != null) {
                                        float[] pw = WeaponTransformHelper.computeRenderedGlobalTransform(pNode, weaponModel);
                                        float[] sw = WeaponTransformHelper.computeRenderedGlobalTransform(parentSocketNode, parentR.getGltfModel());
                                        if (pw != null && sw != null) {
                                            curr.socketWorldPos[0] = pw[12] + sw[12];
                                            curr.socketWorldPos[1] = pw[13] + sw[13];
                                            curr.socketWorldPos[2] = pw[14] + sw[14];
                                            computedPos = true;
                                            socketNode = parentSocketNode;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                if (socketNode == null) {
                    String[] CANDIDATES = {
                        "socket_optic", "socket_scope", "socket_sight", "socket_top", "socket_rail",
                        "mount_optic", "mount_scope", "mount_sight", "rail_optic", "rail_scope", "mount_rail",
                        "optic_socket", "scope_socket", "sight_socket", "optic_mount", "scope_mount",
                        "optic", "scope", "sight", "top_rail", "rail_top", "picatinny", "bone_optic", "bone_scope"
                    };
                    for (String cName : CANDIDATES) {
                        socketNode = WeaponAttachmentRenderer.findGltfNode(weaponModel, cName);
                        if (socketNode != null) break;
                    }
                }

                WeaponLayoutLoader.ModuleSlot layoutSlot = (layout != null && layout.slots != null) ? layout.slots.get(slotKey) : null;
                WeaponLayoutLoader.ModuleSlot.AttachmentOffset customOff =
                        (layoutSlot != null) ? layoutSlot.getAttachmentOffset(cleanId) : null;

                if (customOff != null && customOff.hasAds()) {
                    if (!Float.isNaN(customOff.getAdsX())) curr.customX = customOff.getAdsX();
                    if (!Float.isNaN(customOff.getAdsY())) curr.customY = customOff.getAdsY();
                    if (!Float.isNaN(customOff.getAdsZ()) && Math.abs(customOff.getAdsZ()) > 0.0001f) {
                        curr.customZ = customOff.getAdsZ();
                        curr.targetEyeReliefZ = customOff.getAdsZ();
                    }
                    if (!Float.isNaN(customOff.adsScale) && customOff.adsScale > 0.01f) curr.scale = customOff.adsScale;
                    if (!Float.isNaN(customOff.adsFov) && customOff.adsFov > 4.0f) {
                        curr.viewmodelFov = computeViewmodelAdsFov(curr.scopeZoom, customOff.adsFov, defaultAdsFov, baseFov);
                    }
                } else if (layoutSlot != null) {
                    if (Float.isNaN(curr.customX) && !Float.isNaN(layoutSlot.getAdsX())) curr.customX = layoutSlot.getAdsX();
                    if (Float.isNaN(curr.customY) && !Float.isNaN(layoutSlot.getAdsY())) curr.customY = layoutSlot.getAdsY();
                    if (Float.isNaN(curr.customZ) && !Float.isNaN(layoutSlot.getAdsZ())) {
                        curr.customZ = layoutSlot.getAdsZ();
                        curr.targetEyeReliefZ = layoutSlot.getAdsZ();
                    }
                }

                if (socketNode != null && !computedPos) {
                    float[] s = WeaponTransformHelper.computeRenderedGlobalTransform(socketNode, weaponModel);
                    if (s != null) {
                        curr.socketWorldPos[0] = s[12];
                        curr.socketWorldPos[1] = s[13];
                        curr.socketWorldPos[2] = s[14];
                    }
                }

                if (layoutSlot != null) {
                    float offX = customOff != null ? customOff.getOffsetX() : layoutSlot.getOffsetX();
                    float offY = customOff != null ? customOff.getOffsetY() : layoutSlot.getOffsetY();
                    float offZ = customOff != null ? customOff.getOffsetZ() : layoutSlot.getOffsetZ();
                    curr.socketWorldPos[0] += offX;
                    curr.socketWorldPos[1] += offY;
                    curr.socketWorldPos[2] += offZ;
                }

                String lowerId = cleanId.toLowerCase(Locale.ROOT);
                boolean isBracketOrRail = lowerId.contains("kron") || lowerId.contains("mount") || lowerId.contains("rail")
                        || lowerId.contains("bracket") || lowerId.contains("adapter") || lowerId.contains("planka") || lowerId.contains("riser")
                        || lowerId.contains("skrep") || lowerId.contains("dovetail");
                if (isBracketOrRail) continue;

                int score = 0;
                if (directAttData != null) {
                    if (directAttData.isOpticalScope || directAttData.isCollimator) score += 60;
                    if (!Float.isNaN(directAttData.adsModelX) || !Float.isNaN(directAttData.adsModelY) || !Float.isNaN(directAttData.adsModelZ)) score += 50;
                    if (directAttData.scopeZoom > 1.05f) score += 20;
                }
                if (attachItem != null) {
                    if (attachItem.isOpticalScope() || attachItem.isCollimator()) score += 60;
                    if (!Float.isNaN(attachItem.getAdsModelX()) || !Float.isNaN(attachItem.getAdsModelY()) || !Float.isNaN(attachItem.getAdsModelZ())) score += 50;
                    if (attachItem.getScopeZoom() > 1.05f) score += 20;
                }
                if (lensNode != null) {
                    score += 30;
                }

                if (score >= 20 && score > bestScore) {
                    bestScore = score;
                    bestParams = curr;
                }
            }

            if (bestParams != null) {
                return bestParams;
            }
        }

        params.hasExternalOptic = false;
        if (liveData != null) {
            params.scale = defaultScale;
            params.rotX = !Float.isNaN(liveData.adsModelRotX) ? liveData.adsModelRotX : 0f;
            params.rotY = defaultRotY;
            params.rotZ = !Float.isNaN(liveData.adsModelRotZ) ? liveData.adsModelRotZ : 0f;

            params.customX = !Float.isNaN(liveData.adsModelX) ? liveData.adsModelX : Float.NaN;
            params.customY = !Float.isNaN(liveData.adsModelY) ? liveData.adsModelY : Float.NaN;
            params.customZ = !Float.isNaN(liveData.adsModelZ) ? liveData.adsModelZ : Float.NaN;
            params.scopeZoom = (liveData.hasBuiltInOptic && liveData.scopeZoom > 0f) ? liveData.scopeZoom : 1.0f;

            if (!Float.isNaN(params.customZ) && params.customZ != 0f) {
                params.targetEyeReliefZ = params.customZ;
            } else if (liveData.adsZ != 0f && !Float.isNaN(liveData.adsZ)) {
                params.targetEyeReliefZ = liveData.adsZ;
            } else {
                params.targetEyeReliefZ = defaultZ;
            }

            params.viewmodelFov = computeViewmodelAdsFov(params.scopeZoom, liveData.adsFov, defaultAdsFov, baseFov);

            if (weaponModel != null) {
                params.opticAnchorNode = findOpticAnchor(weaponModel);
                if (params.opticAnchorNode != null) {
                    params.opticModel = weaponModel;
                }
            }
        }
        return params;
    }

    public static final class IronSightTarget {
        public String source = "";
        public float worldX = 0f;
        public float worldY = 0f;
        public float worldZ = 0f;
    }

    public static IronSightTarget resolveIronSight(GltfModel weaponModel, @Nullable WeaponData data, String weaponId) {
        IronSightTarget target = new IronSightTarget();
        if (weaponModel == null || weaponModel.getNodeModels() == null) {
            target.source = "fallback";
            target.worldY = 0.015f;
            return target;
        }

        // 1. Проверяем прямые кандидаты прицела (sight, default_sight, aim, mushka, celik)
        NodeModel candidateNode = null;
        for (String c : SIGHT_NODE_CANDIDATES) {
            for (NodeModel node : weaponModel.getNodeModels()) {
                if (node.getName() == null) continue;
                String n = node.getName().toLowerCase(Locale.ROOT);
                if (n.equals(c) || n.startsWith(c + ".") || n.startsWith(c + "_")) {
                    candidateNode = node;
                    break;
                }
            }
            if (candidateNode != null) break;
        }

        if (candidateNode != null) {
            float[] sPos = getWeaponNodeWorldPos(candidateNode, weaponModel);
            if (sPos != null) {
                // Если кость находится по центру оружия (|X| < 5мм) и на высоте Y > 2мм:
                // Это честный верхний прицел / планка (Western rifles, пистолеты PM, MP5, G36, M4 и т.д.)
                if (Math.abs(sPos[0]) < 0.005f && sPos[1] > 0.002f) {
                    target.source = candidateNode.getName();
                    target.worldX = 0f; // строго по центру перекрестия
                    target.worldY = sPos[1];
                    target.worldZ = sPos[2];
                    return target;
                }
                // Если |X| >= 0.005f (как на АК, СВД, ВСС где кость "sight" на ласточкином хвосте сбоку):
                // Пропускаем эту боковую кость для механического прицела!
            }
        }

        // 2. Для автоматов/винтовок и пистолетов: ориентируемся по затвору/затворной раме (zatvor / slide)
        NodeModel zatvorNode = null;
        for (NodeModel node : weaponModel.getNodeModels()) {
            if (node.getName() == null) continue;
            String n = node.getName().toLowerCase(Locale.ROOT);
            if (n.equals("zatvor") || n.equals("slide") || n.equals("bolt")) {
                zatvorNode = node;
                break;
            }
        }

        boolean isPistol = (data != null && "pistol".equalsIgnoreCase(data.type)) || WeaponAttachmentRenderer.isPistol(weaponId, data);

        if (zatvorNode != null) {
            float[] zPos = getWeaponNodeWorldPos(zatvorNode, weaponModel);
            if (zPos != null) {
                target.source = zatvorNode.getName() + (isPistol ? "_pistol" : "_rifle");
                target.worldX = 0f; // строго по центру перекрестия
                // Для пистолетов целик выступает над затвором на +2.5мм
                // Для автоматов и винтовок (АК, СВД, ВСС и др.) целик сидит на колодке над рамой на +15мм
                target.worldY = zPos[1] + (isPistol ? 0.0025f : 0.0150f);
                target.worldZ = zPos[2];
                return target;
            }
        }

        // 3. Для дробовиков / ружей без подвижного затвора: ствол (barrel / stvol / mount1)
        for (NodeModel node : weaponModel.getNodeModels()) {
            if (node.getName() == null) continue;
            String n = node.getName().toLowerCase(Locale.ROOT);
            if (n.equals("barrel") || n.equals("stvol") || n.equals("mount1")) {
                float[] bPos = getWeaponNodeWorldPos(node, weaponModel);
                if (bPos != null) {
                    target.source = node.getName() + "_sight";
                    target.worldX = 0f;
                    target.worldY = bPos[1] + 0.0100f;
                    target.worldZ = bPos[2];
                    return target;
                }
            }
        }

        target.source = "default_fallback";
        target.worldX = 0f;
        target.worldY = 0.015f;
        target.worldZ = 0f;
        return target;
    }

    @Nullable
    public static NodeModel findSightNode(GltfModel weaponModel, @Nullable WeaponData data) {
        if (weaponModel == null || weaponModel.getNodeModels() == null) return null;
        for (String candidate : SIGHT_NODE_CANDIDATES) {
            for (NodeModel node : weaponModel.getNodeModels()) {
                if (node.getName() == null) continue;
                String n = node.getName().toLowerCase(Locale.ROOT);
                if (n.equals(candidate) || n.startsWith(candidate + ".") || n.startsWith(candidate + "_")) {
                    return node;
                }
            }
        }
        return null;
    }

    private AdsPose computeAdsPose(WeaponData liveData, ItemStack stack, ModelState state) {
        AdsPose pose = new AdsPose();
        GltfModel weaponModel = (state != null) ? state.gltfModel : null;
        OpticAdsParameters params = resolveOpticParameters(stack, liveData, weaponModel);

        pose.fov = params.viewmodelFov;
        pose.targetScale = params.scale;
        pose.quat = WeaponTransformHelper.quatFromEulerDeg(params.rotX, params.rotY, params.rotZ);

        if (!params.hasExternalOptic) {
            boolean useAutoSight = (liveData == null || (liveData.autoSight && !liveData.manualAds));
            IronSightTarget ironSight = (useAutoSight && weaponModel != null)
                    ? resolveIronSight(weaponModel, liveData, this.weaponId)
                    : null;

            if (ironSight != null) {
                float scale = params.scale;
                // Вращаем положение точки визирования по кватерниону ориентации оружия (rotX, rotY, rotZ),
                // чтобы оружие при наклоне ствола (pitch/yaw/roll) вращалось строго вокруг точки прицела
                float[] rotSight = WeaponTransformHelper.transformVectorByQuat(
                        pose.quat, ironSight.worldX, ironSight.worldY, ironSight.worldZ);

                // Строгое выравнивание по перекрестию (центр экрана X=0, Y=0)
                pose.tx = -scale * rotSight[0];
                pose.ty = -scale * rotSight[1];

                // Реалистичное приближение к камере (Z):
                if (!Float.isNaN(params.customZ) && Math.abs(params.customZ - (-8.5f)) > 0.01f && params.customZ != 0f) {
                    pose.tz = params.customZ;
                } else {
                    boolean isPistol = (liveData != null && "pistol".equalsIgnoreCase(liveData.type)) || WeaponAttachmentRenderer.isPistol(this.weaponId, liveData);
                    pose.tz = isPistol ? -5.0f : -6.2f;
                }

                DEBUG_ADS_INFO.valid = true;
                DEBUG_ADS_INFO.weaponId = this.weaponId;
                DEBUG_ADS_INFO.sightNodeName = ironSight.source;
                DEBUG_ADS_INFO.sightWorldX = ironSight.worldX;
                DEBUG_ADS_INFO.sightWorldY = ironSight.worldY;
                DEBUG_ADS_INFO.sightWorldZ = ironSight.worldZ;
                DEBUG_ADS_INFO.isAuto = true;
            } else {
                // Ручные пользовательские координаты (adsModelX, adsModelY, adsModelZ)
                pose.tx = !Float.isNaN(params.customX) ? params.customX : 0.0f;
                pose.ty = !Float.isNaN(params.customY) ? params.customY : 0.0f;
                pose.tz = (!Float.isNaN(params.customZ) && params.customZ != 0f) ? params.customZ : params.targetEyeReliefZ;
                if (pose.tz == 0f) pose.tz = -6.2f;

                DEBUG_ADS_INFO.valid = true;
                DEBUG_ADS_INFO.weaponId = this.weaponId;
                DEBUG_ADS_INFO.sightNodeName = "manual";
                DEBUG_ADS_INFO.isAuto = false;
            }
        } else {
            // Для навесной оптики (прицелы EOTech, ПСО-1, коллиматоры и т.д.)
            float wx = params.socketWorldPos[0];
            float wy = params.socketWorldPos[1];
            float wz = params.socketWorldPos[2];

            if (params.opticModel != null && params.opticAnchorNode != null) {
                float[] lensOffset = computeOpticCenterInAttachment(params.opticModel, params.opticAnchorNode);
                wx += lensOffset[0];
                wy += lensOffset[1];
                wz += lensOffset[2];
            }

            // Добавляем индивидуальную калибровку прицела (ads_model_x, ads_model_y)
            if (!Float.isNaN(params.customX)) {
                wx += params.customX;
            }
            if (!Float.isNaN(params.customY)) {
                wy += params.customY;
            }

            float theta = (float) Math.toRadians(params.rotY);
            float cosT = (float) Math.cos(theta);
            float sinT = (float) Math.sin(theta);
            pose.tx = -params.scale * (wx * cosT + wz * sinT);
            pose.ty = -params.scale * wy;
            pose.tz = (!Float.isNaN(params.customZ) && params.customZ != 0f)
                    ? params.customZ
                    : params.targetEyeReliefZ;
            if (pose.tz == 0f) pose.tz = -6.2f;
        }

        // Универсальная микро-калибровка через adsOffsetX / adsOffsetY / adsOffsetZ
        if (liveData != null) {
            if (!Float.isNaN(liveData.adsOffsetX)) pose.tx += liveData.adsOffsetX;
            if (!Float.isNaN(liveData.adsOffsetY)) pose.ty += liveData.adsOffsetY;
            if (!Float.isNaN(liveData.adsOffsetZ)) pose.tz += liveData.adsOffsetZ;
        }

        DEBUG_ADS_INFO.adsTx = pose.tx;
        DEBUG_ADS_INFO.adsTy = pose.ty;
        DEBUG_ADS_INFO.adsTz = pose.tz;

        return pose;
    }

    private void applyTransformations(WeaponData liveData, Minecraft mc, AdsPose adsPose) {
        float smoothT = smoothstep(adsProgress);

        float[] hipQuat = WeaponTransformHelper.quatFromEulerDeg(0f, liveData.fpRotY, 0f);
        float[] finalQuat = WeaponTransformHelper.slerp(hipQuat, adsPose.quat, smoothT);

        float modelScale = lerp(liveData.fpScale, adsPose.targetScale, smoothT);

        float curX = lerp(liveData.fpX, adsPose.tx, smoothT);
        float curY = lerp(liveData.fpY, adsPose.ty, smoothT);
        float curZ = lerp(liveData.fpZ, adsPose.tz, smoothT);

        GL11.glTranslatef(curX, curY, curZ);
        WeaponTransformHelper.multMatrixColumnMajor(WeaponTransformHelper.quaternionToMatrix(finalQuat));
        applyRecoilKick(mc, smoothT);
        GL11.glScalef(modelScale, modelScale, modelScale);
    }

    // =========================================================================
    // БЛОК: ЭКСТРАКЦИЯ И КИНЕМАТИКА ГИЛЬЗ
    // =========================================================================
    @Nullable
    public static NodeModel findEjectionNode(GltfModel model) {
        return findEjectionNode(model, null);
    }

    @Nullable
    public static NodeModel findEjectionNode(GltfModel model, @Nullable WeaponData data) {
        if (model == null || model.getNodeModels() == null) return null;

        // 1. Прямое указание ноды из JSON-конфига оружия
        if (data != null && data.casingNode != null && !data.casingNode.trim().isEmpty()) {
            NodeModel customNode = GltfNodeHelper.findNodeIgnoreCase(model, data.casingNode.trim());
            if (customNode != null) return customNode;
        }

        // 2. Стандартный список кандидатов (sleeve_spawn, sleeve, ejection_port и т.д.)
        for (String candidate : EJECTION_NODE_CANDIDATES) {
            for (NodeModel node : model.getNodeModels()) {
                if (node.getName() == null) continue;
                String n = node.getName().toLowerCase(Locale.ROOT);
                if (n.equals(candidate) || n.startsWith(candidate + ".") || n.startsWith(candidate + "_")) {
                    return node;
                }
            }
        }

        // 3. Поиск ноды по калибру оружия
        if (data != null && data.caliber != null && !data.caliber.trim().isEmpty()) {
            String calKey = ShellCasingManager.normalizeKey(data.caliber);
            for (NodeModel node : model.getNodeModels()) {
                if (node.getName() == null) continue;
                String n = node.getName().toLowerCase(Locale.ROOT);
                String cleanN = ShellCasingManager.normalizeKey(n);
                if (cleanN.equals(calKey) || n.contains(calKey) || n.contains(calKey.replace("x", "_"))) {
                    return node;
                }
            }
        }

        // 4. Эвристический поиск нод, содержащих ключевые слова
        for (NodeModel node : model.getNodeModels()) {
            if (node.getName() == null) continue;
            String n = node.getName().toLowerCase(Locale.ROOT);
            if (n.contains("sleeve") || n.contains("eject") || n.contains("casing") || n.contains("gilza")
                    || n.contains("shell_") || n.contains("cartridge") || n.contains("extractor")) {
                return node;
            }
        }

        // 5. Поиск нод формата калибра (например "9_39", "545_39", "762_39", "762_54", "556_45", "9_18", "9_19", "12_70")
        for (NodeModel node : model.getNodeModels()) {
            if (node.getName() == null) continue;
            String n = node.getName().toLowerCase(Locale.ROOT);
            if (n.matches(".*\\b\\d+[_x]\\d+.*")) {
                return node;
            }
        }

        return null;
    }

 // -------------------------------------------------------------------------
    // ПАТЧ В computeEjectionTransform В WeaponModelRenderer.java
    // -------------------------------------------------------------------------
    @Nullable
    public EjectionTransformResult computeEjectionTransform(ItemStack stack, net.minecraft.entity.player.EntityPlayer player) {
        if (player == null || player.world == null) return null;

        WeaponData liveData = resolveLiveWeaponData();
        if (liveData == null) return null;

        if (!this.ejectCacheValid) return null;

        double spawnWorldX = this.ejectWorldX;
        double spawnWorldY = this.ejectWorldY;
        double spawnWorldZ = this.ejectWorldZ;

        if (DEBUG_EJECTION_INFO.valid) {
            System.out.println("[MZ-CASING-INFO] Выброс гильзы (" + this.weaponId + "): кость='" 
                    + DEBUG_EJECTION_INFO.socketName + "', pos=(" + String.format(Locale.ROOT, "%.3f, %.3f, %.3f", spawnWorldX, spawnWorldY, spawnWorldZ) + ")");
        }

        float[] baseVel = (liveData.casingVelocity != null && liveData.casingVelocity.length >= 3)
                ? liveData.casingVelocity : new float[]{ 3.4f, 0.20f, -0.35f };

        java.util.Random rand = player.world.rand;
        float spread = Math.max(0.0f, liveData.casingSpread);
        float sideSpeed = (baseVel[0] * this.ejectLocalSideSign) + (rand.nextFloat() - 0.5f) * spread * 2.0f;
        float rawUp = (baseVel[1] < 0.8f) ? (baseVel[1] + 1.5f) : baseVel[1];
        float upSpeed   = rawUp + (rand.nextFloat() - 0.5f) * spread * 1.0f;
        float fwdSpeed  = baseVel[2] + (rand.nextFloat() - 0.5f) * spread * 1.0f;

        double velX = this.ejectRgtX * sideSpeed + this.ejectUpX * upSpeed + this.ejectFwdX * fwdSpeed + player.motionX * 0.4;
        double velY = this.ejectRgtY * sideSpeed + this.ejectUpY * upSpeed + this.ejectFwdY * fwdSpeed + player.motionY * 0.4;
        double velZ = this.ejectRgtZ * sideSpeed + this.ejectUpZ * upSpeed + this.ejectFwdZ * fwdSpeed + player.motionZ * 0.4;

        double axisX = rand.nextDouble() - 0.5;
        double axisY = rand.nextDouble() * 0.4 + 0.6;
        double axisZ = rand.nextDouble() - 0.5;
        double alen = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
        if (alen > 0.0001) { axisX /= alen; axisY /= alen; axisZ /= alen; }
        else { axisX = 0; axisY = 1; axisZ = 0; }

        float angularSpeed = liveData.casingAngularSpeed + (rand.nextFloat() - 0.5f) * 10.0f;

        net.minecraft.util.math.BlockPos bPos = new net.minecraft.util.math.BlockPos(spawnWorldX, spawnWorldY, spawnWorldZ);
        int light = player.world.getCombinedLight(bPos, 0);
        float lightX = (float) (light & 0xFFFF);
        float lightY = (float) ((light >> 16) & 0xFFFF);

        float casingScale = liveData.casingScale > 0.001f ? liveData.casingScale : 1.0f;

        return new EjectionTransformResult(
                spawnWorldX, spawnWorldY, spawnWorldZ,
                velX, velY, velZ,
                axisX, axisY, axisZ,
                angularSpeed, casingScale,
                lightX, lightY
        );
    }

    public static void renderOpticalScopePasses(WeaponModelRenderer target, GltfModel model,
            String shaderId, WeaponTextureResolver resolver,
            WeaponData data, ItemStack shaderStack,
            boolean isGUI, boolean aimingAds, String reticlePath,
            float adsProgress, float eyeOffsetX, float eyeOffsetY,
            Runnable beforePunchAttachments) {
        WeaponScopeRenderer.renderOpticalScopePasses(target, model, shaderId, resolver, data, shaderStack,
                isGUI, aimingAds, reticlePath, adsProgress, eyeOffsetX, eyeOffsetY, beforePunchAttachments);
    }

    public static float computeViewmodelAdsFov(float scopeZoom, float customAdsFov, float defaultAdsFov, float baseFov) {
        if (customAdsFov > 4.0f && customAdsFov < 170.0f) {
            return customAdsFov;
        }
        if (scopeZoom > 1.05f) {
            return 55.0f;
        }
        if (defaultAdsFov > 4.0f && defaultAdsFov < 170.0f) {
            return defaultAdsFov;
        }
        return (baseFov > 30.0f) ? baseFov : 70.0f;
    }

    public static float getInterpolatedFovMultiplier(ItemStack stack, WeaponModelRenderer renderer) {
        return 1.0f;
    }

    public static float calculateOpticFov(float scopeZoom, float adsFov, float defaultFov) {
        if (adsFov > 4.0f && adsFov < 110.0f) {
            return adsFov;
        }
        if (scopeZoom > 0.01f) {
            if (scopeZoom <= 1.05f) {
                return -1.0f;
            }
            float calculated = 70.0f / scopeZoom;
            return Math.max(4.0f, Math.min(85.0f, calculated));
        }
        return defaultFov > 4.0f ? defaultFov : 70.0f;
    }

    public static float calculateOpticFov(float scopeZoom, float defaultFov) {
        if (scopeZoom > 1.05f) {
            float calculated = 70.0f / scopeZoom;
            return Math.max(4.0f, Math.min(85.0f, calculated));
        }
        return -1.0f;
    }

    public static float getTargetWorldFov(ItemStack stack, WeaponData liveData) {
        if (stack == null || stack.isEmpty()) return 70.0f;
        Minecraft mc = Minecraft.getMinecraft();
        float baseFov = mc.gameSettings.fovSetting;

        float fov = -1.0f;
        NBTTagCompound at = WeaponAttachmentRenderer.getAttachmentsTag(stack);
        if (at != null && !at.hasNoTags()) {
            for (String slotKey : at.getKeySet()) {
                String rawId = WeaponAttachmentRenderer.extractRawId(at.getTag(slotKey));
                if (rawId == null || rawId.isEmpty()) continue;
                String cleanId = rawId.contains(":") ? new ResourceLocation(rawId).getResourcePath() : rawId;

                WeaponModelRenderer attR = WeaponAttachmentRenderer.resolveRenderer(rawId, cleanId);
                net.minecraft.item.Item item = net.minecraft.item.Item.REGISTRY.getObject(new ResourceLocation(rawId));
                if (item == null) item = net.minecraft.item.Item.REGISTRY.getObject(new ResourceLocation("minezone", cleanId));
                ItemWeaponAttachment attachItem = (item instanceof ItemWeaponAttachment) ? (ItemWeaponAttachment) item : null;

                float scopeZoom = 1.0f;
                if (attR != null && attR.getWeaponData() != null && attR.getWeaponData().scopeZoom > 0f) {
                    scopeZoom = attR.getWeaponData().scopeZoom;
                } else if (attachItem != null && attachItem.getScopeZoom() > 0f) {
                    scopeZoom = attachItem.getScopeZoom();
                }

                if (scopeZoom > 1.05f) {
                    fov = calculateOpticFov(scopeZoom, baseFov);
                    break;
                }
            }
        }

        if (fov < 0f && liveData != null && liveData.hasBuiltInOptic && liveData.scopeZoom > 1.05f) {
            fov = calculateOpticFov(liveData.scopeZoom, baseFov);
        }

        return (fov > 0f) ? fov : baseFov;
    }

    private static void resetFOV() {
        GlStateManager.popMatrix();
        GlStateManager.matrixMode(GL11.GL_PROJECTION);
        GlStateManager.popMatrix();
        GlStateManager.matrixMode(GL11.GL_MODELVIEW);
    }

    private static void setupFOV(Minecraft mc, float fov) {
        fov = Math.max(1f, Math.min(179f, fov));
        float aspect = (float) mc.displayWidth / Math.max(mc.displayHeight, 1);
        GlStateManager.matrixMode(GL11.GL_PROJECTION);
        GlStateManager.pushMatrix();
        GlStateManager.loadIdentity();
        Project.gluPerspective(fov, aspect, NEAR_PLANE, FAR_PLANE);
        GlStateManager.matrixMode(GL11.GL_MODELVIEW);
        GlStateManager.pushMatrix();
    }

    private void renderFirstPerson(ItemStack stack, Minecraft mc, ModelState state) {
        WeaponData liveData = resolveLiveWeaponData();
        if (liveData == null) return;

        effectsRenderer.ensureHandNodesVisible(state.gltfModel);

        WeaponShaderManager.setAdsHideGeometry(false);
        WeaponScopeRenderer.resetAdsHideFlags();
        WeaponScopeRenderer.commitFpOverlayState(false, 0f);

        long now = System.nanoTime();
        float delta = 0f;
        if (lastFrameTime != 0L) {
            long diff = now - lastFrameTime;
            if (diff > 100_000L) {
                delta = (float) (diff / 1_000_000_000.0);
                lastFrameTime = now;
            }
        } else {
            lastFrameTime = now;
        }
        delta = Math.max(0f, Math.min(delta, 0.1f));

        WeaponStatCalculator.CalculatedStats stats = WeaponStatCalculator.calculate(stack);
        this.isSilenced = (stats != null && stats.isSilenced);
        if (this.isSilenced) {
            effectsRenderer.clearMuzzleFlash();
        }
        float adsSpeed = Math.max(0.001f, stats.aimTime);

        String slotNow = getActiveSlot();
        boolean aimBlocked = isAimBlockedBySlot(slotNow);
        boolean effectiveAiming = this.isAiming && !aimBlocked;

        if (this.forcedAds) {
            adsProgress = this.forcedAdsValue;
        } else if (effectiveAiming) {
            adsProgress = Math.min(1.0f, adsProgress + delta / adsSpeed);
        } else {
            float unAimSpeed = aimBlocked ? RELOAD_UNAIM_SPEED : adsSpeed;
            adsProgress = Math.max(0.0f, adsProgress - delta / Math.max(0.001f, unAimSpeed));
        }

        if (mc.player.isSprinting() && !effectiveAiming)
            sprintProgress = Math.min(1f, sprintProgress + delta / 0.20f);
        else
            sprintProgress = Math.max(0f, sprintProgress - delta / 0.20f);

        float currentPitch = mc.player.rotationPitch;
        float currentYaw   = mc.player.rotationYaw;
        float dYaw   = currentYaw   - lastYaw;
        float dPitch = currentPitch - lastPitch;
        lastYaw   = currentYaw;
        lastPitch = currentPitch;

        float smoothT = smoothstep(adsProgress);
        float parallaxDamping = lerp(1.0f, 0.24f, smoothT);

        float swayInfluenceX = com.fanisdeop.minezone.client.event.SwayBreathHandler.currentSwayX * 0.65f;
        float swayInfluenceY = com.fanisdeop.minezone.client.event.SwayBreathHandler.currentSwayY * 0.65f;

        float targetEyeX = (dYaw   * 0.28f + swayInfluenceX) * parallaxDamping;
        float targetEyeY = (dPitch * 0.28f + swayInfluenceY) * parallaxDamping;

        eyeOffsetX = lerp(eyeOffsetX, targetEyeX, delta * 16.0f);
        eyeOffsetY = lerp(eyeOffsetY, targetEyeY, delta * 16.0f);
        
        eyeOffsetX = Math.max(-0.42f, Math.min(0.42f, eyeOffsetX));
        eyeOffsetY = Math.max(-0.42f, Math.min(0.42f, eyeOffsetY));

        List<String> hiddenDefaults = Collections.emptyList();
        boolean hideEkran = false;

        try {
            GlStateManager.matrixMode(GL11.GL_MODELVIEW);
            GlStateManager.pushMatrix();
            try {
                CURRENT_MODELVIEW_MATRIX.clear();
                GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, CURRENT_MODELVIEW_MATRIX);
                float bobX = CURRENT_MODELVIEW_MATRIX.get(12);
                float bobY = CURRENT_MODELVIEW_MATRIX.get(13);
                GlStateManager.loadIdentity();
                GL11.glTranslatef(lerp(bobX, 0f, smoothT), lerp(bobY, 0f, smoothT), 0f);

                boolean isEmptyState = (stack != null) && (stack.getItem() instanceof ItemCustomWeapon)
                        && ((ItemCustomWeapon) stack.getItem()).isEmpty(stack);
                boolean hasTacticalGrip = WeaponAttachmentRenderer.hasTacticalGripEquipped(stack, liveData, this.weaponId);
                boolean hasGL = WeaponAttachmentRenderer.hasUnderbarrelEquipped(stack);
                float[] gripArmOffset = WeaponAttachmentRenderer.getTacticalGripArmOffset(stack, liveData, this.weaponId);
                float[] gripArmRot = WeaponAttachmentRenderer.getTacticalGripArmRotation(stack, liveData, this.weaponId);
                animator.setGripArmConfig(hasTacticalGrip, gripArmOffset, gripArmRot);
                animator.setWeaponState(isEmptyState, hasGL);

                animator.resetPoseToIdle(liveData, state.track);

                AdsPose dynamicAds = computeAdsPose(liveData, stack, state);

                float activeSpeed = 1.0f;
                String activeSlot = getActiveSlot();
                if (activeSlot != null) {
                    if (WeaponData.ANIM_DRAW.equals(activeSlot)) {
                        AnimationDef animDef = liveData.getAnimation(activeSlot);
                        float naturalDuration = (animDef != null) ? animDef.getDurationSeconds() : 1.0f;
                        if (stats.drawTime > 0.01f && naturalDuration > 0.001f) {
                            activeSpeed = naturalDuration / stats.drawTime;
                        }
                    } else if (activeSlot != null && activeSlot.startsWith("reload")) {
                        boolean isEmpty = (stack != null) && (stack.getItem() instanceof ItemCustomWeapon)
                                && ((ItemCustomWeapon) stack.getItem()).isEmpty(stack);
                        float targetReloadTime = isEmpty ? stats.reloadTimeFull : stats.reloadTimeTactical;
                        if (targetReloadTime <= 0.01f) targetReloadTime = stats.reloadTimeFull > 0.01f ? stats.reloadTimeFull : 2.5f;

                        AnimationDef animDef = liveData.getAnimation(activeSlot);
                        float naturalDuration = (animDef != null) ? animDef.getDurationSeconds() : 1.0f;
                        if (targetReloadTime > 0.01f && naturalDuration > 0.001f) {
                            activeSpeed = naturalDuration / targetReloadTime;
                        }
                    } else if (activeSlot != null && !activeSlot.contains("dry") && (activeSlot.startsWith("shoot") || activeSlot.contains("shoot") || activeSlot.contains("fire"))) {
                        float shotInterval = 60.0f / Math.max(1, stats.rpm);
                        AnimationDef animDef = liveData.getAnimation(activeSlot);
                        float naturalDuration = (animDef != null) ? animDef.getDurationSeconds() : 0.1f;
                        if (shotInterval > 0.001f && naturalDuration > shotInterval) {
                            activeSpeed = naturalDuration / shotInterval;
                        }
                    }
                }

                animator.update(mc, liveData, state.gltfModel, state.track, adsProgress, activeSpeed);
                float baseFov = (liveData.weaponFov > 0f)
                        ? liveData.weaponFov
                        : (liveData.fpFov > 0f ? liveData.fpFov : 70.0f);

                float currentFov = lerp(baseFov, dynamicAds.fov, smoothT);

                setupFOV(mc, currentFov);
                GL11.glDepthRange(0.0, 0.1);

                applyTransformations(liveData, mc, dynamicAds);

                cacheEjectionPortWorld(mc, liveData, state, currentFov);

                WeaponShaderManager.setupLightingForWeapon(
                        mc, TransformType.FIRST_PERSON_RIGHT_HAND, false, liveData);

                hideEkran = liveData.isDetector && DetectorRenderer.isDischarged(stack);
                if (hideEkran) {
                    GltfNodeHelper.setNodeVisible(state.gltfModel, "ekran", false);
                }

                hiddenDefaults = WeaponAttachmentRenderer.hideDefaultMeshesForAttachments(
                        stack, state.gltfModel, this.weaponId);

                boolean isAimingAds = adsProgress > 0.05F;
                
                boolean externalOpticMounted = (findOpticAttachmentWorldPos(stack, state.gltfModel) != null);
                boolean useBuiltInScope = !externalOpticMounted && liveData.hasBuiltInOptic && isAimingAds;

                if (isAimingAds) {
                    WeaponShaderManager.bindWeaponShader(stack, weaponId, textureResolver, liveData, false);
                }

                WeaponShaderManager.setOpticMask(false, 0, 0, 0);

                boolean stencilActive = false;
                if (useBuiltInScope) {
                    WeaponScopeRenderer.renderOpticalScopePasses(
                            this, state.gltfModel, weaponId, textureResolver, liveData, stack,
                            false, true, null, adsProgress, eyeOffsetX, eyeOffsetY, null);
                } else {
                    if (externalOpticMounted && isAimingAds) {
                        stencilActive = WeaponAttachmentRenderer.runScopeStencilPrePass(stack, state, weaponId);
                        WeaponShaderManager.bindWeaponShader(stack, weaponId, textureResolver, liveData, false);
                    }
                    renderGltfScene(state.scene);
                }

                WeaponAttachmentRenderer.renderAttachments(
                        stack, state, false, adsProgress, this.weaponId, eyeOffsetX, eyeOffsetY);

                if (stencilActive) {
                    GL11.glDisable(GL11.GL_STENCIL_TEST);
                    GL11.glStencilMask(0xFF);
                }

                WeaponScopeRenderer.flushDeferredReticle();

                if (liveData.isDetector && !hideEkran) {
                    try {
                        detectorRenderer.renderDetectorMesh(
                                mc, liveData, state.gltfModel, stack, weaponId, textureResolver);
                    } catch (Throwable ignored) {}
                    GLStateSanitizer.sanitize();
                }

                GLStateSanitizer.restoreProgram();
                GLStateSanitizer.sanitize();
                WeaponShaderManager.hardResetStates();
                WeaponShaderManager.setAdsHideGeometry(false);

                GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY);
                GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
                GL11.glDisableClientState(GL11.GL_NORMAL_ARRAY);
                GL11.glDisableClientState(GL11.GL_COLOR_ARRAY);

                GlStateManager.setActiveTexture(GL13.GL_TEXTURE0);
                GlStateManager.enableTexture2D();
                GL11.glEnable(GL11.GL_TEXTURE_2D);

                GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
                GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f);
                GlStateManager.enableBlend();
                GlStateManager.tryBlendFuncSeparate(
                        GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA,
                        GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO
                );
                GlStateManager.depthMask(true);
                GlStateManager.enableDepth();
                GlStateManager.depthFunc(GL11.GL_LEQUAL);
                OpenGlHelper.glUseProgram(0);

                int oldPass = net.minecraftforge.client.MinecraftForgeClient.getRenderPass();
                try {
                    net.minecraftforge.client.ForgeHooksClient.setRenderPass(0);
                    effectsRenderer.renderAnimatedSleeves(mc, state.gltfModel, weaponId);
                } finally {
                    net.minecraftforge.client.ForgeHooksClient.setRenderPass(oldPass);
                }

                GlStateManager.disableLighting();
                GlStateManager.depthMask(false);
                if (!this.isSilenced) {
                    effectsRenderer.renderMuzzleFlashIfActive(mc, liveData, delta, adsProgress);
                }
                GlStateManager.depthMask(true);
                GlStateManager.enableLighting();

                if (DEBUG_SHOW_EJECTION_GIZMO) {
                    renderFirstPersonEjectionGizmo(mc, state, liveData);
                }
            } finally {
                WeaponShaderManager.setAdsHideGeometry(false);
                WeaponScopeRenderer.skipScopeBody = false;
                WeaponScopeRenderer.reticleOnlyPass = false;
                GL11.glDisable(GL11.GL_CLIP_PLANE0);
                GL11.glDepthRange(0.0, 1.0);
                resetFOV();
                GL11.glStencilMask(0xFF);
                GL11.glDisable(GL11.GL_STENCIL_TEST);
                WeaponAttachmentRenderer.restoreDefaultMeshes(state.gltfModel, hiddenDefaults);
                if (hideEkran) {
                    GltfNodeHelper.setNodeVisible(state.gltfModel, "ekran", true);
                }
                WeaponScopeRenderer.applyScopeVisibility(state.gltfModel, 0f, WeaponScopeRenderer.ScopePass.BODY);
                WeaponScopeRenderer.restoreAllNodesVisible(state.gltfModel);
                WeaponScopeRenderer.cleanGlClientStates();
            }
        } finally {
            GlStateManager.matrixMode(GL11.GL_MODELVIEW);
            GlStateManager.popMatrix();
        }
    }

    @Nullable
    private float[] findOpticAttachmentWorldPos(ItemStack stack, GltfModel weaponModel) {
        OpticAdsParameters p = resolveOpticParameters(stack, resolveLiveWeaponData(), weaponModel);
        return p.hasExternalOptic ? p.socketWorldPos : null;
    }

    public void renderWithBoundShader() {
        ModelState state = this.modelState;
        if (state == null || state.scene == null) return;

        GlStateManager.enableDepth();
        GlStateManager.depthMask(false);
        GlStateManager.disableCull();
        GL11.glDisable(GL11.GL_CULL_FACE);

        GlStateManager.enableBlend();
        GL11.glEnable(GL11.GL_BLEND);
        GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE);
        GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE);

        GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
        GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f);

        GlStateManager.setActiveTexture(GL13.GL_TEXTURE0);
        GlStateManager.enableTexture2D();
        GL11.glEnable(GL11.GL_TEXTURE_2D);

        int oldPass = net.minecraftforge.client.MinecraftForgeClient.getRenderPass();
        try {
            net.minecraftforge.client.ForgeHooksClient.setRenderPass(1);
            GlStateManager.enableBlend();
            GL11.glEnable(GL11.GL_BLEND);
            GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE);
            GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE);
            state.scene.renderForShaderMod();

            net.minecraftforge.client.ForgeHooksClient.setRenderPass(0);
            GlStateManager.enableBlend();
            GL11.glEnable(GL11.GL_BLEND);
            GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE);
            GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE);
            state.scene.renderForShaderMod();
        } finally {
            net.minecraftforge.client.ForgeHooksClient.setRenderPass(oldPass);
            GlStateManager.enableCull();
        }
    }
    private float[] cachedMeshCentroid = null;

    public float[] getMeshCentroid() {
        if (cachedMeshCentroid != null) return cachedMeshCentroid;
        ModelState state = this.modelState;
        if (state != null && state.gltfModel != null) {
            cachedMeshCentroid = computeModelCentroid(state.gltfModel);
            return cachedMeshCentroid;
        }
        return new float[]{ 0f, 0f, 0f };
    }

    public static float[] computeModelCentroid(GltfModel gltfModel) {
        if (gltfModel == null || gltfModel.getNodeModels() == null) return new float[]{ 0f, 0f, 0f };
        float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY, minZ = Float.POSITIVE_INFINITY;
        float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY, maxZ = Float.NEGATIVE_INFINITY;
        boolean any = false;

        for (NodeModel node : gltfModel.getNodeModels()) {
            float[] aabb = scanMeshAabb(node);
            if (aabb != null) {
                float[] g = WeaponTransformHelper.computeRenderedGlobalTransform(node, gltfModel);
                float gx = g != null ? g[12] : 0f;
                float gy = g != null ? g[13] : 0f;
                float gz = g != null ? g[14] : 0f;

                minX = Math.min(minX, aabb[0] + gx);
                minY = Math.min(minY, aabb[1] + gy);
                minZ = Math.min(minZ, aabb[2] + gz);
                maxX = Math.max(maxX, aabb[3] + gx);
                maxY = Math.max(maxY, aabb[4] + gy);
                maxZ = Math.max(maxZ, aabb[5] + gz);
                any = true;
            }
        }
        if (!any) return new float[]{ 0f, 0f, 0f };
        return new float[]{
            0.5f * (minX + maxX),
            0.5f * (minY + maxY),
            0.5f * (minZ + maxZ)
        };
    }
    private void cacheEjectionPortWorld(Minecraft mc, WeaponData liveData, ModelState state, float vmFov) {
        net.minecraft.entity.player.EntityPlayer player = mc.player;
        if (player == null || state == null || liveData == null) {
            this.ejectCacheValid = false;
            return;
        }

        NodeModel ejectNode = findEjectionNode(state.gltfModel, liveData);
        float gx = 0.035f;
        float gy = 0.020f;
        float gz = -0.150f;
        if (ejectNode != null) {
            float[] g = WeaponTransformHelper.computeRenderedGlobalTransform(ejectNode, state.gltfModel);
            if (g != null && g.length >= 16) {
                gx = g[12];
                gy = g[13];
                gz = g[14];
            }
        }
        if (liveData.casingOffset != null && liveData.casingOffset.length >= 3) {
            gx += liveData.casingOffset[0];
            gy += liveData.casingOffset[1];
            gz += liveData.casingOffset[2];
        }
        this.ejectLocalSideSign = (gx < -0.005f) ? -1.0f : 1.0f;

        CURRENT_MODELVIEW_MATRIX.clear();
        GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, CURRENT_MODELVIEW_MATRIX);

        float m00 = CURRENT_MODELVIEW_MATRIX.get(0);
        float m01 = CURRENT_MODELVIEW_MATRIX.get(1);
        float m02 = CURRENT_MODELVIEW_MATRIX.get(2);
        float m04 = CURRENT_MODELVIEW_MATRIX.get(4);
        float m05 = CURRENT_MODELVIEW_MATRIX.get(5);
        float m06 = CURRENT_MODELVIEW_MATRIX.get(6);
        float m08 = CURRENT_MODELVIEW_MATRIX.get(8);
        float m09 = CURRENT_MODELVIEW_MATRIX.get(9);
        float m10 = CURRENT_MODELVIEW_MATRIX.get(10);
        float m12 = CURRENT_MODELVIEW_MATRIX.get(12);
        float m13 = CURRENT_MODELVIEW_MATRIX.get(13);
        float m14 = CURRENT_MODELVIEW_MATRIX.get(14);

        // p_vm — camera-local позиция ноды во вьюмодели
        float vmX = m00 * gx + m04 * gy + m08 * gz + m12;
        float vmY = m01 * gx + m05 * gy + m09 * gz + m13;
        float vmZ = m02 * gx + m06 * gy + m10 * gz + m14;

        if (vmFov < 1.0f) vmFov = 45.0f;
        float worldFov = mc.gameSettings.fovSetting;
        if (worldFov < 1.0f) worldFov = 70.0f;
        double fovRatio = Math.tan(Math.toRadians(worldFov * 0.5))
                        / Math.tan(Math.toRadians(vmFov * 0.5));

        // Физическая глубина перед глазами игрока в метрах
        float mvScale = (float) Math.sqrt(m00 * m00 + m01 * m01 + m02 * m02);
        if (mvScale < 1.0e-5f) mvScale = 1.0f;
        double depthM = (double) (-vmZ / mvScale);
        if (depthM < 0.20) depthM = 0.20;
        if (depthM > 1.20) depthM = 1.20;

        // Точная угловая репроекция: пиксель на экране в vmFov совпадает с пикселем в worldFov
        double effectiveZ = Math.max(0.20, (double) (-vmZ / mvScale));
        double camRelX = (vmX / mvScale) * (depthM / effectiveZ) * fovRatio;
        double camRelY = (vmY / mvScale) * (depthM / effectiveZ) * fovRatio;
        double camRelZ = depthM;

        float partialTicks = mc.getRenderPartialTicks();
        float pitch = player.prevRotationPitch + (player.rotationPitch - player.prevRotationPitch) * partialTicks;
        float yaw   = player.prevRotationYaw   + (player.rotationYaw   - player.prevRotationYaw)   * partialTicks;
        pitch += com.fanisdeop.minezone.client.event.SwayBreathHandler.currentSwayY;
        yaw   += com.fanisdeop.minezone.client.event.SwayBreathHandler.currentSwayX;

        double pRad = Math.toRadians(pitch);
        double yRad = Math.toRadians(yaw);
        double cosP = Math.cos(pRad);
        double sinP = Math.sin(pRad);
        double cosY = Math.cos(yRad);
        double sinY = Math.sin(yRad);

        // Ортонормированный базис камеры в мировых координатах
        this.ejectFwdX = -sinY * cosP;
        this.ejectFwdY = -sinP;
        this.ejectFwdZ =  cosY * cosP;

        this.ejectRgtX = -cosY;
        this.ejectRgtY =  0.0;
        this.ejectRgtZ = -sinY;

        this.ejectUpX  = -sinY * sinP;
        this.ejectUpY  =  cosP;
        this.ejectUpZ  =  cosY * sinP;

        double eyeX = (mc.getRenderManager() != null && mc.getRenderManager().renderViewEntity != null)
                ? mc.getRenderManager().viewerPosX
                : player.lastTickPosX + (player.posX - player.lastTickPosX) * partialTicks;
        double eyeY = (mc.getRenderManager() != null && mc.getRenderManager().renderViewEntity != null)
                ? (mc.getRenderManager().viewerPosY + mc.getRenderManager().renderViewEntity.getEyeHeight())
                : player.lastTickPosY + (player.posY - player.lastTickPosY) * partialTicks + player.getEyeHeight();
        double eyeZ = (mc.getRenderManager() != null && mc.getRenderManager().renderViewEntity != null)
                ? mc.getRenderManager().viewerPosZ
                : player.lastTickPosZ + (player.posZ - player.lastTickPosZ) * partialTicks;

        this.ejectWorldX = eyeX + this.ejectFwdX * camRelZ + this.ejectRgtX * camRelX + this.ejectUpX * camRelY;
        this.ejectWorldY = eyeY + this.ejectFwdY * camRelZ + this.ejectRgtY * camRelX + this.ejectUpY * camRelY;
        this.ejectWorldZ = eyeZ + this.ejectFwdZ * camRelZ + this.ejectRgtZ * camRelX + this.ejectUpZ * camRelY;
        this.ejectCacheValid = true;

        float[] baseVel = (liveData.casingVelocity != null && liveData.casingVelocity.length >= 3)
                ? liveData.casingVelocity : new float[]{ 3.4f, 0.20f, -0.35f };

        DEBUG_EJECTION_INFO.valid = true;
        DEBUG_EJECTION_INFO.weaponId = this.weaponId;
        DEBUG_EJECTION_INFO.socketName = (ejectNode != null) ? ejectNode.getName() : "fallback (default)";
        DEBUG_EJECTION_INFO.originX = this.ejectWorldX;
        DEBUG_EJECTION_INFO.originY = this.ejectWorldY;
        DEBUG_EJECTION_INFO.originZ = this.ejectWorldZ;
        DEBUG_EJECTION_INFO.fwdX = this.ejectFwdX;
        DEBUG_EJECTION_INFO.fwdY = this.ejectFwdY;
        DEBUG_EJECTION_INFO.fwdZ = this.ejectFwdZ;
        DEBUG_EJECTION_INFO.rgtX = this.ejectRgtX;
        DEBUG_EJECTION_INFO.rgtY = this.ejectRgtY;
        DEBUG_EJECTION_INFO.rgtZ = this.ejectRgtZ;
        DEBUG_EJECTION_INFO.upX = this.ejectUpX;
        DEBUG_EJECTION_INFO.upY = this.ejectUpY;
        DEBUG_EJECTION_INFO.upZ = this.ejectUpZ;
        DEBUG_EJECTION_INFO.baseVel = baseVel;
        DEBUG_EJECTION_INFO.spread = Math.max(0.0f, liveData.casingSpread);
        DEBUG_EJECTION_INFO.velX = this.ejectRgtX * baseVel[0] + this.ejectUpX * baseVel[1] + this.ejectFwdX * baseVel[2];
        DEBUG_EJECTION_INFO.velY = this.ejectRgtY * baseVel[0] + this.ejectUpY * baseVel[1] + this.ejectFwdY * baseVel[2];
        DEBUG_EJECTION_INFO.velZ = this.ejectRgtZ * baseVel[0] + this.ejectUpZ * baseVel[1] + this.ejectFwdZ * baseVel[2];
        DEBUG_EJECTION_INFO.lastUpdateTime = System.currentTimeMillis();
    }

    private void renderFirstPersonEjectionGizmo(Minecraft mc, ModelState state, WeaponData liveData) {
        if (state == null || state.gltfModel == null || liveData == null) return;

        NodeModel ejectNode = findEjectionNode(state.gltfModel, liveData);
        float gx = 0.035f, gy = 0.020f, gz = -0.150f;
        if (ejectNode != null) {
            float[] g = WeaponTransformHelper.computeRenderedGlobalTransform(ejectNode, state.gltfModel);
            if (g != null && g.length >= 16) {
                gx = g[12];
                gy = g[13];
                gz = g[14];
            }
        }
        if (liveData.casingOffset != null && liveData.casingOffset.length >= 3) {
            gx += liveData.casingOffset[0];
            gy += liveData.casingOffset[1];
            gz += liveData.casingOffset[2];
        }

        float[] baseVel = (liveData.casingVelocity != null && liveData.casingVelocity.length >= 3)
                ? liveData.casingVelocity : new float[]{ 3.4f, 0.20f, -0.35f };
        float spread = Math.max(0.0f, liveData.casingSpread);

        GLStateSanitizer.saveCurrentProgram();
        OpenGlHelper.glUseProgram(0);

        GlStateManager.pushMatrix();
        GlStateManager.translate(gx, gy, gz);

        GlStateManager.disableTexture2D();
        GlStateManager.enableBlend();
        GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO);
        GlStateManager.disableLighting();
        GlStateManager.disableDepth();
        GlStateManager.depthMask(false);
        GlStateManager.disableCull();

        net.minecraft.client.renderer.Tessellator tess = net.minecraft.client.renderer.Tessellator.getInstance();
        net.minecraft.client.renderer.BufferBuilder bb = tess.getBuffer();

        float pulse = 0.008f + 0.002f * (float) Math.sin(System.currentTimeMillis() * 0.01);

        // 1. Точка вылета (Пульсирующая сфера)
        bb.begin(GL11.GL_TRIANGLE_FAN, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_COLOR);
        bb.pos(0, 0, 0).color(1.0f, 0.9f, 0.2f, 1.0f).endVertex();
        for (int i = 0; i <= 16; i++) {
            double angle = i * 2.0 * Math.PI / 16.0;
            bb.pos(Math.cos(angle) * pulse, Math.sin(angle) * pulse, 0).color(0.0f, 1.0f, 1.0f, 0.7f).endVertex();
        }
        tess.draw();

        bb.begin(GL11.GL_TRIANGLE_FAN, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_COLOR);
        bb.pos(0, 0, 0).color(1.0f, 0.9f, 0.2f, 1.0f).endVertex();
        for (int i = 0; i <= 16; i++) {
            double angle = i * 2.0 * Math.PI / 16.0;
            bb.pos(0, Math.cos(angle) * pulse, Math.sin(angle) * pulse).color(0.0f, 1.0f, 1.0f, 0.7f).endVertex();
        }
        tess.draw();

        // 2. Локальные оси (X=Red, Y=Green, Z=Blue)
        float axLen = 0.035f;
        GL11.glLineWidth(2.5f);
        bb.begin(GL11.GL_LINES, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_COLOR);
        // +X (Right / Red)
        bb.pos(0, 0, 0).color(1f, 0.1f, 0.1f, 1.0f).endVertex();
        bb.pos(axLen, 0, 0).color(1f, 0.1f, 0.1f, 1.0f).endVertex();
        // +Y (Up / Green)
        bb.pos(0, 0, 0).color(0.1f, 1f, 0.1f, 1.0f).endVertex();
        bb.pos(0, axLen, 0).color(0.1f, 1f, 0.1f, 1.0f).endVertex();
        // +Z (Back / Blue)
        bb.pos(0, 0, 0).color(0.2f, 0.4f, 1f, 1.0f).endVertex();
        bb.pos(0, 0, axLen).color(0.2f, 0.4f, 1f, 1.0f).endVertex();
        tess.draw();

        // 3. Основной вектор траектории вылета гильзы
        double vLen = Math.sqrt(baseVel[0] * baseVel[0] + baseVel[1] * baseVel[1] + baseVel[2] * baseVel[2]);
        double rayScale = 0.040;
        double vx = baseVel[0] * rayScale;
        double vy = baseVel[1] * rayScale;
        double vz = baseVel[2] * rayScale;

        GL11.glLineWidth(4.0f);
        bb.begin(GL11.GL_LINES, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_COLOR);
        bb.pos(0, 0, 0).color(1.0f, 0.85f, 0.0f, 1.0f).endVertex();
        bb.pos(vx, vy, vz).color(1.0f, 0.35f, 0.0f, 1.0f).endVertex();
        tess.draw();

        // Стрелка на конце вектора
        double tip = 0.012;
        double nvx = (vLen > 1e-4) ? (baseVel[0] / vLen) : 1.0;
        double nvy = (vLen > 1e-4) ? (baseVel[1] / vLen) : 0.0;
        double nvz = (vLen > 1e-4) ? (baseVel[2] / vLen) : 0.0;

        bb.begin(GL11.GL_LINES, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_COLOR);
        bb.pos(vx, vy, vz).color(1.0f, 1.0f, 0.0f, 1.0f).endVertex();
        bb.pos(vx - nvx * tip + 0.004, vy - nvy * tip, vz - nvz * tip).color(1.0f, 0.4f, 0.0f, 1.0f).endVertex();
        bb.pos(vx, vy, vz).color(1.0f, 1.0f, 0.0f, 1.0f).endVertex();
        bb.pos(vx - nvx * tip - 0.004, vy - nvy * tip, vz - nvz * tip).color(1.0f, 0.4f, 0.0f, 1.0f).endVertex();
        tess.draw();

        // 4. Конус разброса (Spread Cone)
        if (spread > 0.001f) {
            GL11.glLineWidth(1.5f);
            double coneRadius = Math.sqrt(vx * vx + vy * vy + vz * vz) * Math.tan(Math.min(spread * 0.35f, 0.6f));

            bb.begin(GL11.GL_LINE_LOOP, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_COLOR);
            for (int i = 0; i < 20; i++) {
                double a = i * 2.0 * Math.PI / 20.0;
                double cx = vx + Math.cos(a) * coneRadius * 0.5;
                double cy = vy + Math.sin(a) * coneRadius;
                double cz = vz + Math.cos(a) * coneRadius * 0.5;
                bb.pos(cx, cy, cz).color(1.0f, 0.6f, 0.1f, 0.5f).endVertex();
            }
            tess.draw();

            bb.begin(GL11.GL_LINES, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_COLOR);
            for (int i = 0; i < 4; i++) {
                double a = i * Math.PI * 0.5;
                double cx = vx + Math.cos(a) * coneRadius * 0.5;
                double cy = vy + Math.sin(a) * coneRadius;
                double cz = vz + Math.cos(a) * coneRadius * 0.5;
                bb.pos(0, 0, 0).color(1.0f, 0.8f, 0.2f, 0.35f).endVertex();
                bb.pos(cx, cy, cz).color(1.0f, 0.4f, 0.1f, 0.6f).endVertex();
            }
            tess.draw();
        }

        // 5. Текстовый HUD Billboard
        GlStateManager.enableTexture2D();
        GlStateManager.pushMatrix();
        GlStateManager.translate(0, 0.035, 0);

        float textScale = 0.0012f;
        GlStateManager.scale(-textScale, -textScale, textScale);

        net.minecraft.client.gui.FontRenderer fr = mc.fontRenderer;
        if (fr != null) {
            String nodeName = (ejectNode != null) ? ejectNode.getName() : "fallback (default)";
            String title = "§6[EJECT GIZMO] §e" + nodeName;
            String velStr = String.format(java.util.Locale.US, "§fVel: §a[%.2f, %.2f, %.2f] §7(%.1f m/s)", baseVel[0], baseVel[1], baseVel[2], vLen);
            String sprStr = String.format(java.util.Locale.US, "§fSpread: §e%.2f", spread);

            int w1 = fr.getStringWidth(title);
            int w2 = fr.getStringWidth(velStr);
            int w3 = fr.getStringWidth(sprStr);
            int maxW = Math.max(w1, Math.max(w2, w3));

            int pad = 2;
            int bgX1 = -maxW / 2 - pad;
            int bgX2 = maxW / 2 + pad;
            int bgY1 = -32;
            int bgY2 = -2;

            GlStateManager.disableTexture2D();
            bb.begin(GL11.GL_QUADS, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_COLOR);
            bb.pos(bgX1, bgY1, 0).color(0f, 0f, 0f, 0.75f).endVertex();
            bb.pos(bgX1, bgY2, 0).color(0f, 0f, 0f, 0.75f).endVertex();
            bb.pos(bgX2, bgY2, 0).color(0f, 0f, 0f, 0.75f).endVertex();
            bb.pos(bgX2, bgY1, 0).color(0f, 0f, 0f, 0.75f).endVertex();
            tess.draw();
            GlStateManager.enableTexture2D();

            fr.drawString(title, -w1 / 2, -30, 0xFFFFFF);
            fr.drawString(velStr, -w2 / 2, -20, 0xFFFFFF);
            fr.drawString(sprStr, -w3 / 2, -10, 0xFFFFFF);
        }

        GlStateManager.popMatrix();

        GlStateManager.enableDepth();
        GlStateManager.depthMask(true);
        GlStateManager.enableLighting();
        GlStateManager.enableCull();
        GlStateManager.popMatrix();

        GLStateSanitizer.restoreProgram();
    }
}