Compare commits

...
Author SHA1 Message Date
slprime fab584b8c1 Merge branch 'master' into spawn-overlay-styles 2026-11-10 19:25:17 +02:00
OrderedSet 0fc657c9d9 Clarity pass 2026-11-08 11:35:23 -08:00
OrderedSet 0e6f01f84d Make vertical radius configurable 2026-11-08 11:20:42 -08:00
OrderedSet 1478f21eb4 Improve performance another 2x 2026-11-08 10:38:34 -08:00
Martin Robertz dab939acb7 Merge branch 'master' into spawn-overlay-styles 2026-11-08 19:11:37 +01:00
OrderedSet 4d63ed13dc Add imports for move 2026-11-08 08:47:57 -08:00
OrderedSet 76b62072ab Move Mob* files to moboverlay/ 2026-11-08 08:41:37 -08:00
OrderedSet 42c47e5a89 Align images to north 2026-11-08 01:15:28 -08:00
github-actions[bot]andGitHub GTNH Actions 8d8f8370fc Optimising images contained in spawn-overlay-styles for #1111 (#1112)
Co-authored-by: GitHub GTNH Actions <[email protected]>
2026-11-08 00:47:21 -08:00
OrderedSet 753fc0da3a Clarity 2026-11-08 00:41:29 -08:00
OrderedSet ef8cac9c39 Comment quality + 1 comment accuracy fix 2026-11-08 00:34:56 -08:00
OrderedSet aa8f1c8adb Clarity rename Mob Spawn -> Mob Overlay 2026-11-08 00:23:11 -08:00
OrderedSet b29494935d Swap to cleaner GTNH logo + pufferfish. And cleanup 2026-11-08 00:18:12 -08:00
OrderedSet d6789c44d4 Use IWorldAccess to get instant updates 2026-11-07 23:47:12 -08:00
OrderedSet d07059cb5a Optimize + spotless 2026-11-07 23:33:44 -08:00
OrderedSet b7dcd0fb3d Comment cleanup 2026-11-07 23:09:54 -08:00
OrderedSet d04718c5c6 Add light level overlay variants, set default to connected 2026-11-07 22:59:28 -08:00
11 changed files with 809 additions and 101 deletions

No files matched your search

@@ -17,6 +17,7 @@ import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.StatCollector;
import net.minecraft.world.storage.SaveFormatComparator;
import net.minecraftforge.common.MinecraftForge;
@@ -57,6 +58,8 @@ import codechicken.nei.config.OptionToggleButtonBoubs;
import codechicken.nei.config.OptionUtilities;
import codechicken.nei.config.preset.GuiPresetList;
import codechicken.nei.event.NEIConfigsLoadedEvent;
import codechicken.nei.moboverlay.MobOverlayScan;
import codechicken.nei.moboverlay.MobOverlayStyle;
import codechicken.nei.recipe.IRecipeHandler;
import codechicken.nei.recipe.InformationHandler;
import codechicken.nei.recipe.RecipeCatalysts;
@@ -442,6 +445,45 @@ public class NEIClientConfig {
tag.getTag("world.overlays.lock").getBooleanValue(true);
API.addOption(new OptionToggleButton("world.overlays.lock", true));
tag.getTag("world.overlays.mobOverlayStyle")
.setComment("Mark on each block of the mob spawn overlay: " + MobOverlayStyle.formatAllNames())
.getValue(MobOverlayStyle.DEFAULT.formatName());
API.addOption(new OptionCycled("world.overlays.mobOverlayStyle", MobOverlayStyle.values().length, true) {
@Override
public int value() {
return MobOverlayStyle.parseStyle(getTag().getValue()).ordinal();
}
@Override
public void setValue(int value) {
getTag().setValue(MobOverlayStyle.values()[value].formatName());
}
@Override
public String getButtonText() {
final MobOverlayStyle style = MobOverlayStyle.parseStyle(getTag().getValue());
return StatCollector.translateToLocal(style.getLangKey());
}
});
tag.getTag("world.overlays.mobOverlayRadius")
.setComment("Blocks scanned on each side of the player by the mob spawn overlay")
.getIntValue(MobOverlayScan.DEFAULT_RADIUS);
API.addOption(
new OptionIntegerField(
"world.overlays.mobOverlayRadius",
MobOverlayScan.MIN_RADIUS,
MobOverlayScan.MAX_RADIUS));
tag.getTag("world.overlays.mobOverlayVerticalRadius")
.setComment("Blocks scanned above and below the player by the mob spawn overlay")
.getIntValue(MobOverlayScan.DEFAULT_VERTICAL_RADIUS);
API.addOption(
new OptionIntegerField(
"world.overlays.mobOverlayVerticalRadius",
MobOverlayScan.MIN_VERTICAL_RADIUS,
MobOverlayScan.MAX_VERTICAL_RADIUS));
tag.getTag("inventory.itempanelScale").getIntValue(100);
ItemsPanelGrid.updateScale();
API.addOption(new OptionIntegerField("inventory.itempanelScale", 1, 1000) {
@@ -13,18 +13,21 @@ import net.minecraft.entity.EnumCreatureType;
import net.minecraft.entity.passive.EntityPig;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.EnumSkyBlock;
import net.minecraft.world.SpawnerAnimals;
import net.minecraft.world.World;
import net.minecraft.world.biome.BiomeGenBase;
import net.minecraft.world.chunk.Chunk;
import org.lwjgl.opengl.GL11;
import com.cardinalstar.cubicchunks.world.api.IMinMaxHeight;
import codechicken.lib.math.MathHelper;
import codechicken.nei.KeyManager.IKeyStateTracker;
import codechicken.nei.moboverlay.MobOverlayDisplayList;
import codechicken.nei.moboverlay.MobOverlayFace;
import codechicken.nei.moboverlay.MobOverlayScan;
import codechicken.nei.moboverlay.MobOverlayStyle;
import cpw.mods.fml.relauncher.ReflectionHelper;
public class WorldOverlayRenderer implements IKeyStateTracker {
@@ -32,8 +35,9 @@ public class WorldOverlayRenderer implements IKeyStateTracker {
public static int chunkOverlay = 0;
public static int mobOverlay = 0;
private static byte[] mobSpawnCache;
private static long mobOverlayUpdateTime;
private static final MobOverlayScan MOB_OVERLAY_SCAN = new MobOverlayScan();
private static final MobOverlayFace MARKED_FACE = new MobOverlayFace();
private static final MobOverlayDisplayList MOB_OVERLAY_LIST = new MobOverlayDisplayList();
public static void reset() {
mobOverlay = 0;
@@ -82,51 +86,6 @@ public class WorldOverlayRenderer implements IKeyStateTracker {
GL11.glPopAttrib();
}
private static void buildMobSpawnOverlay(Entity entity) {
if (mobSpawnCache == null) {
mobSpawnCache = new byte[33 * 33 * 33]; // 35kB
}
World world = entity.worldObj;
int worldHeight = world.getHeight();
int x1 = (int) entity.posX;
int z1 = (int) entity.posZ;
int y1 = (int) MathHelper.clip(entity.posY, 16, worldHeight - 16);
for (int i = 0; i <= 32; i++) {
int x = x1 - 16 + i;
for (int j = 0; j <= 32; j++) {
int z = z1 - 16 + j;
int bufIndex = (i * 33 + j) * 33;
Chunk chunk = world.getChunkFromBlockCoords(x, z);
BiomeGenBase biome = world.getBiomeGenForCoords(x, z);
if (biome.getSpawnableList(EnumCreatureType.monster).isEmpty() || biome.getSpawningChance() <= 0) {
mobSpawnCache[bufIndex] = -1;
continue;
}
int maxHeight = chunk.worldObj.getHeightValue(x, z);
for (int k = 0; k <= 32; k++) {
int y = y1 - 16 + k;
if (y >= worldHeight) {
mobSpawnCache[bufIndex + k] = -1;
continue;
}
if (y > maxHeight) {
mobSpawnCache[bufIndex + k] = -1;
break;
}
mobSpawnCache[bufIndex + k] = getSpawnMode(chunk, x, y, z);
}
}
}
}
private static double getOverlayYOffset(World world, int x, int y, int z) {
Block block = world.getBlock(x, y, z);
if (block instanceof BlockSnow) {
@@ -136,72 +95,97 @@ public class WorldOverlayRenderer implements IKeyStateTracker {
return 0.02;
}
/**
* The model view matrix is relative to ({@code intOffsetX}, {@code intOffsetY}, {@code intOffsetZ}), the camera
* position cast to {@code int}.
*/
private static void renderMobSpawnOverlay(Entity entity, int intOffsetX, int intOffsetY, int intOffsetZ) {
if (mobOverlay == 0) {
if (mobSpawnCache != null) {
mobSpawnCache = null;
}
MOB_OVERLAY_SCAN.clear();
MOB_OVERLAY_LIST.delete();
return;
}
long worldTime = entity.worldObj.getTotalWorldTime();
if (mobSpawnCache == null || mobOverlayUpdateTime != worldTime) {
mobOverlayUpdateTime = worldTime;
buildMobSpawnOverlay(entity);
MOB_OVERLAY_SCAN.update(
entity,
NEIClientConfig.getIntSetting("world.overlays.mobOverlayRadius"),
NEIClientConfig.getIntSetting("world.overlays.mobOverlayVerticalRadius"));
final MobOverlayStyle style = MobOverlayStyle
.parseStyle(NEIClientConfig.getSetting("world.overlays.mobOverlayStyle").getValue());
final long worldTicks = entity.worldObj.getTotalWorldTime();
if (MOB_OVERLAY_LIST.isCompileDue(MOB_OVERLAY_SCAN.getModesVersion(), style, worldTicks)) {
compileMobOverlayList(entity.worldObj, style, worldTicks);
}
GL11.glDisable(GL11.GL_TEXTURE_2D);
beginMobOverlayDraw(style);
MOB_OVERLAY_LIST.draw(intOffsetX, intOffsetY, intOffsetZ);
endMobOverlayDraw();
}
/**
* Add a marked face to {@link #MOB_OVERLAY_LIST} for each block of {@link #MOB_OVERLAY_SCAN} where mobs can spawn.
*/
private static void compileMobOverlayList(World world, MobOverlayStyle style, long worldTicks) {
MOB_OVERLAY_LIST.beginCompile(MOB_OVERLAY_SCAN, style, worldTicks);
final float alpha = style.computeAlpha();
final Tessellator tess = Tessellator.instance;
tess.startDrawing(style.glMode);
final int span = MOB_OVERLAY_SCAN.getSpan();
for (int i = 0; i < span; i++) {
for (int j = 0; j < span; j++) {
for (int k = 0; k < MOB_OVERLAY_SCAN.getHeight(); k++) {
if (MOB_OVERLAY_SCAN.readSpawnMode(i, k, j) == 0) continue;
final double yOffset = getOverlayYOffset(
world,
MOB_OVERLAY_SCAN.getMinX() + i,
MOB_OVERLAY_SCAN.getMinY() + k,
MOB_OVERLAY_SCAN.getMinZ() + j);
MARKED_FACE.moveTo(i, k + yOffset, j);
loadNeighbourModes(i, k, j);
MARKED_FACE.applyColor(style.colouring, alpha);
style.addVertices(MARKED_FACE);
}
}
}
tess.draw();
MOB_OVERLAY_LIST.endCompile();
}
private static void beginMobOverlayDraw(MobOverlayStyle style) {
final ResourceLocation texture = style.texture;
if (texture == null) {
GL11.glDisable(GL11.GL_TEXTURE_2D);
} else {
GL11.glEnable(GL11.GL_TEXTURE_2D);
Minecraft.getMinecraft().getTextureManager().bindTexture(texture);
}
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glDisable(GL11.GL_LIGHTING);
GL11.glLineWidth(1.5F);
// the outline of `MobOverlayStyle.CONNECTED` lies in the plane of the tint. With depth writes on, the depth
// test discards most of the outline
GL11.glDepthMask(false);
}
int curSpawnMode = 2;
World world = entity.worldObj;
int x1 = (int) entity.posX - intOffsetX;
int z1 = (int) entity.posZ - intOffsetZ;
int y1 = (int) MathHelper.clip(entity.posY, 16, world.getHeight() - 16) - intOffsetY;
final Tessellator tess = Tessellator.instance;
tess.startDrawing(GL11.GL_LINES);
tess.setColorOpaque(255, 0, 0);
for (int i = 0; i <= 32; i++) {
int x = x1 - 16 + i;
for (int j = 0; j <= 32; j++) {
int z = z1 - 16 + j;
int cacheIndex = (i * 33 + j) * 33;
for (int k = 0; k <= 32; k++) {
int y = y1 - 16 + k;
int spawnMode = mobSpawnCache[cacheIndex + k];
if (spawnMode == 0) {
continue;
} else if (spawnMode == -1) {
break;
}
if (spawnMode != curSpawnMode) {
if (spawnMode == 1) {
tess.setColorOpaque(255, 255, 0);
} else {
tess.setColorOpaque(255, 0, 0);
}
curSpawnMode = spawnMode;
}
double yOff = getOverlayYOffset(world, x + intOffsetX, y + intOffsetY, z + intOffsetZ);
tess.addVertex(x, y + yOff, z);
tess.addVertex(x + 1, y + yOff, z + 1);
tess.addVertex(x + 1, y + yOff, z);
tess.addVertex(x, y + yOff, z + 1);
}
private static void loadNeighbourModes(int i, int k, int j) {
final int span = MOB_OVERLAY_SCAN.getSpan();
for (int offsetX = -1; offsetX <= 1; offsetX++) {
for (int offsetZ = -1; offsetZ <= 1; offsetZ++) {
final int neighbourI = i + offsetX;
final int neighbourJ = j + offsetZ;
final boolean isInScan = neighbourI >= 0 && neighbourI < span && neighbourJ >= 0 && neighbourJ < span;
final byte spawnMode = isInScan ? MOB_OVERLAY_SCAN.readSpawnMode(neighbourI, k, neighbourJ)
: MobOverlayFace.OUTSIDE_SCAN;
MARKED_FACE.setNeighbourMode(offsetX, offsetZ, spawnMode);
}
}
}
tess.draw();
/** Undo the GL state changes of {@link #beginMobOverlayDraw}, except the texture binding. */
private static void endMobOverlayDraw() {
GL11.glDepthMask(true);
GL11.glEnable(GL11.GL_LIGHTING);
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glDisable(GL11.GL_BLEND);
@@ -210,7 +194,7 @@ public class WorldOverlayRenderer implements IKeyStateTracker {
private static final Entity dummyEntity = new EntityPig(null);
private static final AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox(0, 0, 0, 0, 0, 0);
private static byte getSpawnMode(Chunk chunk, int x, int y, int z) {
public static byte getSpawnMode(Chunk chunk, int x, int y, int z) {
if (y >= chunk.worldObj.getHeight()) {
return 0;
}
@@ -0,0 +1,65 @@
package codechicken.nei.moboverlay;
import net.minecraft.client.renderer.GLAllocation;
import org.lwjgl.opengl.GL11;
/** The mob spawn overlay quads, compiled into a GL display list. */
public final class MobOverlayDisplayList {
private int listId = -1;
private int modesVersion;
private MobOverlayStyle style;
private int minX;
private int minY;
private int minZ;
private long compiledTicks;
/**
* Compiles after a {@link MobOverlayScan#getModesVersion} change are debounced to one per {@code MIN_COMPILE_TICKS}
* ticks, because the version changes several times per second while the player moves fast, and a compile takes a
* millisecond or more.
*/
public boolean isCompileDue(int currentModesVersion, MobOverlayStyle currentStyle, long worldTicks) {
final int MIN_COMPILE_TICKS = 5;
if (listId < 0 || style != currentStyle) return true;
// after a dimension change or a server switch, `worldTicks` can be lower than `compiledTicks`
final boolean isWaitOver = worldTicks - compiledTicks >= MIN_COMPILE_TICKS || worldTicks < compiledTicks;
return modesVersion != currentModesVersion && isWaitOver;
}
/**
* Quad positions are relative to block ({@link MobOverlayScan#getMinX}, {@link MobOverlayScan#getMinY},
* {@link MobOverlayScan#getMinZ}) of {@code scan}.
*/
public void beginCompile(MobOverlayScan scan, MobOverlayStyle newStyle, long worldTicks) {
if (listId < 0) listId = GLAllocation.generateDisplayLists(1);
modesVersion = scan.getModesVersion();
style = newStyle;
minX = scan.getMinX();
minY = scan.getMinY();
minZ = scan.getMinZ();
compiledTicks = worldTicks;
GL11.glNewList(listId, GL11.GL_COMPILE);
}
public void endCompile() {
GL11.glEndList();
}
/** The model view matrix is relative to block ({@code cameraX}, {@code cameraY}, {@code cameraZ}). */
public void draw(int cameraX, int cameraY, int cameraZ) {
GL11.glPushMatrix();
GL11.glTranslated(minX - cameraX, minY - cameraY, minZ - cameraZ);
GL11.glCallList(listId);
GL11.glPopMatrix();
}
public void delete() {
if (listId < 0) return;
GLAllocation.deleteDisplayLists(listId);
listId = -1;
}
}
@@ -0,0 +1,88 @@
package codechicken.nei.moboverlay;
import net.minecraft.client.renderer.Tessellator;
/** Vertex builder for one block's top face in the mob spawn overlay. */
public final class MobOverlayFace {
public static final byte OUTSIDE_SCAN = -2;
private static final int NEIGHBOURHOOD_SIZE = 3;
private final byte[] neighbourModes = new byte[NEIGHBOURHOOD_SIZE * NEIGHBOURHOOD_SIZE];
private double x;
private double y;
private double z;
/** {@code (x, y, z)} is the north-west corner of the block's top face. */
public void moveTo(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public void setNeighbourMode(int offsetX, int offsetZ, byte spawnMode) {
neighbourModes[indexNeighbour(offsetX, offsetZ)] = spawnMode;
}
public void applyColor(MobOverlayStyle.Colouring colouring, float alpha) {
final boolean isNightOnly = neighbourModes[indexNeighbour(0, 0)] == MobOverlayScan.NIGHT_ONLY;
final int rgb = switch (colouring) {
case OVERLAY -> isNightOnly ? 0xFFFF00 : 0xFF0000;
case STICKER -> isNightOnly ? 0xFFFFFF : 0xFF7373;
};
Tessellator.instance.setColorRGBA_I(rgb, Math.round(alpha * 255));
}
public boolean isJoined(int offsetX, int offsetZ) {
final byte neighbourMode = neighbourModes[indexNeighbour(offsetX, offsetZ)];
// no outline along the edge of the scan
return neighbourMode == neighbourModes[indexNeighbour(0, 0)] || neighbourMode == OUTSIDE_SCAN;
}
public void addFullFace() {
addVertex(0, 0);
addVertex(0, 1);
addVertex(1, 1);
addVertex(1, 0);
}
/** Add one cell of a 3 x 3 grid over the face. The outer rows and columns of the grid are {@code width} wide. */
public void addCell(int offsetX, int offsetZ, double width) {
final double west = computeCellStart(offsetX, width);
final double east = 1 - computeCellStart(-offsetX, width);
final double north = computeCellStart(offsetZ, width);
final double south = 1 - computeCellStart(-offsetZ, width);
addVertex(west, north);
addVertex(west, south);
addVertex(east, south);
addVertex(east, north);
}
/** The texture is upright to a player facing north (-z). */
public void addTexturedFace() {
addTexturedVertex(0, 0);
addTexturedVertex(0, 1);
addTexturedVertex(1, 1);
addTexturedVertex(1, 0);
}
/** @param offsetX 0 at the west edge, 1 at the east edge */
public void addVertex(double offsetX, double offsetZ) {
Tessellator.instance.addVertex(x + offsetX, y, z + offsetZ);
}
private void addTexturedVertex(double offsetX, double offsetZ) {
Tessellator.instance.setTextureUV(offsetX, offsetZ);
addVertex(offsetX, offsetZ);
}
private static int indexNeighbour(int offsetX, int offsetZ) {
return (offsetX + 1) * NEIGHBOURHOOD_SIZE + offsetZ + 1;
}
private static double computeCellStart(int offset, double width) {
if (offset < 0) return 0;
return offset == 0 ? width : 1 - width;
}
}
@@ -0,0 +1,353 @@
package codechicken.nei.moboverlay;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EnumCreatureType;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.world.IWorldAccess;
import net.minecraft.world.World;
import net.minecraft.world.biome.BiomeGenBase;
import net.minecraft.world.chunk.Chunk;
import codechicken.lib.math.MathHelper;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.WorldOverlayRenderer;
/**
* Spawn modes in a box around the player, from {@link WorldOverlayRenderer#getSpawnMode}: 0 for no spawns, 1 for night
* only and 2 for any time. The box moves with the player once the player is a few blocks from the box's centre, and
* {@link #update} scans only the blocks that enter the box. {@link #update} rescans the column of a block or light
* change on the next tick. On each tick, {@link #update} also rescans a slice of the columns, for changes with no block
* update, such as a boat that moves into the box.
*/
public final class MobOverlayScan {
public static final byte NIGHT_ONLY = 1;
public static final int MIN_RADIUS = 8;
public static final int MAX_RADIUS = 64;
public static final int DEFAULT_RADIUS = 32;
public static final int MIN_VERTICAL_RADIUS = 4;
public static final int MAX_VERTICAL_RADIUS = 64;
public static final int DEFAULT_VERTICAL_RADIUS = 16;
private static final int RECENTRE_DISTANCE = 4;
private static final int VERTICAL_RECENTRE_DISTANCE = 2;
private static final int RESCAN_TICKS = 40;
private final IWorldAccess changeListener = new ChangeListener();
private final Column column = new Column();
private int radius;
private int verticalRadius;
private int span;
private int height;
// a ring buffer over the box, indexed by `findCell()`
private byte[] modes = new byte[0];
// indexed by `findColumn(x, z)`
private boolean[] dirty = new boolean[0];
private boolean hasDirtyColumns;
private int minX;
private int minY;
private int minZ;
// the slice rescan of `update()` continues from this column, numbered `(x - minX) * span + (z - minZ)`
private int nextRescanColumn;
private int modesVersion;
private boolean hasModes;
private long scannedTick;
// world of `modes`, with `changeListener` added
private World scannedWorld;
public void clear() {
if (scannedWorld != null) scannedWorld.removeWorldAccess(changeListener);
radius = 0;
verticalRadius = 0;
span = 0;
height = 0;
modes = new byte[0];
dirty = new boolean[0];
hasModes = false;
scannedWorld = null;
}
/**
* Scan every column in this call after {@link #clear}, a change of either radius, a world change, or a move of
* {@link #getSpan} blocks or more across or {@link #getHeight} blocks or more up or down. {@code newRadius} is
* clamped between {@link #MIN_RADIUS} and {@link #MAX_RADIUS}, and {@code newVerticalRadius} between
* {@link #MIN_VERTICAL_RADIUS} and {@link #MAX_VERTICAL_RADIUS}.
*/
public void update(Entity entity, int newRadius, int newVerticalRadius) {
final World world = entity.worldObj;
final int clampedRadius = (int) MathHelper.clip(newRadius, MIN_RADIUS, MAX_RADIUS);
final int clampedVerticalRadius = (int) MathHelper
.clip(newVerticalRadius, MIN_VERTICAL_RADIUS, MAX_VERTICAL_RADIUS);
if (radius != clampedRadius || verticalRadius != clampedVerticalRadius) {
resize(clampedRadius, clampedVerticalRadius);
}
final int centreX = MathHelper.floor_double(entity.posX);
final int centreY = (int) MathHelper.clip(entity.posY, verticalRadius, world.getHeight() - verticalRadius);
final int centreZ = MathHelper.floor_double(entity.posZ);
final boolean isNearBox = Math.abs(centreX - radius - minX) < span && Math.abs(centreZ - radius - minZ) < span
&& Math.abs(centreY - verticalRadius - minY) < height;
if (!(hasModes && world == scannedWorld && isNearBox)) {
if (world != scannedWorld) watchWorld(world);
scanFullBox(centreX, centreY, centreZ);
return;
}
final long worldTicks = world.getTotalWorldTime();
if (worldTicks == scannedTick) return;
scannedTick = worldTicks;
boolean isChanged = moveBox(centreX, centreY, centreZ);
if (hasDirtyColumns) isChanged |= rescanDirtyColumns();
isChanged |= rescanSlice();
if (isChanged) modesVersion++;
}
/** The version changes when a spawn mode changes or the box moves. */
public int getModesVersion() {
return modesVersion;
}
public int getSpan() {
return span;
}
public int getHeight() {
return height;
}
public int getMinX() {
return minX;
}
public int getMinY() {
return minY;
}
public int getMinZ() {
return minZ;
}
/** {@code i} and {@code j} are from 0 to {@link #getSpan}, and {@code k} is from 0 to {@link #getHeight}. */
public byte readSpawnMode(int i, int k, int j) {
return modes[findCell(minX + i, minY + k, minZ + j)];
}
private void resize(int newRadius, int newVerticalRadius) {
radius = newRadius;
verticalRadius = newVerticalRadius;
span = 2 * newRadius + 1;
height = 2 * newVerticalRadius + 1;
modes = new byte[span * span * height];
dirty = new boolean[span * span];
hasModes = false;
}
private void scanFullBox(int centreX, int centreY, int centreZ) {
final long startNanos = System.nanoTime();
minX = centreX - radius;
minY = centreY - verticalRadius;
minZ = centreZ - radius;
for (int x = minX; x < minX + span; x++) {
for (int z = minZ; z < minZ + span; z++) {
scanColumn(x, z);
}
}
hasModes = true;
modesVersion++;
scannedTick = scannedWorld.getTotalWorldTime();
NEIClientConfig.logger.info(
"moboverlay.scan.full radius={} verticalRadius={} columns={} micros={}",
radius,
verticalRadius,
span * span,
(System.nanoTime() - startNanos) / 1000);
}
private void watchWorld(World world) {
if (scannedWorld != null) scannedWorld.removeWorldAccess(changeListener);
world.addWorldAccess(changeListener);
scannedWorld = world;
}
/** @return whether the box moved */
private boolean moveBox(int centreX, int centreY, int centreZ) {
boolean isMoved = false;
final int offsetX = centreX - radius - minX;
if (Math.abs(offsetX) > RECENTRE_DISTANCE) {
final int enteringX = offsetX > 0 ? minX + span : minX + offsetX;
minX += offsetX;
for (int x = enteringX; x < enteringX + Math.abs(offsetX); x++) {
for (int z = minZ; z < minZ + span; z++) {
scanColumn(x, z);
}
}
isMoved = true;
}
final int offsetZ = centreZ - radius - minZ;
if (Math.abs(offsetZ) > RECENTRE_DISTANCE) {
final int enteringZ = offsetZ > 0 ? minZ + span : minZ + offsetZ;
minZ += offsetZ;
for (int x = minX; x < minX + span; x++) {
for (int z = enteringZ; z < enteringZ + Math.abs(offsetZ); z++) {
scanColumn(x, z);
}
}
isMoved = true;
}
final int offsetY = centreY - verticalRadius - minY;
if (Math.abs(offsetY) > VERTICAL_RECENTRE_DISTANCE) {
final int enteringY = offsetY > 0 ? minY + height : minY + offsetY;
minY += offsetY;
for (int x = minX; x < minX + span; x++) {
for (int z = minZ; z < minZ + span; z++) {
column.moveTo(x, z);
column.scan(enteringY, enteringY + Math.abs(offsetY));
}
}
isMoved = true;
}
return isMoved;
}
private boolean rescanDirtyColumns() {
hasDirtyColumns = false;
boolean isChanged = false;
for (int x = minX; x < minX + span; x++) {
for (int z = minZ; z < minZ + span; z++) {
if (dirty[findColumn(x, z)]) isChanged |= scanColumn(x, z);
}
}
return isChanged;
}
private boolean rescanSlice() {
final int columnCount = span * span;
final int sliceEnd = Math.min(columnCount, nextRescanColumn + (columnCount + RESCAN_TICKS - 1) / RESCAN_TICKS);
boolean isChanged = false;
for (; nextRescanColumn < sliceEnd; nextRescanColumn++) {
final int x = minX + nextRescanColumn / span;
final int z = minZ + nextRescanColumn % span;
isChanged |= scanColumn(x, z);
}
if (nextRescanColumn == columnCount) nextRescanColumn = 0;
return isChanged;
}
/** @return whether a spawn mode changed */
private boolean scanColumn(int x, int z) {
column.moveTo(x, z);
dirty[findColumn(x, z)] = false;
return column.scan(minY, minY + height);
}
private int findColumn(int x, int z) {
return Math.floorMod(x, span) * span + Math.floorMod(z, span);
}
private int findCell(int x, int y, int z) {
return findColumn(x, z) * height + Math.floorMod(y, height);
}
/**
* {@link #moveTo} reads the chunk, the height map and the biome of a column, and {@link #scan} reads the column's
* blocks.
*/
private final class Column {
private int x;
private int z;
private Chunk chunk;
private int topY;
private boolean canBiomeSpawn;
private void moveTo(int newX, int newZ) {
x = newX;
z = newZ;
final BiomeGenBase biome = scannedWorld.getBiomeGenForCoords(x, z);
canBiomeSpawn = !biome.getSpawnableList(EnumCreatureType.monster).isEmpty()
&& biome.getSpawningChance() > 0;
chunk = scannedWorld.getChunkFromBlockCoords(x, z);
topY = Math.min(scannedWorld.getHeight() - 1, scannedWorld.getHeightValue(x, z));
}
/** @return whether a spawn mode changed */
private boolean scan(int fromY, int toY) {
boolean isChanged = false;
for (int y = fromY; y < toY; y++) {
final byte spawnMode = canBiomeSpawn && y <= topY ? WorldOverlayRenderer.getSpawnMode(chunk, x, y, z)
: 0;
final int cell = findCell(x, y, z);
isChanged |= modes[cell] != spawnMode;
modes[cell] = spawnMode;
}
return isChanged;
}
}
/** Marks the columns of the block and light changes in {@link #scannedWorld}. */
private final class ChangeListener implements IWorldAccess {
@Override
public void markBlockForUpdate(int x, int y, int z) {
markChanged(x, y, z);
}
@Override
public void markBlockForRenderUpdate(int x, int y, int z) {
markChanged(x, y, z);
}
@Override
public void markBlockRangeForRenderUpdate(int fromX, int fromY, int fromZ, int toX, int toY, int toZ) {
for (int x = Math.max(fromX, minX); x <= Math.min(toX, minX + span - 1); x++) {
for (int z = Math.max(fromZ, minZ); z <= Math.min(toZ, minZ + span - 1); z++) {
markColumn(x, z);
}
}
}
@Override
public void playSound(String soundName, double x, double y, double z, float volume, float pitch) {}
@Override
public void playSoundToNearExcept(EntityPlayer player, String soundName, double x, double y, double z,
float volume, float pitch) {}
@Override
public void spawnParticle(String particleName, double x, double y, double z, double velocityX, double velocityY,
double velocityZ) {}
@Override
public void onEntityCreate(Entity entity) {}
@Override
public void onEntityDestroy(Entity entity) {}
@Override
public void playRecord(String recordName, int x, int y, int z) {}
@Override
public void broadcastSound(int soundId, int x, int y, int z, int data) {}
@Override
public void playAuxSFX(EntityPlayer player, int sfxType, int x, int y, int z, int data) {}
@Override
public void destroyBlockPartially(int breakerId, int x, int y, int z, int progress) {}
@Override
public void onStaticEntitiesChanged() {}
private void markChanged(int x, int y, int z) {
// spawn modes depend on the block below and the block above
final boolean isInBox = x >= minX && x < minX + span && z >= minZ && z < minZ + span;
if (isInBox && y >= minY - 1 && y <= minY + height) markColumn(x, z);
}
private void markColumn(int x, int z) {
dirty[findColumn(x, z)] = true;
hasDirtyColumns = true;
}
}
}
@@ -0,0 +1,154 @@
package codechicken.nei.moboverlay;
import java.util.Locale;
import java.util.StringJoiner;
import javax.annotation.Nullable;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.opengl.GL11;
/**
* Each style is a mark drawn on every block where mobs can spawn. {@code world.overlays.mobOverlayStyle} is saved as
* {@link #formatName()}.
*/
public enum MobOverlayStyle {
/** NEI's original cross from corner to corner. */
X(GL11.GL_LINES) {
@Override
public void addVertices(MobOverlayFace face) {
face.addVertex(0, 0);
face.addVertex(1, 1);
face.addVertex(1, 0);
face.addVertex(0, 1);
}
},
/**
* A light tint, with a stronger outline around each area of one spawn mode, like a connected texture.
*/
CONNECTED(GL11.GL_QUADS) {
@Override
public void addVertices(MobOverlayFace face) {
final double OUTLINE_WIDTH = 1.0 / 16;
face.addFullFace();
face.applyColor(Colouring.OVERLAY, MARK_ALPHA);
for (int offsetX = -1; offsetX <= 1; offsetX++) {
for (int offsetZ = -1; offsetZ <= 1; offsetZ++) {
// a corner cell is drawn when either edge next to the corner is outlined, and on an inside corner
final boolean isOutline = !(face.isJoined(offsetX, offsetZ) && face.isJoined(offsetX, 0)
&& face.isJoined(0, offsetZ));
if (isOutline) face.addCell(offsetX, offsetZ, OUTLINE_WIDTH);
}
}
}
},
/** A small square in the middle of the block, with no tint. */
SQUARE(GL11.GL_QUADS) {
@Override
public void addVertices(MobOverlayFace face) {
final double CENTRE = 0.5;
final double HALF_SIDE = 7.0 / 64;
face.addVertex(CENTRE - HALF_SIDE, CENTRE - HALF_SIDE);
face.addVertex(CENTRE - HALF_SIDE, CENTRE + HALF_SIDE);
face.addVertex(CENTRE + HALF_SIDE, CENTRE + HALF_SIDE);
face.addVertex(CENTRE + HALF_SIDE, CENTRE - HALF_SIDE);
}
},
/** The GTNH logo without the globe. */
GTNH_LOGO("gtnh_logo", Colouring.OVERLAY) {
@Override
public void addVertices(MobOverlayFace face) {
face.addTexturedFace();
}
},
/** The pufferfish sticker. */
PUFFERFISH("pufferfish", Colouring.STICKER) {
@Override
public void addVertices(MobOverlayFace face) {
face.addTexturedFace();
}
};
public static final MobOverlayStyle DEFAULT = CONNECTED;
private static final float TINT_ALPHA = 0.15F;
// the square and the outline cover a small part of the block and need more opacity than the tint to stay visible
private static final float MARK_ALPHA = 0.45F;
public final int glMode;
@Nullable
public final ResourceLocation texture;
public final Colouring colouring;
MobOverlayStyle(int glMode) {
this.glMode = glMode;
this.texture = null;
this.colouring = Colouring.OVERLAY;
}
MobOverlayStyle(String textureName, Colouring colouring) {
this.glMode = GL11.GL_QUADS;
this.texture = new ResourceLocation("nei", "textures/overlay/" + textureName + ".png");
this.colouring = colouring;
}
/** Case-insensitive. Returns {@link #DEFAULT} for an unknown name. */
public static MobOverlayStyle parseStyle(String rawStyle) {
for (MobOverlayStyle candidate : values()) {
if (candidate.formatName().equalsIgnoreCase(rawStyle)) return candidate;
}
return DEFAULT;
}
/** @return the {@link #formatName} of every style, separated by commas */
public static String formatAllNames() {
final StringJoiner names = new StringJoiner(", ");
for (MobOverlayStyle style : values()) {
names.add(style.formatName());
}
return names.toString();
}
public String formatName() {
return name().toLowerCase(Locale.ROOT);
}
public String getLangKey() {
return switch (this) {
case X -> "nei.options.world.overlays.mobOverlayStyle.x";
case CONNECTED -> "nei.options.world.overlays.mobOverlayStyle.connected";
case SQUARE -> "nei.options.world.overlays.mobOverlayStyle.square";
case GTNH_LOGO -> "nei.options.world.overlays.mobOverlayStyle.gtnh_logo";
case PUFFERFISH -> "nei.options.world.overlays.mobOverlayStyle.pufferfish";
};
}
public float computeAlpha() {
return switch (this) {
case CONNECTED -> TINT_ALPHA;
case SQUARE -> MARK_ALPHA;
// the alpha is in the PNGs
case X, GTNH_LOGO, PUFFERFISH -> 1.0F;
};
}
public abstract void addVertices(MobOverlayFace face);
public enum Colouring {
/** Yellow where mobs spawn only at night, red where mobs spawn at any time. */
OVERLAY,
/** The texture's colours, tinted red where mobs spawn at any time. */
STICKER
}
}
@@ -566,6 +566,16 @@ nei.options.world.overlays=Overlays Settings
nei.options.world.overlays.lock=Lock overlays between dimensions
nei.options.world.overlays.lock.true=Enabled
nei.options.world.overlays.lock.false=Disabled
nei.options.world.overlays.mobOverlayStyle=Mob Spawn Overlay Style
nei.options.world.overlays.mobOverlayStyle.x=X
nei.options.world.overlays.mobOverlayStyle.connected=Connected
nei.options.world.overlays.mobOverlayStyle.square=Square
nei.options.world.overlays.mobOverlayStyle.gtnh_logo=GTNH Logo
nei.options.world.overlays.mobOverlayStyle.pufferfish=Pufferfish
nei.options.world.overlays.mobOverlayRadius=Mob Spawn Overlay Horizontal Radius
nei.options.world.overlays.mobOverlayRadius.tip=Horizontal block radius scanned around you. Range 8-64 blocks
nei.options.world.overlays.mobOverlayVerticalRadius=Mob Spawn Overlay Vertical Radius
nei.options.world.overlays.mobOverlayVerticalRadius.tip=Blocks scanned above and below you. Range 4-64 blocks
nei.options.tools=Tools
nei.options.tools.export_configs=Default Config Files
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,6 @@
{
"texture": {
"blur": true,
"clamp": true
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,6 @@
{
"texture": {
"blur": false,
"clamp": true
}
}