Загрузка данных
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();
}
}