From e24c558b0304fafc5a353e72de7b77916c3f42b6 Mon Sep 17 00:00:00 2001 From: Uraneptus Date: Wed, 18 Sep 2024 17:26:42 +0200 Subject: [PATCH] port changes from 1.20.1-37 --- .../java/vazkii/neat/NeatFiberConfig.java | 60 +++++++++++++++ .../java/vazkii/neat/NeatNeoForgeConfig.java | 42 +++++++++++ .../java/vazkii/neat/HealthBarRenderer.java | 75 +++++++++++++------ .../src/main/java/vazkii/neat/NeatConfig.java | 8 +- .../neat/mixin/EntityRendererMixin.java | 22 ++++++ Xplat/src/main/resources/neat.mixins.json | 1 + gradle.properties | 2 +- 7 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 Xplat/src/main/java/vazkii/neat/mixin/EntityRendererMixin.java diff --git a/Fabric/src/main/java/vazkii/neat/NeatFiberConfig.java b/Fabric/src/main/java/vazkii/neat/NeatFiberConfig.java index d15fe2b..08b81ca 100644 --- a/Fabric/src/main/java/vazkii/neat/NeatFiberConfig.java +++ b/Fabric/src/main/java/vazkii/neat/NeatFiberConfig.java @@ -54,6 +54,7 @@ public static void setup() { private static class Client implements NeatConfig.ConfigAccess { private final PropertyMirror maxDistance = PropertyMirror.create(INTEGER); + private final PropertyMirror maxDistanceWithoutLineOfSight = PropertyMirror.create(INTEGER); private final PropertyMirror renderInF1 = PropertyMirror.create(BOOLEAN); private final PropertyMirror heightAbove = PropertyMirror.create(DOUBLE); private final PropertyMirror drawBackground = PropertyMirror.create(BOOLEAN); @@ -66,15 +67,20 @@ private static class Client implements NeatConfig.ConfigAccess { private final PropertyMirror showArmor = PropertyMirror.create(BOOLEAN); private final PropertyMirror groupArmor = PropertyMirror.create(BOOLEAN); private final PropertyMirror colorByType = PropertyMirror.create(BOOLEAN); + private final PropertyMirror textColor = PropertyMirror.create(STRING); private final PropertyMirror hpTextHeight = PropertyMirror.create(INTEGER); private final PropertyMirror showMaxHP = PropertyMirror.create(BOOLEAN); private final PropertyMirror showCurrentHP = PropertyMirror.create(BOOLEAN); private final PropertyMirror showPercentage = PropertyMirror.create(BOOLEAN); + private final PropertyMirror showOnPassive = PropertyMirror.create(BOOLEAN); + private final PropertyMirror showOnHostile = PropertyMirror.create(BOOLEAN); private final PropertyMirror showOnPlayers = PropertyMirror.create(BOOLEAN); private final PropertyMirror showOnBosses = PropertyMirror.create(BOOLEAN); private final PropertyMirror showOnlyFocused = PropertyMirror.create(BOOLEAN); private final PropertyMirror showFullHealth = PropertyMirror.create(BOOLEAN); private final PropertyMirror enableDebugInfo = PropertyMirror.create(BOOLEAN); + private final PropertyMirror showEntityName = PropertyMirror.create(BOOLEAN); + private final PropertyMirror disableNameTag = PropertyMirror.create(BOOLEAN); private final PropertyMirror> blacklist = PropertyMirror.create(ConfigTypes.makeList(STRING)); public ConfigTree configure(ConfigTreeBuilder builder) { @@ -82,6 +88,10 @@ public ConfigTree configure(ConfigTreeBuilder builder) { .withComment("Maximum distance in blocks at which health bars should render") .finishValue(maxDistance::mirror) + .beginValue("maxDistanceWithoutLineOfSight", INTEGER, 8) + .withComment("Maximum distance in blocks at which health bars should render without line of sight") + .finishValue(maxDistanceWithoutLineOfSight::mirror) + .beginValue("renderInF1", BOOLEAN, false) .withComment("Whether health bars should render when the HUD is disabled with F1") .finishValue(renderInF1::mirror) @@ -130,6 +140,10 @@ public ConfigTree configure(ConfigTreeBuilder builder) { .withComment("Color the bar differently depending on whether the entity is hostile or is a boss") .finishValue(colorByType::mirror) + .beginValue("textColor", STRING, "FFFFFF") + .withComment("Text color in hex code format") + .finishValue(textColor::mirror) + .beginValue("hpTextHeight", INTEGER, 14) .withComment("Height of the text on the health bar") .finishValue(hpTextHeight::mirror) @@ -146,6 +160,14 @@ public ConfigTree configure(ConfigTreeBuilder builder) { .withComment("Whether the percentage health of the mob should be shown") .finishValue(showPercentage::mirror) + .beginValue("showOnPassive", BOOLEAN, true) + .withComment("Whether bars on passive mobs should be shown") + .finishValue(showOnPassive::mirror) + + .beginValue("showOnHostile", BOOLEAN, true) + .withComment("Whether bars on hostile mobs should be shown (does not include bosses)") + .finishValue(showOnHostile::mirror) + .beginValue("showOnPlayers", BOOLEAN, true) .withComment("Whether bars on players should be shown") .finishValue(showOnPlayers::mirror) @@ -166,6 +188,14 @@ public ConfigTree configure(ConfigTreeBuilder builder) { .withComment("Show extra debug info on the bar when F3 is enabled") .finishValue(enableDebugInfo::mirror) + .beginValue("showEntityName", BOOLEAN, true) + .withComment("Show entity name") + .finishValue(showEntityName::mirror) + + .beginValue("disableNameTag", BOOLEAN, false) + .withComment("Disables the rendering of the vanilla name tag") + .finishValue(disableNameTag::mirror) + .beginValue("blacklist", ConfigTypes.makeList(STRING), NeatConfig.DEFAULT_DISABLED) .withComment("Entity ID's that should not have bars rendered") .finishValue(blacklist::mirror); @@ -178,6 +208,11 @@ public int maxDistance() { return maxDistance.getValue(); } + @Override + public int maxDistanceWithoutLineOfSight() { + return maxDistanceWithoutLineOfSight.getValue(); + } + @Override public boolean renderInF1() { return renderInF1.getValue(); @@ -238,6 +273,11 @@ public boolean colorByType() { return colorByType.getValue(); } + @Override + public String textColor() { + return textColor.getValue(); + } + @Override public int hpTextHeight() { return hpTextHeight.getValue(); @@ -258,6 +298,16 @@ public boolean showPercentage() { return showPercentage.getValue(); } + @Override + public boolean showOnPassive() { + return showOnPassive.getValue(); + } + + @Override + public boolean showOnHostile() { + return showOnHostile.getValue(); + } + @Override public boolean showOnPlayers() { return showOnPlayers.getValue(); @@ -283,6 +333,16 @@ public boolean enableDebugInfo() { return enableDebugInfo.getValue(); } + @Override + public boolean showEntityName() { + return showEntityName.getValue(); + } + + @Override + public boolean disableNameTag() { + return disableNameTag.getValue(); + } + @Override public List blacklist() { return blacklist.getValue(); diff --git a/NeoForge/src/main/java/vazkii/neat/NeatNeoForgeConfig.java b/NeoForge/src/main/java/vazkii/neat/NeatNeoForgeConfig.java index 2f405d6..10d5dc1 100644 --- a/NeoForge/src/main/java/vazkii/neat/NeatNeoForgeConfig.java +++ b/NeoForge/src/main/java/vazkii/neat/NeatNeoForgeConfig.java @@ -17,6 +17,7 @@ public static void init(ModContainer container) { private static class ForgeNeatConfig implements NeatConfig.ConfigAccess { private final ModConfigSpec.ConfigValue maxDistance; + private final ModConfigSpec.ConfigValue maxDistanceWithoutLineOfSight; private final ModConfigSpec.ConfigValue renderInF1; private final ModConfigSpec.ConfigValue heightAbove; private final ModConfigSpec.ConfigValue drawBackground; @@ -29,14 +30,19 @@ private static class ForgeNeatConfig implements NeatConfig.ConfigAccess { private final ModConfigSpec.ConfigValue showArmor; private final ModConfigSpec.ConfigValue groupArmor; private final ModConfigSpec.ConfigValue colorByType; + private final ModConfigSpec.ConfigValue textColor; private final ModConfigSpec.ConfigValue hpTextHeight; private final ModConfigSpec.ConfigValue showMaxHP; private final ModConfigSpec.ConfigValue showCurrentHP; private final ModConfigSpec.ConfigValue showPercentage; + private final ModConfigSpec.ConfigValue showOnPassive; + private final ModConfigSpec.ConfigValue showOnHostile; private final ModConfigSpec.ConfigValue showOnPlayers; private final ModConfigSpec.ConfigValue showOnBosses; private final ModConfigSpec.ConfigValue showOnlyFocused; private final ModConfigSpec.ConfigValue showFullHealth; + private final ModConfigSpec.ConfigValue showEntityName; + private final ModConfigSpec.ConfigValue disableNameTag; private final ModConfigSpec.ConfigValue enableDebugInfo; private final ModConfigSpec.ConfigValue> blacklist; @@ -44,6 +50,7 @@ public ForgeNeatConfig(ModConfigSpec.Builder builder) { builder.push("general"); maxDistance = builder.define("max_distance", 24); + maxDistanceWithoutLineOfSight = builder.define("max_distance_without_line_of_sight", 8); renderInF1 = builder.comment("Render if F1 is pressed").define("render_without_gui", false); heightAbove = builder.define("height_above_mob", 0.6); drawBackground = builder.define("draw_background", true); @@ -56,15 +63,20 @@ public ForgeNeatConfig(ModConfigSpec.Builder builder) { showArmor = builder.define("show_armor", true); groupArmor = builder.comment("Condense 5 iron icons into 1 diamond icon").define("group_armor", true); colorByType = builder.comment("Color health bar by type instead of health percentage").define("color_health_bar_by_type", false); + textColor = builder.comment("Text color in hex code format").define("text_color", "FFFFFF"); hpTextHeight = builder.define("hp_text_height", 14); showMaxHP = builder.define("show_max_hp", true); showCurrentHP = builder.define("show_current_hp", true); showPercentage = builder.define("show_hp_percentage", true); + showOnPassive = builder.comment("Whether bars on passive mobs should be shown").define("show_on_passive", true); + showOnHostile = builder.comment("Whether bars on hostile mobs should be shown (does not include bosses)").define("show_on_hostile", true); showOnPlayers = builder.define("display_on_players", true); showOnBosses = builder.define("display_on_bosses", true); showOnlyFocused = builder.define("only_health_bar_for_target", false); showFullHealth = builder.define("show_entity_full_health", true); enableDebugInfo = builder.define("show_debug_with_f3", true); + showEntityName = builder.define("show_entity_name", true); + disableNameTag = builder.comment("Disables the rendering of the vanilla name tag").define("disable_name_tag", false); blacklist = builder.comment("Blacklist uses entity IDs, not their display names. Use F3 to see them in the Neat bar.") .defineList("blacklist", NeatConfig.DEFAULT_DISABLED, a -> true); @@ -76,6 +88,11 @@ public int maxDistance() { return maxDistance.get(); } + @Override + public int maxDistanceWithoutLineOfSight() { + return maxDistanceWithoutLineOfSight.get(); + } + @Override public boolean renderInF1() { return renderInF1.get(); @@ -136,6 +153,11 @@ public boolean colorByType() { return colorByType.get(); } + @Override + public String textColor() { + return textColor.get(); + } + @Override public int hpTextHeight() { return hpTextHeight.get(); @@ -156,6 +178,16 @@ public boolean showPercentage() { return showPercentage.get(); } + @Override + public boolean showOnPassive() { + return showOnPassive.get(); + } + + @Override + public boolean showOnHostile() { + return showOnHostile.get(); + } + @Override public boolean showOnPlayers() { return showOnPlayers.get(); @@ -181,6 +213,16 @@ public boolean enableDebugInfo() { return enableDebugInfo.get(); } + @Override + public boolean showEntityName() { + return showEntityName.get(); + } + + @Override + public boolean disableNameTag() { + return disableNameTag.get(); + } + @SuppressWarnings("unchecked") @Override public List blacklist() { diff --git a/Xplat/src/main/java/vazkii/neat/HealthBarRenderer.java b/Xplat/src/main/java/vazkii/neat/HealthBarRenderer.java index c4cea95..6b6bf5a 100644 --- a/Xplat/src/main/java/vazkii/neat/HealthBarRenderer.java +++ b/Xplat/src/main/java/vazkii/neat/HealthBarRenderer.java @@ -98,6 +98,8 @@ private static ItemStack getIcon(LivingEntity entity, boolean boss) { return new ItemStack(Items.SPIDER_EYE); } else if (type.is(EntityTypeTags.UNDEAD)) { return new ItemStack(Items.ROTTEN_FLESH); + } else if (type.is(EntityTypeTags.ILLAGER)) { + return new ItemStack(Items.IRON_AXE); } else { return ItemStack.EMPTY; } @@ -112,11 +114,9 @@ private static int getColor(LivingEntity entity, boolean colorByType, boolean bo r = 128; g = 0; b = 128; - } - if (entity instanceof Monster) { + } else if (!entity.getType().getCategory().isFriendly()) { r = 255; g = 0; - b = 0; } return 0xff000000 | r << 16 | g << 8 | b; } else { @@ -126,14 +126,11 @@ private static int getColor(LivingEntity entity, boolean colorByType, boolean bo } } - private static final TagKey> FORGE_BOSS_TAG = - TagKey.create(Registries.ENTITY_TYPE, ResourceLocation.fromNamespaceAndPath("forge", "bosses")); - - private static final TagKey> FABRIC_BOSS_TAG = + private static final TagKey> BOSS_TAG = TagKey.create(Registries.ENTITY_TYPE, ResourceLocation.fromNamespaceAndPath("c", "bosses")); private static boolean isBoss(Entity entity) { - return entity.getType().is(FORGE_BOSS_TAG) || entity.getType().is(FABRIC_BOSS_TAG); + return entity.getType().is(BOSS_TAG); } private static boolean shouldShowPlate(LivingEntity living, Entity cameraEntity) { @@ -152,7 +149,8 @@ private static boolean shouldShowPlate(LivingEntity living, Entity cameraEntity) float distance = living.distanceTo(cameraEntity); if (distance > NeatConfig.instance.maxDistance() - || !living.hasLineOfSight(cameraEntity)) { + || (distance > NeatConfig.instance.maxDistanceWithoutLineOfSight() + && !living.hasLineOfSight(cameraEntity))) { return false; } if (!NeatConfig.instance.showOnBosses() && isBoss(living)) { @@ -167,10 +165,34 @@ private static boolean shouldShowPlate(LivingEntity living, Entity cameraEntity) if (NeatConfig.instance.showOnlyFocused() && getEntityLookedAt(cameraEntity) != living) { return false; } + if (!NeatConfig.instance.showOnPassive() && living.getType().getCategory().isFriendly()) { + return false; + } + if (!NeatConfig.instance.showOnHostile() && (!living.getType().getCategory().isFriendly() && !isBoss(living))) { + return false; + } + + if (living.hasPassenger(cameraEntity)) { + return false; + } boolean visible = true; - if (cameraEntity instanceof Player cameraPlayer) { - visible = !living.isInvisibleTo(cameraPlayer); + if (cameraEntity instanceof Player cameraPlayer + && living.isInvisibleTo(cameraPlayer)) { + boolean wearingThings = false; + for (ItemStack armorSlot : living.getArmorSlots()) { + if (!armorSlot.isEmpty()) { + wearingThings = true; + } + } + for (ItemStack handSlot : living.getHandSlots()) { + if (!handSlot.isEmpty()) { + wearingThings = true; + } + } + if (!wearingThings) { + visible = false; + } } Team livingTeam = living.getTeam(); Team cameraTeam = cameraEntity.getTeam(); @@ -190,12 +212,12 @@ public static void hookRender(Entity entity, PoseStack poseStack, MultiBufferSou Quaternionf cameraOrientation) { final Minecraft mc = Minecraft.getInstance(); - if (!(entity instanceof LivingEntity living) || (!living.getPassengers().isEmpty() && living.getPassengers().get(0) instanceof LivingEntity)) { - // TODO handle mob stacks properly + if (!(entity instanceof LivingEntity living)) { return; } - if (!shouldShowPlate(living, mc.gameRenderer.getMainCamera().getEntity())) { + //This was previously mc.gameRenderer.getMainCamera().getEntity() but that caused an incompatibility with RealCamera + if (!shouldShowPlate(living, mc.cameraEntity)) { return; } @@ -226,6 +248,9 @@ public static void hookRender(Entity entity, PoseStack poseStack, MultiBufferSou if (NeatConfig.instance.drawBackground()) { float padding = NeatConfig.instance.backgroundPadding(); int bgHeight = NeatConfig.instance.backgroundHeight(); + if (!NeatConfig.instance.showEntityName()) { + bgHeight -= (int) 4F; + } builder.addVertex(poseStack.last().pose(), -halfSize - padding, -bgHeight, 0.01F).setColor(0, 0, 0, 60).setUv(0.0F, 0.0F).setLight(light); builder.addVertex(poseStack.last().pose(), -halfSize - padding, barHeight + padding, 0.01F).setColor(0, 0, 0, 60).setUv(0.0F, 0.5F).setLight(light); @@ -262,16 +287,18 @@ public static void hookRender(Entity entity, PoseStack poseStack, MultiBufferSou // Text { - final int white = 0xFFFFFF; + final int textColor = HexFormat.fromHexDigits(NeatConfig.instance.textColor()); final int black = 0; // Name { - poseStack.pushPose(); - poseStack.translate(-halfSize, -4.5F, 0F); - poseStack.scale(textScale, textScale, textScale); - mc.font.drawInBatch(name, 0, 0, white, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); - poseStack.popPose(); + if (NeatConfig.instance.showEntityName()) { + poseStack.pushPose(); + poseStack.translate(-halfSize, -4.5F, 0F); + poseStack.scale(textScale, textScale, textScale); + mc.font.drawInBatch(name, 0, 0, textColor, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); + poseStack.popPose(); + } } // Health values (and debug ID) @@ -285,19 +312,19 @@ public static void hookRender(Entity entity, PoseStack poseStack, MultiBufferSou if (NeatConfig.instance.showCurrentHP()) { String hpStr = HEALTH_FORMAT.format(living.getHealth()); - mc.font.drawInBatch(hpStr, 2, h, white, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); + mc.font.drawInBatch(hpStr, 2, h, textColor, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); } if (NeatConfig.instance.showMaxHP()) { String maxHpStr = ChatFormatting.BOLD + HEALTH_FORMAT.format(living.getMaxHealth()); - mc.font.drawInBatch(maxHpStr, (int) (halfSize / healthValueTextScale * 2) - mc.font.width(maxHpStr) - 2, h, white, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); + mc.font.drawInBatch(maxHpStr, (int) (halfSize / healthValueTextScale * 2) - mc.font.width(maxHpStr) - 2, h, textColor, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); } if (NeatConfig.instance.showPercentage()) { String percStr = (int) (100 * living.getHealth() / living.getMaxHealth()) + "%"; - mc.font.drawInBatch(percStr, (int) (halfSize / healthValueTextScale) - mc.font.width(percStr) / 2.0F, h, white, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); + mc.font.drawInBatch(percStr, (int) (halfSize / healthValueTextScale) - mc.font.width(percStr) / 2.0F, h, textColor, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); } if (NeatConfig.instance.enableDebugInfo() && mc.getDebugOverlay().showDebugScreen()) { var id = BuiltInRegistries.ENTITY_TYPE.getKey(living.getType()); - mc.font.drawInBatch("ID: \"" + id + "\"", 0, h + 16, white, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); + mc.font.drawInBatch("ID: \"" + id + "\"", 0, h + 16, textColor, false, poseStack.last().pose(), buffers, Font.DisplayMode.NORMAL, black, light); } poseStack.popPose(); } diff --git a/Xplat/src/main/java/vazkii/neat/NeatConfig.java b/Xplat/src/main/java/vazkii/neat/NeatConfig.java index 467b61c..fe39fd6 100644 --- a/Xplat/src/main/java/vazkii/neat/NeatConfig.java +++ b/Xplat/src/main/java/vazkii/neat/NeatConfig.java @@ -9,6 +9,7 @@ public class NeatConfig { public interface ConfigAccess { int maxDistance(); + int maxDistanceWithoutLineOfSight(); boolean renderInF1(); double heightAbove(); boolean drawBackground(); @@ -21,19 +22,24 @@ public interface ConfigAccess { boolean showArmor(); boolean groupArmor(); boolean colorByType(); + String textColor(); int hpTextHeight(); boolean showMaxHP(); boolean showCurrentHP(); boolean showPercentage(); + boolean showOnPassive(); + boolean showOnHostile(); boolean showOnPlayers(); boolean showOnBosses(); boolean showOnlyFocused(); boolean showFullHealth(); boolean enableDebugInfo(); + boolean showEntityName(); + boolean disableNameTag(); List blacklist(); } - public static final List DEFAULT_DISABLED = List.of("minecraft:shulker", "minecraft:armor_stand", "minecraft:cod", "minecraft:salmon", "minecraft:pufferfish", "minecraft:tropical_fish"); + public static final List DEFAULT_DISABLED = List.of("minecraft:shulker", "minecraft:armor_stand", "minecraft:cod", "minecraft:salmon", "minecraft:pufferfish", "minecraft:tropical_fish", "minecraft:tadpole"); public static ConfigAccess instance; } diff --git a/Xplat/src/main/java/vazkii/neat/mixin/EntityRendererMixin.java b/Xplat/src/main/java/vazkii/neat/mixin/EntityRendererMixin.java new file mode 100644 index 0000000..75a92fc --- /dev/null +++ b/Xplat/src/main/java/vazkii/neat/mixin/EntityRendererMixin.java @@ -0,0 +1,22 @@ +package vazkii.neat.mixin; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import vazkii.neat.NeatConfig; + +@Mixin(EntityRenderer.class) +public class EntityRendererMixin { + + @Inject(method = "render(Lnet/minecraft/world/entity/Entity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/EntityRenderer;renderNameTag(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/network/chat/Component;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;IF)V"), cancellable = true) + private void neat_disableNameTag(Entity $$0, float $$1, float $$2, PoseStack $$3, MultiBufferSource $$4, int $$5, CallbackInfo ci) { + if (NeatConfig.instance.disableNameTag()) { + ci.cancel(); + } + } +} diff --git a/Xplat/src/main/resources/neat.mixins.json b/Xplat/src/main/resources/neat.mixins.json index bf08bd7..8bad1fb 100644 --- a/Xplat/src/main/resources/neat.mixins.json +++ b/Xplat/src/main/resources/neat.mixins.json @@ -8,6 +8,7 @@ "client": [ "AccessorRenderType", "EntityRenderDispatcherMixin", + "EntityRendererMixin", "MinecraftMixin" ], "injectors": { diff --git a/gradle.properties b/gradle.properties index 52f312a..957aeb9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,6 @@ org.gradle.jvmargs=-Xmx2G \ group=vazkii.neat neoforge_version=21.1.1 mod_id=neat -build_number=37 +build_number=38 mod_name=Neat mc_version=1.21