diff --git a/docs/roadmap.md b/docs/roadmap.md index 280a35c..975902a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -6,20 +6,24 @@ - [x] `NullPointerException` when a scanner is active and world disappears (bug: #21, pr: #25) - [ ] clumps of blocks (bug for internal walls: #22, issue: #23, pr: ?) - [ ] bounds for frustum - - [ ] walls no longer flicker - - [ ] focused clump has an outline (at minimum, `DEBUG_LINES`) — so we have external vertices for camera distance calculation + - [x] walls no longer flicker + - [x] focused clump has an outline (at minimum, `DEBUG_LINES`) — ... + - [ ] ... so we have external vertices for camera distance calculation ## Bugs -- [ ] Settings - - [ ] only one setting at a time is saved +- [x] Settings + - [x] only one setting at a time is saved -- [ ] Scanner - - [ ] internal walls tend to flicker in and out of existence - - [ ] closing app / disconnecting from server ends with a `NullPointerException` if the scanner is active +- [x] Scanner + - [x] internal walls tend to flicker in and out of existence + - [x] closing app / disconnecting from server ends with a `NullPointerException` if the scanner is active ## Features +- [ ] Settings + - [ ] per-world saves on top of common defaults + - [ ] _end of minimal viable code_ - [ ] Scanner - [ ] clumps of blocks - [ ] label the clumps diff --git a/fabric/any/src/main/java/com/midnightbits/scanner/fabric/FabricAnimationHost.java b/fabric/any/src/main/java/com/midnightbits/scanner/fabric/FabricAnimationHost.java index 1bcea2b..7dd64f6 100644 --- a/fabric/any/src/main/java/com/midnightbits/scanner/fabric/FabricAnimationHost.java +++ b/fabric/any/src/main/java/com/midnightbits/scanner/fabric/FabricAnimationHost.java @@ -28,7 +28,8 @@ public void initialize(Sonar source) { return; this.tick(Clock.currentTimeMillis()); - this.source.remove(this.source.oldEchoes(new MinecraftClientCore(client))); + if (this.source.remove(this.source.oldEchoes(new MinecraftClientCore(client)))) + this.source.splitToNuggets(); }); WorldRenderEvents.LAST.register(this::renderLevel); } @@ -45,6 +46,6 @@ public void apply(List shimmers) { private void renderLevel(WorldRenderContext context) { final var shimmers = new GatherShimmers(); this.run(shimmers); - Pixel.renderLevel(context, source.echoes(), shimmers.cloud); + Pixels.renderLevel(context, source.nuggets(), shimmers.cloud); } } diff --git a/fabric/any/src/main/java/com/midnightbits/scanner/fabric/Mesh.java b/fabric/any/src/main/java/com/midnightbits/scanner/fabric/Mesh.java deleted file mode 100644 index c75ed22..0000000 --- a/fabric/any/src/main/java/com/midnightbits/scanner/fabric/Mesh.java +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2024 Marcin Zdun -// This code is licensed under MIT license (see LICENSE for details) - -package com.midnightbits.scanner.fabric; - -import com.midnightbits.scanner.rt.math.V3i; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -class Mesh { - public static void cleanPixels(Collection pixels) { - final Map vertices = new HashMap<>(); - - for (final var pixel : pixels) { - final var control = vertices.get(pixel.position()); - if (control != null && control != pixel) { - pixel.sides = 0; - return; - } - - vertices.put(pixel.position(), pixel); - } - - final var positions = Set.copyOf(vertices.keySet()); - for (final var pos: positions) { - final var pixel = vertices.get(pos); - if (pixel == null) { continue; } - - final var x0 = vertices.get(pixel.position().add(-1, 0, 0)); - final var x1 = vertices.get(pixel.position().add(1, 0, 0)); - final var y0 = vertices.get(pixel.position().add(0, -1, 0)); - final var y1 = vertices.get(pixel.position().add(0, 1, 0)); - final var z0 = vertices.get(pixel.position().add(0, 0, -1)); - final var z1 = vertices.get(pixel.position().add(0, 0, 1)); - - cleanSide(pixel, x0, Pixel.SIDE_X0, Pixel.SIDE_X1); - cleanSide(pixel, x1, Pixel.SIDE_X1, Pixel.SIDE_X0); - cleanSide(pixel, y0, Pixel.SIDE_Y0, Pixel.SIDE_Y1); - cleanSide(pixel, y1, Pixel.SIDE_Y1, Pixel.SIDE_Y0); - cleanSide(pixel, z0, Pixel.SIDE_Z0, Pixel.SIDE_Z1); - cleanSide(pixel, z1, Pixel.SIDE_Z1, Pixel.SIDE_Z0); - - vertices.remove(pos); - } - } - - private static void cleanSide(Pixel pixel, Pixel neighbour, int mySide, int theirSide) { - if (neighbour == null || neighbour.argb != pixel.argb) { - return; - } - pixel.sides &= ~mySide; - neighbour.sides &= ~theirSide; - } -} diff --git a/fabric/any/src/main/java/com/midnightbits/scanner/fabric/Pixel.java b/fabric/any/src/main/java/com/midnightbits/scanner/fabric/Pixel.java deleted file mode 100644 index bb49319..0000000 --- a/fabric/any/src/main/java/com/midnightbits/scanner/fabric/Pixel.java +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2024 Marcin Zdun -// This code is licensed under MIT license (see LICENSE for details) - -package com.midnightbits.scanner.fabric; - -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import com.midnightbits.scanner.rt.core.Id; -import com.midnightbits.scanner.rt.core.fabric.Minecraft; -import org.joml.Matrix4f; - -import com.midnightbits.scanner.rt.math.V3i; -import com.midnightbits.scanner.sonar.BlockEcho; -import com.midnightbits.scanner.sonar.graphics.Shimmers; -import com.mojang.blaze3d.platform.GlStateManager; -import com.mojang.blaze3d.systems.RenderSystem; - -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; -import net.minecraft.client.render.BufferRenderer; -import net.minecraft.client.render.Camera; -import net.minecraft.client.render.GameRenderer; -import net.minecraft.client.render.Tessellator; -import net.minecraft.client.render.VertexConsumer; -import net.minecraft.client.render.VertexFormat; -import net.minecraft.client.render.VertexFormats; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Box; -import net.minecraft.util.math.ColorHelper; -import net.minecraft.util.math.Vec3d; - -public class Pixel { - private record Vertex(int dx, int dy, int dz) { - void apply(VertexConsumer buffer, Matrix4f matrix, int argb) { - buffer.vertex(matrix, dx, dy, dz).color(argb); - } - } - - private static final Vertex v000 = new Vertex(0, 0, 0); - private static final Vertex v001 = new Vertex(0, 0, 1); - private static final Vertex v010 = new Vertex(0, 1, 0); - private static final Vertex v011 = new Vertex(0, 1, 1); - private static final Vertex v100 = new Vertex(1, 0, 0); - private static final Vertex v101 = new Vertex(1, 0, 1); - private static final Vertex v110 = new Vertex(1, 1, 0); - private static final Vertex v111 = new Vertex(1, 1, 1); - - private static final Vertex[][] triangles = new Vertex[][] { - new Vertex[] { v000, v100, v010, v100, v110, v010 }, - new Vertex[] { v001, v101, v000, v101, v100, v000 }, - new Vertex[] { v011, v111, v001, v111, v101, v001 }, - new Vertex[] { v010, v110, v011, v110, v111, v011 }, - new Vertex[] { v001, v000, v011, v000, v010, v011 }, - new Vertex[] { v100, v101, v110, v101, v111, v110 }, - }; - - final static int SIDE_X0 = 1 << 4; - final static int SIDE_X1 = 1 << 5; - final static int SIDE_Y0 = 1 << 1; - final static int SIDE_Y1 = 1 << 3; - final static int SIDE_Z0 = 1; - final static int SIDE_Z1 = 1 << 2; - final static int ALL_SIDES = SIDE_X0 | SIDE_X1 | SIDE_Y0 | SIDE_Y1 | SIDE_Z0 | SIDE_Z1; - - private static final Id EMPTY_ID = Id.of("", ""); - - final V3i position; - final Id id; - final int argb; - int sides = ALL_SIDES; - final double distanceSquared; - - Pixel(V3i position, Id id, int argb, Vec3d camera) { - this.position = position; - this.id = id; - this.argb = argb; - this.distanceSquared = new BlockPos(Minecraft.vec3iOf(position)).toCenterPos().squaredDistanceTo(camera); - } - - public V3i position() { - return position; - } - - static Pixel of(BlockEcho echo, Vec3d camera) { - return new Pixel(echo.position(), echo.id(), echo.argb32(), camera); - } - - void draw(VertexConsumer buffer, MatrixStack matrices, Camera camera) { - final var cam = camera.getPos(); - final var x = position.getX() - cam.x; - final var y = position.getY() - cam.y; - final var z = position.getZ() - cam.z; - matrices.push(); - matrices.translate(x, y, z); - final var m = matrices.peek().getPositionMatrix(); - - for (var side = 0; side < triangles.length; ++side) { - final var flag = 1 << side; - if ((sides & flag) == 0) { - continue; - } - - final var wall = triangles[side]; - for (final var vertex : wall) { - vertex.apply(buffer, m, argb); - } - } - - matrices.pop(); - } - - public static void renderLevel(WorldRenderContext context, Iterable echoes, List shimmers) { - final var frustum = context.frustum(); - assert frustum != null; - - final var camera = context.camera(); - final var cameraPosF = camera.getPos(); - - final var allPixels = StreamSupport - .stream(echoes.spliterator(), false) - .map((echo) -> Pixel.of(echo, cameraPosF)) - .collect(Collectors.toSet()); - - for (final var shimmer : shimmers) { - final var alpha = (int) (shimmer.alpha() * 255.0 / 8.0 + .5); - int argbWalls = ColorHelper.Argb.withAlpha(alpha, 0x8080FF); - - for (final var pos : shimmer.blocks()) { - allPixels.add(new Pixel(pos, EMPTY_ID, argbWalls, cameraPosF)); - } - } - - Mesh.cleanPixels(allPixels); - - final var visiblePixels = new java.util.ArrayList<>(allPixels - .stream() - .filter((pixel) -> { - if ((pixel.sides & Pixel.ALL_SIDES) == 0) { - return false; - } - final var pos = pixel.position(); - final var box = new Box(pos.getX(), pos.getY(), pos.getZ(), (pos.getX() + 1), (pos.getY() + 1), - (pos.getZ() + 1)); - return frustum.isVisible(box); - }) - .toList()); - visiblePixels.sort((lhs, rhs) -> Double.compare(rhs.distanceSquared, lhs.distanceSquared)); - - if (visiblePixels.isEmpty() && shimmers.isEmpty()) { - return; - } - - final var matrices = context.matrixStack(); - assert matrices != null; - - RenderSystem.disableDepthTest(); - RenderSystem.enableBlend(); - RenderSystem.blendFunc( - GlStateManager.SrcFactor.SRC_ALPHA, - GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA); - RenderSystem.setShader(GameRenderer::getPositionColorProgram); - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - - final var tessellator = Tessellator.getInstance(); - final var buffer = tessellator.begin(VertexFormat.DrawMode.TRIANGLES, VertexFormats.POSITION_COLOR); - - for (final var pixel : visiblePixels) { - pixel.draw(buffer, matrices, camera); - } - - final var end = buffer.endNullable(); - if (end != null) - BufferRenderer.drawWithGlobalProgram(end); - - RenderSystem.enableDepthTest(); - } -} diff --git a/fabric/any/src/main/java/com/midnightbits/scanner/fabric/Pixels.java b/fabric/any/src/main/java/com/midnightbits/scanner/fabric/Pixels.java new file mode 100644 index 0000000..2c65dd8 --- /dev/null +++ b/fabric/any/src/main/java/com/midnightbits/scanner/fabric/Pixels.java @@ -0,0 +1,103 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.fabric; + +import java.util.List; + +import com.midnightbits.scanner.sonar.EchoNugget; +import com.midnightbits.scanner.sonar.graphics.*; +import net.minecraft.client.render.*; +import net.minecraft.util.math.Box; +import org.joml.Matrix4f; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; + +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; + +public class Pixels { + + private record GlProgramVertexConsumer(VertexConsumer buffer) implements GlProgramConsumer { + @Override + public void vertexColor(Matrix4f matrix, float x, float y, float z, int argb32) { + buffer.vertex(matrix, x, y, z).color(argb32); + } + } + + public static void renderLevel(WorldRenderContext context, List nuggets, List shimmers) { + final var frustum = context.frustum(); + if (frustum == null) { + return; + } + + final var contextMatrices = context.matrixStack(); + if (contextMatrices == null) { + return; + } + + final var matrices = new MatrixStack(contextMatrices.peek().getPositionMatrix()); + + final var camera = context.camera(); + final var cameraPos = camera.getPos().toVector3f(); + + final var frustumFilter = new FrustumFilter() { + @Override + public boolean contains(EchoState.AABB bounds) { + return frustum.isVisible( + new Box(bounds.minX(), bounds.minY(), bounds.minZ(), + bounds.maxX(), bounds.maxY(), bounds.maxZ())); + } + }; + + final var allShimmers = EchoNugget.group(Shimmers.toEchoStates(shimmers, .5)); + + final var visibleNuggets = EchoNugget.filterVisible(nuggets, frustumFilter); + final var visibleShimmers = EchoNugget.filterVisible(allShimmers, frustumFilter); + + if (visibleNuggets.isEmpty() && visibleShimmers.isEmpty()) { + return; + } + + RenderSystem.disableDepthTest(); + RenderSystem.enableBlend(); + RenderSystem.blendFunc( + GlStateManager.SrcFactor.SRC_ALPHA, + GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA); + RenderSystem.setShader(GameRenderer::getPositionColorProgram); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + + final var tessellator = Tessellator.getInstance(); + { + final var buffer = tessellator.begin(VertexFormat.DrawMode.TRIANGLES, VertexFormats.POSITION_COLOR); + final var glProgram = new GlProgramVertexConsumer(buffer); + + for (final var nugget : visibleShimmers) { + nugget.draw(glProgram, matrices, cameraPos); + } + + for (final var nugget: visibleNuggets) { + nugget.draw(glProgram, matrices, cameraPos); + } + + final var builtBuffer = buffer.endNullable(); + if (builtBuffer != null) + BufferRenderer.drawWithGlobalProgram(builtBuffer); + } + + { + final var buffer = tessellator.begin(VertexFormat.DrawMode.DEBUG_LINES, VertexFormats.POSITION_COLOR); + final var glProgram = new GlProgramVertexConsumer(buffer); + + for (final var nugget: visibleNuggets) { + nugget.sketch(glProgram, matrices, cameraPos); + } + + final var builtBuffer = buffer.endNullable(); + if (builtBuffer != null) + BufferRenderer.drawWithGlobalProgram(builtBuffer); + } + + RenderSystem.enableDepthTest(); + } +} diff --git a/scanner/src/main/java/com/midnightbits/scanner/rt/core/Id.java b/scanner/src/main/java/com/midnightbits/scanner/rt/core/Id.java index aa3c342..8302302 100644 --- a/scanner/src/main/java/com/midnightbits/scanner/rt/core/Id.java +++ b/scanner/src/main/java/com/midnightbits/scanner/rt/core/Id.java @@ -8,6 +8,7 @@ public class Id { public static final char NAMESPACE_SEPARATOR = ':'; public static final String DEFAULT_NAMESPACE = "minecraft"; + public static final String MOD_NAMESPACE = "resource-scanner"; public static final String REALMS_NAMESPACE = "realms"; private final String namespace; private final String path; @@ -35,6 +36,10 @@ public static Id ofVanilla(String path) { return new Id(DEFAULT_NAMESPACE, validatePath(DEFAULT_NAMESPACE, path)); } + public static Id ofMod(String path) { + return new Id(MOD_NAMESPACE, validatePath(MOD_NAMESPACE, path)); + } + public static Id tryParse(String id) { return trySplitOn(id, NAMESPACE_SEPARATOR); } diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/BlockEcho.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/BlockEcho.java index 803ece2..33d9d31 100644 --- a/scanner/src/main/java/com/midnightbits/scanner/sonar/BlockEcho.java +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/BlockEcho.java @@ -23,10 +23,6 @@ public Id id() { return echo.id(); } - public Colors.Proxy color() { - return echo.color(); - } - public boolean equals(@NotNull Object obj) { if (!(obj instanceof Partial other)) { throw new ClassCastException(); diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/BlockEchoes.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/BlockEchoes.java index 4548acc..23169a2 100644 --- a/scanner/src/main/java/com/midnightbits/scanner/sonar/BlockEchoes.java +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/BlockEchoes.java @@ -17,6 +17,7 @@ public final class BlockEchoes implements Iterable { private final TreeSet echoes = new TreeSet<>(); + private List nuggets = List.of(); public static final int ECHO_LIFETIME = 10000; private int lifetime; @@ -36,8 +37,10 @@ public void refresh(int lifetime) { /** * Adds pinged block to list of all seen blocks. * - * @param position where the ping was registered - * @param id what did the echo bounced off of + * @param x axis of where the ping was registered + * @param y axis of where the ping was registered + * @param z axis of where the ping was registered + * @param echo what did the echo bounced off of * @return resulting block */ public BlockEcho echoFrom(int x, int y, int z, Echo echo) { @@ -56,14 +59,11 @@ public BlockEcho echoFrom(BlockEcho.Partial partial) { BlockEcho echo = BlockEcho.echoFrom(partial); echoes.add(echo); - splitToNuggets(); return echo; } - public void remove(Predicate whichOnes) { - if (evictBlocks(stream().filter(whichOnes))) { - splitToNuggets(); - } + public boolean remove(Predicate whichOnes) { + return evictBlocks(stream().filter(whichOnes)); } public Predicate oldEchoes(ClientCore client) { @@ -82,6 +82,14 @@ public Predicate oldEchoes(ClientCore client) { }); } + List nuggets() { + return nuggets; + } + + public void splitToNuggets() { + nuggets = EchoNugget.group(echoes); + } + private Stream stream() { return echoes.stream(); } @@ -95,9 +103,6 @@ private boolean evictBlocks(Stream stream) { return oldSize != echoes.size(); } - private void splitToNuggets() { - } - @NotNull @Override public Iterator iterator() { diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/Echo.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/Echo.java index fb6a4de..f536a6f 100644 --- a/scanner/src/main/java/com/midnightbits/scanner/sonar/Echo.java +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/Echo.java @@ -10,15 +10,13 @@ import java.util.Objects; public record Echo(Id id, Colors.Proxy color) implements Comparable { + public static Echo of(Id id, Colors.Proxy color) { return new Echo(id, color); } @Override - public boolean equals(Object obj) { - if (obj == null) { - throw new NullPointerException(); - } + public boolean equals(@NotNull Object obj) { if (!(obj instanceof Echo other)) { throw new ClassCastException(); } @@ -30,7 +28,7 @@ public boolean equals(Object obj) { @Override public String toString() { - return "new Echo("+ _idOf(id) + ", " + _colorOf(color) + ")"; + return "new Echo(" + _idOf(id) + ", " + _colorOf(color) + ")"; } private static String _idOf(Id id) { @@ -46,7 +44,7 @@ private static String _idOf(Id id) { } private static String _colorOf(Colors.Proxy color) { - for (final var entry: Colors.BLOCK_TAG_COLORS.entrySet()) { + for (final var entry : Colors.BLOCK_TAG_COLORS.entrySet()) { if (color.equals(entry.getValue())) { return "Colors.BLOCK_TAG_COLORS.get(" + _idOf(entry.getKey()) + ")"; } diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/EchoNugget.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/EchoNugget.java new file mode 100644 index 0000000..4c42a87 --- /dev/null +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/EchoNugget.java @@ -0,0 +1,156 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar; + +import com.midnightbits.scanner.rt.core.Id; +import com.midnightbits.scanner.rt.math.V3i; +import com.midnightbits.scanner.sonar.graphics.*; +import org.jetbrains.annotations.NotNull; +import org.joml.Vector3f; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public class EchoNugget { + private final Map echoStates; + public final Id id; + private final EchoState.AABB bounds; + + private EchoNugget(Id id, Map echoStates) { + this.echoStates = echoStates; + this.id = id; + this.bounds = makeBounds(echoStates); + } + + public void draw(GlProgramConsumer buffer, MatrixStack matrices, Vector3f camera) { + furthestToClosest(buffer, matrices, camera, EchoState::draw); + } + + public void sketch(GlProgramConsumer buffer, MatrixStack matrices, Vector3f camera) { + furthestToClosest(buffer, matrices, camera, EchoState::sketch); + } + + public EchoState.AABB getBounds() { + return bounds; + } + + private static EchoState.AABB makeBounds(Map echoStates) { + var bounds = new EchoState.AABB(0, 0, 0, 0, 0, 0); + + final var iter = echoStates.values().iterator(); + if (iter.hasNext()) { + bounds = iter.next().getBounds(); + } + + while (iter.hasNext()) { + bounds.expand(iter.next().getBounds()); + } + + return bounds; + } + + private interface DrawingFunction { + void apply(EchoState self, GlProgramConsumer buffer, MatrixStack matrices, Vector3f camera); + }; + + private void furthestToClosest(GlProgramConsumer buffer, MatrixStack matrices, Vector3f camera, + DrawingFunction fn) { + furthestToClosest(echoStates.values().stream(), buffer, matrices, camera, fn); + } + + private static void furthestToClosest(Stream echoes, GlProgramConsumer buffer, MatrixStack matrices, Vector3f camera, + DrawingFunction fn) { + final Consumer paint = (pixel) -> fn.apply(pixel.echoState(), buffer, matrices, camera); + + echoes + .map((echo) -> Pixel.of(echo, camera)) + .sorted((lhs, rhs) -> Double.compare(rhs.distanceSquared(), lhs.distanceSquared())) + .forEach(paint); + } + + private List minMaxTo(Vector3f camera) { + final var sorted = echoStates.values().stream() + .map((echo) -> Pixel.of(echo, camera).distanceSquared()) + .sorted((lhs, rhs) -> Double.compare(rhs, lhs)).iterator(); + + final var closest = sorted.next(); + var furthest = closest; + while (sorted.hasNext()) { + furthest = sorted.next(); + } + return List.of(closest, furthest); + } + + private record Distance(EchoNugget nugget, List minMax) implements Comparable { + @Override + public int compareTo(@NotNull Distance other) { + final var lhsMin = minMax.getFirst(); + final var rhsMin = other.minMax.getFirst(); + return Double.compare(rhsMin, lhsMin); + } + } + + public static List sortForCamera(List nuggets, Vector3f camera) { + return nuggets.stream() + .map((nugget) -> new Distance(nugget, nugget.minMaxTo(camera))) + .sorted() + .map((dist) -> dist.nugget) + .toList(); + } + + public static List group(Collection echoes) { + final var result = new ArrayList(); + final var sorter = new TriColorSorter(echoes); + + for (final var group : sorter) { + final var id = group.entrySet().iterator().next().getValue().id(); + result.add(new EchoNugget(id, group)); + } + + return result; + } + + public static List group(Stream echoes) { + final var result = new ArrayList(); + final var sorter = new TriColorSorter(echoes); + + for (final var group : sorter) { + final var id = group.entrySet().iterator().next().getValue().id(); + result.add(new EchoNugget(id, group)); + } + + return result; + } + + public static List filterVisible(List nuggets, FrustumFilter frustum) { + return nuggets.stream() + .filter(frustum::contains) + .map(nugget -> { + final var filtered = nugget.echoStates.values().stream().filter(frustum::contains).toList(); + return nugget.new View(filtered); + }) + .toList(); + } + + public class View { + private final List echoes; + + View(List echoes) { + this.echoes = echoes; + } + + EchoNugget parent() { + return EchoNugget.this; + } + + public void draw(GlProgramConsumer buffer, MatrixStack matrices, Vector3f camera) { + furthestToClosest(echoes.stream(), buffer, matrices, camera, EchoState::draw); + } + + public void sketch(GlProgramConsumer buffer, MatrixStack matrices, Vector3f camera) { + furthestToClosest(echoes.stream(), buffer, matrices, camera, EchoState::sketch); + } + } +} diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/Sonar.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/Sonar.java index 88ca242..f4f7e21 100644 --- a/scanner/src/main/java/com/midnightbits/scanner/sonar/Sonar.java +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/Sonar.java @@ -104,12 +104,20 @@ public void echoFrom(BlockEcho.Partial partial) { echoConsumer.accept(echo); } + public void splitToNuggets() { + echoes.splitToNuggets(); + } + public Iterable echoes() { return echoes; } - public void remove(Predicate whichOnes) { - echoes.remove(whichOnes); + public List nuggets() { + return echoes.nuggets(); + } + + public boolean remove(Predicate whichOnes) { + return echoes.remove(whichOnes); } public Predicate oldEchoes(ClientCore client) { diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/EchoState.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/EchoState.java new file mode 100644 index 0000000..73c8095 --- /dev/null +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/EchoState.java @@ -0,0 +1,150 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar.graphics; + +import com.midnightbits.scanner.rt.core.Id; +import com.midnightbits.scanner.rt.core.ScannerMod; +import com.midnightbits.scanner.rt.math.V3i; +import com.midnightbits.scanner.sonar.BlockEcho; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EchoState { + private static final String TAG = ScannerMod.MOD_ID; + private static final Logger LOGGER = LoggerFactory.getLogger(TAG); + public final BlockEcho echo; + public int sides = Pixel.ALL_SIDES; + public int edges = 0; + public int alpha = Colors.ECHO_ALPHA; + + public EchoState(BlockEcho echo) { + this.echo = echo; + } + + public V3i position() { + return echo.position(); + } + + public Id id() { + return echo.id(); + } + + public Colors.Proxy color() { + return echo.color(); + } + + public void draw(GlProgramConsumer buffer, MatrixStack matrices, Vector3f camera) { + final var m = pushAndAlignWithCamera(matrices, camera); + + if (alpha == 0) { + return; + } + + final var argb32 = alpha | color().rgb24(); + + for (var side = 0; side < Pixel.triangles.length; ++side) { + final var flag = 1 << side; + if ((sides & flag) == 0) { + LOGGER.debug("[{}]: skipping: {}/{}", echo.position(), side, Pixel.side_names[side]); + continue; + } + + final var wall = Pixel.triangles[side]; + for (final var vertex : wall) { + vertex.apply(buffer, m, argb32); + } + } + LOGGER.debug(""); + + matrices.pop(); + } + + public void sketch(GlProgramConsumer buffer, MatrixStack matrices, Vector3f camera) { + final var m = pushAndAlignWithCamera(matrices, camera); + + final var validColor = Colors.OPAQUE | color().rgb24(); + + for (var index = 0; index < Pixel.edges.length; ++index) { + final var flag = 1 << index; + if ((edges & flag) == 0) { + LOGGER.debug("[{}]: skipping: {}/{}", echo.position(), index, Pixel.edge_names[index]); + continue; + } + + Pixel.edges[index].apply(buffer, m, validColor); + } + LOGGER.debug(""); + + matrices.pop(); + } + + public AABB getBounds() { + final var pos = echo.position(); + double minX = pos.getX(); + double minY = pos.getY(); + double minZ = pos.getZ(); + + return new AABB(minX, minY, minZ, minX + 1, minY + 1, minZ + 1); + } + + private Matrix4f pushAndAlignWithCamera(MatrixStack matrices, Vector3f camera) { + final var x = position().getX() - camera.x; + final var y = position().getY() - camera.y; + final var z = position().getZ() - camera.z; + matrices.push(); + matrices.translate(x, y, z); + return matrices.peek().getPositionMatrix(); + } + + public static class AABB { + private double minX; + private double minY; + private double minZ; + private double maxX; + private double maxY; + private double maxZ; + + public AABB(double x1, double y1, double z1, double x2, double y2, double z2) { + this.minX = Math.min(x1, x2); + this.minY = Math.min(y1, y2); + this.minZ = Math.min(z1, z2); + this.maxX = Math.max(x1, x2); + this.maxY = Math.max(y1, y2); + this.maxZ = Math.max(z1, z2); + } + + public double minX() { return minX; } + public double minY() { return minY; } + public double minZ() { return minZ; } + public double maxX() { return maxX; } + public double maxY() { return maxY; } + public double maxZ() { return maxZ; } + + public void expand(AABB other) { + final var minX = quadrupleMin(this.minX, this.maxX, other.minX, other.maxX); + final var maxX = quadrupleMax(this.minX, this.maxX, other.minX, other.maxX); + final var minY = quadrupleMin(this.minY, this.maxY, other.minY, other.maxY); + final var maxY = quadrupleMax(this.minY, this.maxY, other.minY, other.maxY); + final var minZ = quadrupleMin(this.minZ, this.maxZ, other.minZ, other.maxZ); + final var maxZ = quadrupleMax(this.minZ, this.maxZ, other.minZ, other.maxZ); + + this.minX = minX; + this.minY = minY; + this.minZ = minZ; + this.maxX = maxX; + this.maxY = maxY; + this.maxZ = maxZ; + } + + private static double quadrupleMin(double a, double b, double c, double d) { + return Math.min(Math.min(a, b), Math.min(c, d)); + } + + private static double quadrupleMax(double a, double b, double c, double d) { + return Math.max(Math.max(a, b), Math.max(c, d)); + } + } +} diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/FrustumFilter.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/FrustumFilter.java new file mode 100644 index 0000000..00e616d --- /dev/null +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/FrustumFilter.java @@ -0,0 +1,17 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar.graphics; + +import com.midnightbits.scanner.sonar.BlockEcho; +import com.midnightbits.scanner.sonar.EchoNugget; + +public interface FrustumFilter { + default boolean contains(EchoNugget nugget) { + return contains(nugget.getBounds()); + } + default boolean contains(EchoState echoState) { + return contains(echoState.getBounds()); + } + boolean contains(EchoState.AABB bounds); +} diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/GlProgramConsumer.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/GlProgramConsumer.java new file mode 100644 index 0000000..e85f364 --- /dev/null +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/GlProgramConsumer.java @@ -0,0 +1,10 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar.graphics; + +import org.joml.Matrix4f; + +public interface GlProgramConsumer { + void vertexColor(Matrix4f matrix, float x, float y, float z, int argb32); +} diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/MatrixStack.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/MatrixStack.java new file mode 100644 index 0000000..a3cc307 --- /dev/null +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/MatrixStack.java @@ -0,0 +1,50 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar.graphics; + +import org.joml.Matrix4f; + +import java.util.ArrayDeque; +import java.util.Deque; + +public class MatrixStack { + private final Deque stack = new ArrayDeque<>(); + + public MatrixStack(Matrix4f startingMatrix) { + stack.add(new MatrixStack.Entry(new Matrix4f(startingMatrix))); + } + + public void translate(float x, float y, float z) { + MatrixStack.Entry entry = this.stack.getLast(); + entry.positionMatrix.translate(x, y, z); + } + + public void push() { + this.stack.addLast(new MatrixStack.Entry(this.stack.getLast())); + } + + public void pop() { + this.stack.removeLast(); + } + + public MatrixStack.Entry peek() { + return this.stack.getLast(); + } + + public static final class Entry { + final Matrix4f positionMatrix; + + Entry(Matrix4f positionMatrix) { + this.positionMatrix = positionMatrix; + } + + Entry(MatrixStack.Entry matrix) { + this.positionMatrix = new Matrix4f(matrix.positionMatrix); + } + + public Matrix4f getPositionMatrix() { + return this.positionMatrix; + } + } +} diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/Pixel.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/Pixel.java new file mode 100644 index 0000000..ad0297b --- /dev/null +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/Pixel.java @@ -0,0 +1,128 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar.graphics; + +import org.joml.Matrix4f; +import org.joml.Vector3f; + +public record Pixel(EchoState echoState, double distanceSquared) { + + public static Pixel of(EchoState echoState, Vector3f camera) { + final var myPos = echoState.position(); + final var pos = new Vector3f(myPos.getX(), myPos.getY(), myPos.getZ()).add(.5F, .5F, .5F); + return new Pixel(echoState, pos.distanceSquared(camera)); + } + + public record Vertex(int x, int y, int z) { + void apply(GlProgramConsumer buffer, Matrix4f matrix, int argb32) { + buffer.vertexColor(matrix, x, y, z, argb32); + } + + public Vertex sub(Vertex start) { + return new Vertex(x - start.x, y - start.y, z - start.z); + } + } + + public static final Vertex v000 = new Vertex(0, 0, 0); + public static final Vertex v001 = new Vertex(0, 0, 1); + public static final Vertex v010 = new Vertex(0, 1, 0); + public static final Vertex v011 = new Vertex(0, 1, 1); + public static final Vertex v100 = new Vertex(1, 0, 0); + public static final Vertex v101 = new Vertex(1, 0, 1); + public static final Vertex v110 = new Vertex(1, 1, 0); + public static final Vertex v111 = new Vertex(1, 1, 1); + + public static final Vertex[][] triangles = new Vertex[][] { + new Vertex[] { v000, v010, v100, v100, v010, v110 }, + new Vertex[] { v001, v000, v101, v101, v000, v100 }, + new Vertex[] { v011, v001, v111, v111, v001, v101 }, + new Vertex[] { v010, v011, v110, v110, v011, v111 }, + new Vertex[] { v001, v011, v000, v000, v011, v010 }, + new Vertex[] { v100, v110, v101, v101, v110, v111 }, + }; + + public final static int SIDE_Z0 = 1; + public final static int SIDE_Y0 = 1 << 1; + public final static int SIDE_Z1 = 1 << 2; + public final static int SIDE_Y1 = 1 << 3; + public final static int SIDE_X0 = 1 << 4; + public final static int SIDE_X1 = 1 << 5; + public final static int ALL_SIDES = SIDE_X0 | SIDE_X1 | SIDE_Y0 | SIDE_Y1 | SIDE_Z0 | SIDE_Z1; + + public final static String[] side_names = new String[] { + "Z0", + "Y0", + "Z1", + "Y1", + "X0", + "X1", + }; + + public record Edge(Vertex start, Vertex end, int sides, int opposite) { + void apply(GlProgramConsumer buffer, Matrix4f matrix, int argb) { + start.apply(buffer, matrix, argb); + end.apply(buffer, matrix, argb); + } + + public int validSides(int blockSides) { + return blockSides & sides; + } + } + + public static final Edge[] edges = new Edge[] { + /* 0 */ + new Edge(v000, v100, SIDE_Z0 | SIDE_Y0, 2), + new Edge(v001, v101, SIDE_Y0 | SIDE_Z1, 3), + new Edge(v011, v111, SIDE_Z1 | SIDE_Y1, 0), + new Edge(v010, v110, SIDE_Z0 | SIDE_Y1, 1), + + /* 4 */ + new Edge(v000, v010, SIDE_Z0 | SIDE_X0, 6), + new Edge(v001, v011, SIDE_Z1 | SIDE_X0, 7), + new Edge(v101, v111, SIDE_Z1 | SIDE_X1, 4), + new Edge(v100, v110, SIDE_Z0 | SIDE_X1, 5), + + /* 8 */ + new Edge(v000, v001, SIDE_Y0 | SIDE_X0, 10), + new Edge(v010, v011, SIDE_Y1 | SIDE_X0, 11), + new Edge(v110, v111, SIDE_Y1 | SIDE_X1, 8), + new Edge(v100, v101, SIDE_Y0 | SIDE_X1, 9), + }; + + public final static int EDGE_FRONT_BOTTOM = 1; + public final static int EDGE_BACK_BOTTOM = 1 << 1; + public final static int EDGE_BACK_TOP = 1 << 2; + public final static int EDGE_FRONT_TOP = 1 << 3; + + public final static int EDGE_FRONT_LEFT = 1 << 4; + public final static int EDGE_BACK_LEFT = 1 << 5; // our left; if you turned the echo around, it would be back right + public final static int EDGE_BACK_RIGHT = 1 << 6; // same + public final static int EDGE_FRONT_RIGHT = 1 << 7; + + public final static int EDGE_LEFT_BOTTOM = 1 << 8; + public final static int EDGE_LEFT_TOP = 1 << 9; + public final static int EDGE_RIGHT_TOP = 1 << 10; + public final static int EDGE_RIGHT_BOTTOM = 1 << 11; + + public final static String[] edge_names = new String[] { + "FRONT_BOTTOM", + "BACK_BOTTOM", + "BACK_TOP", + "FRONT_TOP", + + "FRONT_LEFT", + "BACK_LEFT", + "BACK_RIGHT", + "FRONT_RIGHT", + + "LEFT_BOTTOM", + "LEFT_TOP", + "RIGHT_TOP", + "RIGHT_BOTTOM", + }; + + public final static int ALL_EDGES = EDGE_FRONT_BOTTOM | EDGE_BACK_BOTTOM | EDGE_BACK_TOP | EDGE_FRONT_TOP | + EDGE_FRONT_LEFT | EDGE_BACK_LEFT | EDGE_BACK_RIGHT | EDGE_FRONT_RIGHT | + EDGE_LEFT_BOTTOM | EDGE_LEFT_TOP | EDGE_RIGHT_BOTTOM | EDGE_RIGHT_TOP; +} diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/Shimmers.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/Shimmers.java index 0e0a5b9..22a1777 100644 --- a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/Shimmers.java +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/Shimmers.java @@ -3,11 +3,19 @@ package com.midnightbits.scanner.sonar.graphics; +import com.midnightbits.scanner.rt.core.Id; import com.midnightbits.scanner.rt.math.V3i; +import com.midnightbits.scanner.sonar.BlockEcho; +import com.midnightbits.scanner.sonar.Echo; +import java.util.Collection; import java.util.List; +import java.util.stream.Stream; public class Shimmers { + private final static Colors.Proxy SHIMMER_BLUE = new Colors.DirectValue(0x8080FF); + private final static Echo shimmer = new Echo(Id.ofMod("shimmer-echo"), SHIMMER_BLUE); + private final List blocks; private double alpha = 0; @@ -26,4 +34,22 @@ public void setAlpha(double alpha) { public double alpha() { return alpha; } + + public Stream toEchoStates(double alphaMax) { + final var alphaChannel = (int) Math.round(255 * (this.alpha * alphaMax)); + final var alpha = alphaChannel << 24; + if (alpha == 0) { + return Stream.of(); + } + + return blocks.stream() + .map((pos) -> new BlockEcho(pos, shimmer, 0)) + .map(EchoState::new) + .peek((state) -> state.alpha = alpha); + } + + public static Stream toEchoStates(List waves, double alphaMax) { + return waves.stream() + .flatMap(wave -> wave.toEchoStates(alphaMax)); + } } diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/TriColorSorter.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/TriColorSorter.java new file mode 100644 index 0000000..8796680 --- /dev/null +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/TriColorSorter.java @@ -0,0 +1,131 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar.graphics; + +import com.midnightbits.scanner.rt.core.ScannerMod; +import com.midnightbits.scanner.rt.math.V3i; +import com.midnightbits.scanner.sonar.BlockEcho; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class TriColorSorter implements Iterator>, Iterable> { + private static final String TAG = ScannerMod.MOD_ID; + private static final Logger LOGGER = LoggerFactory.getLogger(TAG); + + private record AdjacentSides(int x, int y, int z, int mine, int theirs) { + } + + private static final AdjacentSides[] ADJACENT_SIDES = new AdjacentSides[] { + new AdjacentSides(-1, 0, 0, Pixel.SIDE_X0, Pixel.SIDE_X1), + new AdjacentSides(1, 0, 0, Pixel.SIDE_X1, Pixel.SIDE_X0), + new AdjacentSides(0, -1, 0, Pixel.SIDE_Y0, Pixel.SIDE_Y1), + new AdjacentSides(0, 1, 0, Pixel.SIDE_Y1, Pixel.SIDE_Y0), + new AdjacentSides(0, 0, -1, Pixel.SIDE_Z0, Pixel.SIDE_Z1), + new AdjacentSides(0, 0, 1, Pixel.SIDE_Z1, Pixel.SIDE_Z0), + }; + + private final Map pool = new HashMap<>(); + private final Deque white; + + public TriColorSorter(Stream echoes) { + echoes.forEach((echo) -> pool.put(echo.position(), echo)); + white = pool.keySet().stream().sorted().collect(Collectors.toCollection(ArrayDeque::new)); + } + + public TriColorSorter(Collection echoes) { + this(echoes.stream().map(EchoState::new)); + } + + public boolean hasNext() { + return !white.isEmpty(); + } + + private Map moveFromWhiteToBlack() { + Deque grey = new ArrayDeque<>(); + Map black = new HashMap<>(); + + final var nextWhite = pool.get(white.removeFirst()); + grey.add(nextWhite); + while (!grey.isEmpty()) { + EchoState current = grey.removeFirst(); + white.remove(current.position()); + black.put(current.position(), current); + + for (final var side : ADJACENT_SIDES) { + if ((current.sides & side.mine) == 0) { + continue; + } + + final var neighbour = pool.get(current.position().add(side.x, side.y, side.z)); + if (neighbour == null || !neighbour.id().equals(current.id())) { + continue; + } + + current.sides &= ~side.mine; + neighbour.sides &= ~side.theirs; + grey.add(neighbour); + } + } + + return black; + } + + public Map next() { + final var black = moveFromWhiteToBlack(); + + final var sortedPos = black.keySet().stream().sorted().toList(); + + for (final var pos : sortedPos) { + final var pixel = black.get(pos); + + pixel.edges = 0; + for (int edgeIndex = 0; edgeIndex < Pixel.edges.length; ++edgeIndex) { + final var edge = Pixel.edges[edgeIndex]; + + final int mask = edge.validSides(pixel.sides); + if (mask == edge.sides()) { + pixel.edges |= 1 << edgeIndex; + } else if (mask != 0) { + final var oppositeIndex = edge.opposite(); + final var oppositeEdge = Pixel.edges[oppositeIndex]; + final var movement = edge.start().sub(oppositeEdge.start()); + final var oppositePos = pixel.position().add(movement.x(), movement.y(), movement.z()); + final var opposite = black.get(oppositePos); + + if (opposite == null) { + continue; + } + + final int oppositeBit = 1 << oppositeIndex; + if ((opposite.edges & oppositeBit) == 0) { + pixel.edges |= 1 << edgeIndex; + LOGGER.debug("{}", + String.format( + "[%s] %2d (%d%d%d -> %d%d%d): %02x -> %2d -> (%d, %d, %d) -> %x/%x -> adding", + pos, edgeIndex, + edge.start().x(), edge.start().y(), edge.start().z(), + edge.end().x(), edge.end().y(), edge.end().z(), + edge.sides() - mask, oppositeIndex, + movement.x(), movement.y(), movement.z(), + opposite.edges, + opposite.edges & oppositeBit)); + + } + } + } + } + + return black; + } + + @Override + public @NotNull Iterator> iterator() { + return this; + } +} diff --git a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/WaveAnimator.java b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/WaveAnimator.java index 2ed4730..98d72de 100644 --- a/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/WaveAnimator.java +++ b/scanner/src/main/java/com/midnightbits/scanner/sonar/graphics/WaveAnimator.java @@ -66,6 +66,9 @@ public Animation buildAnimation(Scene scene, StageReporter reporter) { public void addEchoes() { echoes.forEach(target::echoFrom); + if (!echoes.isEmpty()) { + target.splitToNuggets(); + } } private static ActionAnimation report(StageReporter reporter, int id, AnimationStep step) { diff --git a/scanner/src/test/java/com/midnightbits/scanner/sonar/graphics/test/ColorsTest.java b/scanner/src/test/java/com/midnightbits/scanner/sonar/graphics/test/ColorsTest.java new file mode 100644 index 0000000..dda338c --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/sonar/graphics/test/ColorsTest.java @@ -0,0 +1,49 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar.graphics.test; + +import com.midnightbits.scanner.rt.core.Id; +import com.midnightbits.scanner.sonar.graphics.ColorDefaults; +import com.midnightbits.scanner.sonar.graphics.Colors; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ColorsTest { + private static final class ColorX implements Colors.Proxy { + @Override + public int rgb24() { + return 0; + } + + @Override + public boolean equals(Colors.Proxy other) { + return false; + } + + @Override + public int compareTo(@NotNull Colors.Proxy o) { + return 0; + } + } + + private static T ident(T value) { + return value; + } + + @Test + void compare() { + final var colorX = new ColorX(); + final var lapis_ores = ColorDefaults.BLOCK_TAG_COLORS.get(Id.ofVanilla("lapis_ores")); + final var redstone_ores = ColorDefaults.BLOCK_TAG_COLORS.get(Id.ofVanilla("redstone_ores")); + + Assertions.assertTrue(lapis_ores.equals(ident(lapis_ores))); + Assertions.assertFalse(lapis_ores.equals(redstone_ores)); + Assertions.assertFalse(lapis_ores.equals(colorX)); + + Assertions.assertEquals(0, lapis_ores.compareTo(ident(lapis_ores))); + Assertions.assertNotEquals(0, lapis_ores.compareTo(redstone_ores)); + Assertions.assertThrows(ClassCastException.class, () -> lapis_ores.compareTo(colorX)); + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/sonar/test/EchoNuggetTest.java b/scanner/src/test/java/com/midnightbits/scanner/sonar/test/EchoNuggetTest.java new file mode 100644 index 0000000..d39f7cf --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/sonar/test/EchoNuggetTest.java @@ -0,0 +1,238 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar.test; + +import com.midnightbits.scanner.rt.core.Id; +import com.midnightbits.scanner.sonar.BlockEcho; +import com.midnightbits.scanner.sonar.Echo; +import com.midnightbits.scanner.sonar.EchoNugget; +import com.midnightbits.scanner.sonar.graphics.Colors; +import com.midnightbits.scanner.sonar.graphics.Pixel; +import com.midnightbits.scanner.utils.test.gl.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +public class EchoNuggetTest { + public static final Colors.Proxy VANILLA = new Colors.DirectValue(Colors.VANILLA); + + private static Echo of(String path) { + return Echo.of(Id.ofVanilla(path), Colors.BLOCK_TAG_COLORS.getOrDefault(Id.of(path + "s"), VANILLA)); + } + + final static Echo gold_ore = of("gold_ore"); + final static Echo coal_ore = of("coal_ore"); + final static Echo iron_ore = of("iron_ore"); + + final static int EDGES_X0 = Pixel.EDGE_FRONT_LEFT | Pixel.EDGE_BACK_LEFT | Pixel.EDGE_LEFT_BOTTOM + | Pixel.EDGE_LEFT_TOP; + final static int EDGES_X1 = Pixel.EDGE_FRONT_RIGHT | Pixel.EDGE_BACK_RIGHT | Pixel.EDGE_RIGHT_BOTTOM + | Pixel.EDGE_RIGHT_TOP; + final static int EDGES_Y0 = Pixel.EDGE_FRONT_BOTTOM | Pixel.EDGE_BACK_BOTTOM | Pixel.EDGE_LEFT_BOTTOM + | Pixel.EDGE_RIGHT_BOTTOM; + final static int EDGES_Y1 = Pixel.EDGE_FRONT_TOP | Pixel.EDGE_BACK_TOP | Pixel.EDGE_LEFT_TOP + | Pixel.EDGE_RIGHT_TOP; + final static int EDGES_Z0 = Pixel.EDGE_FRONT_BOTTOM | Pixel.EDGE_FRONT_TOP | Pixel.EDGE_FRONT_LEFT + | Pixel.EDGE_FRONT_RIGHT; + final static int EDGES_Z1 = Pixel.EDGE_BACK_BOTTOM | Pixel.EDGE_BACK_TOP | Pixel.EDGE_BACK_LEFT + | Pixel.EDGE_BACK_RIGHT; + + @Test + void oppositeEdgesAreStable() { + for (int edgeId = 0; edgeId < Pixel.edges.length; ++edgeId) { + final var edge = Pixel.edges[edgeId]; + final var oppo = Pixel.edges[edge.opposite()]; + final var start = edge.start().sub(oppo.start()); + final var end = edge.end().sub(oppo.end()); + final var msg = String.format("%2d %d%d%d:%d%d%d -> %2d %d%d%d:%d%d%d (%d, %d, %d)/(%d, %d, %d) %n", edgeId, + edge.start().x(), edge.start().y(), edge.start().z(), + edge.end().x(), edge.end().y(), edge.end().z(), + edge.opposite(), + oppo.start().x(), oppo.start().y(), oppo.start().z(), + oppo.end().x(), oppo.end().y(), oppo.end().z(), + start.x(), start.y(), start.z(), + end.x(), end.y(), end.z()); + Assertions.assertEquals(start, end, msg); + Assertions.assertEquals(edgeId, oppo.opposite(), msg); + } + } + + @Test + void disjointedEchoes() { + final var nuggets = EchoNugget.group(Set.of( + BlockEcho.echoFrom(0, 0, 0, gold_ore), + BlockEcho.echoFrom(2, 0, 0, gold_ore), + BlockEcho.echoFrom(1, 1, 0, gold_ore))); + + final var tape = VertexTape.record(nuggets); + + tape.assertPlayback(new VertexVerifier[] { + new BlockVerifier(5, 0, 0, Pixel.ALL_SIDES, 0xFAEE4D), + new MarkerVerifier(0x000000), + new BlockVerifier(4, 1, 0, Pixel.ALL_SIDES, 0xFAEE4D), + new MarkerVerifier(0x000000), + new BlockVerifier(3, 0, 0, Pixel.ALL_SIDES, 0xFAEE4D), + new MarkerVerifier(0x000000), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(5, 0, 0, Pixel.ALL_EDGES, 0xFAEE4D), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(4, 1, 0, Pixel.ALL_EDGES, 0xFAEE4D), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(3, 0, 0, Pixel.ALL_EDGES, 0xFAEE4D), + }); + } + + @Test + void semiDetachedEchos() { + final var nuggets = EchoNugget.group(Set.of( + BlockEcho.echoFrom(0, 0, 0, gold_ore), + BlockEcho.echoFrom(1, 0, 0, iron_ore))); + + final var tape = VertexTape.record(nuggets); + + tape.assertPlayback(new VertexVerifier[] { + new BlockVerifier(4, 0, 0, Pixel.ALL_SIDES, 0x664C33), + new MarkerVerifier(0x000000), + new BlockVerifier(3, 0, 0, Pixel.ALL_SIDES, 0xFAEE4D), + new MarkerVerifier(0x000000), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(4, 0, 0, Pixel.ALL_EDGES, 0x664C33), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(3, 0, 0, Pixel.ALL_EDGES, 0xFAEE4D), + }); + } + + @Test + void attachedEchosX() { + final var nuggets = EchoNugget.group(Set.of( + BlockEcho.echoFrom(0, 0, 0, coal_ore), + BlockEcho.echoFrom(1, 0, 0, coal_ore))); + + final var tape = VertexTape.record(nuggets); + + tape.assertPlayback(new VertexVerifier[] { + new BlockVerifier(4, 0, 0, Pixel.ALL_SIDES & ~Pixel.SIDE_X0, 0x4C4C4C), + new BlockVerifier(3, 0, 0, Pixel.ALL_SIDES & ~Pixel.SIDE_X1, 0x4C4C4C), + new MarkerVerifier(0x000000), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(4, 0, 0, Pixel.ALL_EDGES & ~EDGES_X0, 0x4C4C4C), + new FrameVerifier(3, 0, 0, Pixel.ALL_EDGES & ~EDGES_X1, 0x4C4C4C), + }); + } + + @Test + void attachedEchosY() { + final var nuggets = EchoNugget.group(Set.of( + BlockEcho.echoFrom(0, 0, 0, coal_ore), + BlockEcho.echoFrom(0, 1, 0, coal_ore))); + + final var tape = VertexTape.record(nuggets); + + tape.assertPlayback(new VertexVerifier[] { + new BlockVerifier(3, 1, 0, Pixel.ALL_SIDES & ~Pixel.SIDE_Y0, 0x4C4C4C), + new BlockVerifier(3, 0, 0, Pixel.ALL_SIDES & ~Pixel.SIDE_Y1, 0x4C4C4C), + new MarkerVerifier(0x000000), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(3, 1, 0, Pixel.ALL_EDGES & ~EDGES_Y0, 0x4C4C4C), + new FrameVerifier(3, 0, 0, Pixel.ALL_EDGES & ~EDGES_Y1, 0x4C4C4C), + }); + } + + @Test + void attachedEchosZ() { + final var nuggets = EchoNugget.group(Set.of( + BlockEcho.echoFrom(0, 0, 0, coal_ore), + BlockEcho.echoFrom(0, 0, 1, coal_ore))); + + final var tape = VertexTape.record(nuggets); + + tape.assertPlayback(new VertexVerifier[] { + new BlockVerifier(3, 0, 1, Pixel.ALL_SIDES & ~Pixel.SIDE_Z0, 0x4C4C4C), + new BlockVerifier(3, 0, 0, Pixel.ALL_SIDES & ~Pixel.SIDE_Z1, 0x4C4C4C), + new MarkerVerifier(0x000000), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(3, 0, 1, Pixel.ALL_EDGES & ~EDGES_Z0, 0x4C4C4C), + new FrameVerifier(3, 0, 0, Pixel.ALL_EDGES & ~EDGES_Z1, 0x4C4C4C), + }); + } + + @Test + void attachedEchosXY() { + final var nuggets = EchoNugget.group(Set.of( + BlockEcho.echoFrom(0, 0, 0, coal_ore), + BlockEcho.echoFrom(1, 0, 0, coal_ore), + BlockEcho.echoFrom(0, 1, 0, coal_ore), + BlockEcho.echoFrom(1, 1, 0, coal_ore))); + + final var tape = VertexTape.record(nuggets); + + tape.assertPlayback(new VertexVerifier[] { + new BlockVerifier(4, 1, 0, Pixel.ALL_SIDES & ~(Pixel.SIDE_X0 | Pixel.SIDE_Y0), 0x4C4C4C), + new BlockVerifier(4, 0, 0, Pixel.ALL_SIDES & ~(Pixel.SIDE_X0 | Pixel.SIDE_Y1), 0x4C4C4C), + new BlockVerifier(3, 1, 0, Pixel.ALL_SIDES & ~(Pixel.SIDE_X1 | Pixel.SIDE_Y0), 0x4C4C4C), + new BlockVerifier(3, 0, 0, Pixel.ALL_SIDES & ~(Pixel.SIDE_X1 | Pixel.SIDE_Y1), 0x4C4C4C), + new MarkerVerifier(0x000000), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(4, 1, 0, Pixel.ALL_EDGES & ~(EDGES_X0 | EDGES_Y0), 0x4C4C4C), + new FrameVerifier(4, 0, 0, Pixel.ALL_EDGES & ~(EDGES_X0 | EDGES_Y1), 0x4C4C4C), + new FrameVerifier(3, 1, 0, Pixel.ALL_EDGES & ~(EDGES_X1 | EDGES_Y0), 0x4C4C4C), + new FrameVerifier(3, 0, 0, Pixel.ALL_EDGES & ~(EDGES_X1 | EDGES_Y1), 0x4C4C4C), + }); + } + + @Test + void tetromino() { + final var nuggets = EchoNugget.group(Set.of( + BlockEcho.echoFrom(0, 0, 0, coal_ore), + BlockEcho.echoFrom(1, 0, 0, coal_ore), + BlockEcho.echoFrom(1, 1, 0, coal_ore), + BlockEcho.echoFrom(2, 0, 0, coal_ore))); + + final var tape = VertexTape.record(nuggets); + + tape.assertPlayback(new VertexVerifier[] { + new BlockVerifier(5, 0, 0, Pixel.ALL_SIDES & ~Pixel.SIDE_X0, 0x4C4C4C), + new BlockVerifier(4, 1, 0, Pixel.ALL_SIDES & ~Pixel.SIDE_Y0, 0x4C4C4C), + new BlockVerifier(4, 0, 0, Pixel.SIDE_Y0 | Pixel.SIDE_Z0 | Pixel.SIDE_Z1, 0x4C4C4C), + new BlockVerifier(3, 0, 0, Pixel.ALL_SIDES & ~Pixel.SIDE_X1, 0x4C4C4C), + new MarkerVerifier(0x000000), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(5, 0, 0, Pixel.ALL_EDGES & ~EDGES_X0 | Pixel.EDGE_LEFT_TOP, 0x4C4C4C), + new FrameVerifier(4, 1, 0, Pixel.ALL_EDGES & ~EDGES_Y0, 0x4C4C4C), + new FrameVerifier(4, 0, 0, Pixel.EDGE_FRONT_BOTTOM | Pixel.EDGE_BACK_BOTTOM, 0x4C4C4C), + new FrameVerifier(3, 0, 0, Pixel.ALL_EDGES & ~EDGES_X1 | Pixel.EDGE_RIGHT_TOP, 0x4C4C4C), + }); + } + + @Test + void largeL() { + final var nuggets = EchoNugget.group(Set.of( + BlockEcho.echoFrom(0, 0, 0, coal_ore), + BlockEcho.echoFrom(1, 0, 0, coal_ore), + BlockEcho.echoFrom(0, 1, 0, coal_ore), + BlockEcho.echoFrom(0, 0, 1, coal_ore), + BlockEcho.echoFrom(1, 0, 1, coal_ore), + BlockEcho.echoFrom(0, 1, 1, coal_ore))); + + final var tape = VertexTape.record(nuggets); + + tape.assertPlayback(new VertexVerifier[] { + new BlockVerifier(4, 0, 1, Pixel.ALL_SIDES & ~(Pixel.SIDE_X0 | Pixel.SIDE_Z0), 0x4C4C4C), + new BlockVerifier(4, 0, 0, Pixel.ALL_SIDES & ~(Pixel.SIDE_X0 | Pixel.SIDE_Z1), 0x4C4C4C), + new BlockVerifier(3, 1, 1, Pixel.ALL_SIDES & ~(Pixel.SIDE_Y0 | Pixel.SIDE_Z0), 0x4C4C4C), + new BlockVerifier(3, 0, 1, Pixel.SIDE_X0 | Pixel.SIDE_Y0 | Pixel.SIDE_Z1, 0x4C4C4C), + new BlockVerifier(3, 1, 0, Pixel.ALL_SIDES & ~(Pixel.SIDE_Y0 | Pixel.SIDE_Z1), 0x4C4C4C), + new BlockVerifier(3, 0, 0, Pixel.SIDE_X0 | Pixel.SIDE_Y0 | Pixel.SIDE_Z0, 0x4C4C4C), + new MarkerVerifier(0x000000), + new MarkerVerifier(0xFFFFFF), + new FrameVerifier(4, 0, 1, Pixel.ALL_EDGES & ~(EDGES_X0 | EDGES_Z0) | Pixel.EDGE_LEFT_TOP, 0x4C4C4C), + new FrameVerifier(4, 0, 0, Pixel.ALL_EDGES & ~(EDGES_X0 | EDGES_Z1) | Pixel.EDGE_LEFT_TOP, 0x4C4C4C), + new FrameVerifier(3, 1, 1, Pixel.ALL_EDGES & ~(EDGES_Y0 | EDGES_Z0), 0x4C4C4C), + new FrameVerifier(3, 0, 1, Pixel.ALL_EDGES & ~(EDGES_X1 | EDGES_Y1 | EDGES_Z0), 0x4C4C4C), + new FrameVerifier(3, 1, 0, Pixel.ALL_EDGES & ~(EDGES_Y0 | EDGES_Z1), 0x4C4C4C), + new FrameVerifier(3, 0, 0, Pixel.ALL_EDGES & ~(EDGES_X1 | EDGES_Y1 | EDGES_Z1), 0x4C4C4C), + }); + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/sonar/test/EchoTest.java b/scanner/src/test/java/com/midnightbits/scanner/sonar/test/EchoTest.java new file mode 100644 index 0000000..67194ee --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/sonar/test/EchoTest.java @@ -0,0 +1,45 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.sonar.test; + +import com.midnightbits.scanner.rt.core.Id; +import com.midnightbits.scanner.sonar.BlockEcho; +import com.midnightbits.scanner.sonar.Echo; +import com.midnightbits.scanner.sonar.graphics.Colors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class EchoTest { + public static final Colors.Proxy VANILLA = new Colors.DirectValue(Colors.VANILLA); + public static final Colors.Proxy PURPLE = new Colors.DirectValue(Colors.PURPLE); + public static final Colors.Proxy CLEAR = new Colors.DirectValue(Colors.CLEAR); + + private static T ident(T value) { + return value; + } + + @Test + void equalsTest() { + final var echo1 = new Echo(Id.ofVanilla("iron_ore"), VANILLA); + + Assertions.assertEquals(echo1, ident(echo1)); + Assertions.assertEquals(echo1, new Echo(Id.ofVanilla("iron_ore"), VANILLA)); + Assertions.assertNotEquals(echo1, new Echo(Id.ofVanilla("iron_ore"), PURPLE)); + Assertions.assertNotEquals(echo1, new Echo(Id.ofVanilla("gold_ore"), VANILLA)); + Assertions.assertThrows(ClassCastException.class, () -> echo1.equals(BlockEcho.Partial.of(0, 0, 0, echo1))); + } + + @Test + void stringOf() { + Assertions.assertEquals("new Echo(Id.ofVanilla(\"iron_ore\"), new Color.DirectValue(Colors.VANILLA))", + new Echo(Id.ofVanilla("iron_ore"), VANILLA).toString()); + Assertions.assertEquals("new Echo(Id.ofVanilla(\"iron_ore\"), new Color.DirectValue(Colors.PURPLE))", + new Echo(Id.ofVanilla("iron_ore"), PURPLE).toString()); + Assertions.assertEquals("new Echo(Id.ofVanilla(\"iron_ore\"), new Color.DirectValue(0x000000))", + new Echo(Id.ofVanilla("iron_ore"), CLEAR).toString()); + Assertions.assertEquals( + "new Echo(Id.ofVanilla(\"iron_ore\"), Colors.BLOCK_TAG_COLORS.get(Id.ofVanilla(\"iron_ores\")))", + new Echo(Id.ofVanilla("iron_ore"), Colors.BLOCK_TAG_COLORS.get(Id.ofVanilla("iron_ores"))).toString()); + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/sonar/test/SonarTest.java b/scanner/src/test/java/com/midnightbits/scanner/sonar/test/SonarTest.java index eddf232..c448298 100644 --- a/scanner/src/test/java/com/midnightbits/scanner/sonar/test/SonarTest.java +++ b/scanner/src/test/java/com/midnightbits/scanner/sonar/test/SonarTest.java @@ -42,9 +42,11 @@ public class SonarTest { private final MockedClock clock = new MockedClock(); public static final Echo deepslate_iron_ore = Echo.of(Id.ofVanilla("deepslate_iron_ore"), BlockEchoTest.VANILLA); - public static final Echo deepslate_diamond_ore = Echo.of(Id.ofVanilla("deepslate_diamond_ore"), BlockEchoTest.VANILLA); + public static final Echo deepslate_diamond_ore = Echo.of(Id.ofVanilla("deepslate_diamond_ore"), + BlockEchoTest.VANILLA); public static final Echo diamond_ore = Echo.of(Id.ofVanilla("diamond_ore"), BlockEchoTest.VANILLA); - public static final Echo iron_ore = Echo.of(Id.ofVanilla("iron_ore"), Colors.BLOCK_TAG_COLORS.get(Id.ofVanilla("iron_ores"))); + public static final Echo iron_ore = Echo.of(Id.ofVanilla("iron_ore"), + Colors.BLOCK_TAG_COLORS.get(Id.ofVanilla("iron_ores"))); public static final Echo gold_ore = Echo.of(Id.ofVanilla("gold_ore"), BlockEchoTest.VANILLA); public static final Echo coal_ore = Echo.of(Id.ofVanilla("coal_ore"), BlockEchoTest.VANILLA); @@ -219,6 +221,77 @@ void searchForGold() { "> 29m gold_ore", "> 30m gold_ore", "> 30m gold_ore", "> 31m gold_ore", "> 32m gold_ore", "> 32m gold_ore", }, core.getPlayerMessages()); + + Assertions.assertEquals(23, setup.sonar.nuggets().size()); + } + + @Test + void searchForGold_afterDark() { + clock.timeStamp = 0x123456; + + final var core = new MockClientCore(null, 0f, 0f, MockWorld.TEST_WORLD); + final var setup = new Setup(TEST_BLOCK_DISTANCE, TEST_BLOCK_RADIUS, TEST_ECHO_LIFETIME, + Set.of(Id.ofVanilla("gold_ore"))); + + Assertions.assertFalse(setup.sendPing(core)); + + ((MockAnimatorHost) Services.PLATFORM.getAnimatorHost()).runAll(clock); + + Iterables.assertEquals(new BlockEcho[] { + }, setup.sonar.echoes()); + + Iterables.assertEquals(new String[] { + }, core.getPlayerMessages()); + } + + @Test + void searchForGold_afterDark2() { + clock.timeStamp = 0x123456; + + final var core = new MockClientCore(new V3i(-60, -60, -51), 0f, 0f, null); + final var setup = new Setup(TEST_BLOCK_DISTANCE, TEST_BLOCK_RADIUS, TEST_ECHO_LIFETIME, + Set.of(Id.ofVanilla("gold_ore"))); + + Assertions.assertTrue(setup.sendPing(core)); + + ((MockAnimatorHost) Services.PLATFORM.getAnimatorHost()).runAll(clock); + + Iterables.assertEquals(new BlockEcho[] { + }, setup.sonar.echoes()); + + Iterables.assertEquals(new String[] { + }, core.getPlayerMessages()); + } + + @Test + void removeOldEchoes_afterDark() { + clock.timeStamp = 0x123456; + + final var core1 = new MockClientCore(new V3i(-60, -60, -51), 0f, 0f, MockWorld.TEST_WORLD); + final var core2 = new MockClientCore(new V3i(-60, -60, -51), 0f, 0f, null); + final var setup = new Setup(TEST_BLOCK_DISTANCE, TEST_BLOCK_RADIUS, TEST_ECHO_LIFETIME, + Set.of(Id.ofVanilla("gold_ore"))); + + Assertions.assertTrue(setup.sendPing(core1)); + + ((MockAnimatorHost) Services.PLATFORM.getAnimatorHost()).runAll(clock); + + clock.timeStamp = 0x123456; + Assertions.assertTrue(setup.sonar.remove(setup.sonar.oldEchoes(core2))); + + Iterables.assertEquals(new BlockEcho[] { + }, setup.sonar.echoes()); + + Iterables.assertEquals(new String[] { + "> 1m gold_ore", "> 12m gold_ore", "> 12m gold_ore", "> 14m gold_ore", "> 16m gold_ore", + "> 17m gold_ore", "> 17m gold_ore", "> 18m gold_ore", "> 21m gold_ore", + "> 22m gold_ore", "> 23m gold_ore", "> 24m gold_ore", "> 24m gold_ore", + "> 25m gold_ore", "> 25m gold_ore", "> 25m gold_ore", "> 26m gold_ore", + "> 26m gold_ore", "> 26m gold_ore", "> 27m gold_ore", "> 28m gold_ore", + "> 28m gold_ore", "> 28m gold_ore", "> 29m gold_ore", "> 29m gold_ore", + "> 29m gold_ore", "> 30m gold_ore", "> 30m gold_ore", "> 31m gold_ore", + "> 32m gold_ore", "> 32m gold_ore", + }, core1.getPlayerMessages()); } @Test diff --git a/scanner/src/test/java/com/midnightbits/scanner/test/mocks/MockClientCore.java b/scanner/src/test/java/com/midnightbits/scanner/test/mocks/MockClientCore.java index ed10033..08d6b49 100644 --- a/scanner/src/test/java/com/midnightbits/scanner/test/mocks/MockClientCore.java +++ b/scanner/src/test/java/com/midnightbits/scanner/test/mocks/MockClientCore.java @@ -31,7 +31,7 @@ public MockClientCore(V3i playerPos, float cameraPitch, float cameraYaw, MockWor @Override public BlockInfo getBlockInfo(V3i pos) { - return world.getOrAir(pos); + return world != null ? world.getOrAir(pos) : null; } @Override diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/BlockVerifier.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/BlockVerifier.java new file mode 100644 index 0000000..e97be6d --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/BlockVerifier.java @@ -0,0 +1,32 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import com.midnightbits.scanner.sonar.graphics.Pixel; + +import java.util.ArrayList; +import java.util.List; + +public class BlockVerifier extends SubVerifier { + public BlockVerifier(int x, int y, int z, int sides, int color) { + super(makeSides(x, y, z, sides, color), + String.format("BlockVerifier(%d, %d, %d, 0x%x, 0x%X)", x, y, z, sides, color)); + } + + private static List makeSides(int x, int y, int z, int sides, int color) { + List result = new ArrayList<>(); + for (int i = 0; i < Pixel.triangles.length; ++i) { + final int flag = 1 << i; + if ((sides & flag) == 0) { + VertexVerifier.LOGGER.debug(String.format("BlockVerifier(%d, %d, %d, 0x%x, 0x%X): skipping %d/%s", x, y, + z, sides, color, i, Pixel.side_names[i])); + continue; + } + result.add(new FaceVerifier(x, y, z, i, color)); + } + LOGGER.debug(""); + + return result; + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/EdgeVerifier.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/EdgeVerifier.java new file mode 100644 index 0000000..33696e6 --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/EdgeVerifier.java @@ -0,0 +1,65 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import com.midnightbits.scanner.sonar.graphics.Colors; +import com.midnightbits.scanner.sonar.graphics.Pixel; +import org.joml.Vector3f; +import org.junit.jupiter.api.Assertions; + +import java.util.ArrayList; +import java.util.List; + +public class EdgeVerifier implements VertexVerifier { + final Vector3f origin; + final int edge; + final int color; + + EdgeVerifier(int x, int y, int z, int edge, int color) { + this.origin = new Vector3f(x, y, z); + this.edge = edge; + this.color = color; + } + + public int verticesNeeded() { + return 2; + } + + public String thisCallstackEntry() { + return String.format("EdgeVerifier(%d, %d, %d, %d, 0x%06X)", (int) origin.x, (int) origin.y, (int) origin.z, + edge, color); + } + + public void assertTape(VertexTape tape, String stack) { + final var pos = tape.pos(); + + final List expected = new ArrayList<>(); + final var edge = Pixel.edges[this.edge]; + expected.add(VertexVerifier.apply(edge.start(), origin, Colors.OPAQUE | color)); + expected.add(VertexVerifier.apply(edge.end(), origin, Colors.OPAQUE | color)); + + final var actual = tape.nextN(2); + + Assertions.assertEquals(expected, actual, "at: " + pos + diffTo("\nexpected: ", expected) + + diffTo("\nactual: ", actual) + stack + "\n" + thisCallstackEntry()); + } + + public String diffTo(TestVertex v) { + final var pos = v.pos(); + final int x = (int) (pos.x - origin.x); + final int y = (int) (pos.y - origin.y); + final int z = (int) (pos.z - origin.z); + return "" + x + y + z; + } + + public String diffTo(String prefix, List vs) { + final var builder = new StringBuilder(); + for (final var v : vs) { + if (!builder.isEmpty()) + builder.append(" -> "); + builder.append(diffTo(v)); + } + return prefix + builder; + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/FaceVerifier.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/FaceVerifier.java new file mode 100644 index 0000000..a031e7d --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/FaceVerifier.java @@ -0,0 +1,46 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import com.midnightbits.scanner.sonar.graphics.Colors; +import com.midnightbits.scanner.sonar.graphics.Pixel; +import org.joml.Vector3f; +import org.junit.jupiter.api.Assertions; + +import java.util.ArrayList; +import java.util.List; + +public class FaceVerifier implements VertexVerifier { + final Vector3f origin; + final int side; + final int color; + + FaceVerifier(int x, int y, int z, int side, int color) { + this.origin = new Vector3f(x, y, z); + this.side = side; + this.color = color; + } + + public int verticesNeeded() { + return 6; + } + + public String thisCallstackEntry() { + return String.format("FaceVerifier(%d, %d, %d, %d, 0x%06X)", (int) origin.x, (int) origin.y, (int) origin.z, + side, color); + } + + public void assertTape(VertexTape tape, String stack) { + final var pos = tape.pos(); + + final List expected = new ArrayList<>(); + for (final var vertex : Pixel.triangles[this.side]) { + expected.add(VertexVerifier.apply(vertex, origin, Colors.ECHO_ALPHA | color)); + } + + final var actual = tape.nextN(6); + + Assertions.assertEquals(expected, actual, "at: " + pos + stack + "\n" + thisCallstackEntry()); + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/FrameVerifier.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/FrameVerifier.java new file mode 100644 index 0000000..74a690e --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/FrameVerifier.java @@ -0,0 +1,33 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import com.midnightbits.scanner.sonar.graphics.Pixel; + +import java.util.ArrayList; +import java.util.List; + +public class FrameVerifier extends SubVerifier { + public FrameVerifier(int x, int y, int z, int edges, int color) { + super(makeEdges(x, y, z, edges, color), + String.format("FrameVerifier(%d, %d, %d, 0x%x, 0x%X)", x, y, z, edges, color)); + } + + private static List makeEdges(int x, int y, int z, int edges, int color) { + List result = new ArrayList<>(); + for (int i = 0; i < Pixel.edges.length; ++i) { + final int flag = 1 << i; + if ((edges & flag) == 0) { + VertexVerifier.LOGGER.debug(String.format( + "FrameVerifier(%d, %d, %d, 0x%x, 0x%X): skipping: %s/%s", + x, y, z, edges, color, i, Pixel.edge_names[i])); + continue; + } + result.add(new EdgeVerifier(x, y, z, i, color)); + } + LOGGER.debug(""); + + return result; + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/MarkerVerifier.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/MarkerVerifier.java new file mode 100644 index 0000000..f953bc4 --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/MarkerVerifier.java @@ -0,0 +1,29 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import org.junit.jupiter.api.Assertions; + +public class MarkerVerifier implements VertexVerifier { + final int color; + + public MarkerVerifier(int color) { + this.color = color; + } + + public int verticesNeeded() { + return 1; + } + + public String thisCallstackEntry() { + return String.format("MarkerVerifier(0x%06X)", color); + } + + public void assertTape(VertexTape tape, String stack) { + final var pos = tape.pos(); + final var marker = tape.next(); + Assertions.assertEquals(TestVertex.markerWith(color), marker, + "at: " + pos + stack + "\n" + thisCallstackEntry()); + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/SubVerifier.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/SubVerifier.java new file mode 100644 index 0000000..ff35eef --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/SubVerifier.java @@ -0,0 +1,29 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import java.util.List; + +public class SubVerifier implements VertexVerifier { + final List items; + final String thisEntry; + + SubVerifier(List items, String thisEntry) { + this.items = items; + this.thisEntry = thisEntry; + } + + public int verticesNeeded() { + return items.stream().map(VertexVerifier::verticesNeeded).reduce(0, Integer::sum); + } + + public String thisCallstackEntry() { + return thisEntry; + } + + public void assertTape(VertexTape tape, String stack) { + for (final var item : items) + item.assertTape(tape, stack + "\n" + thisEntry); + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/TestVertex.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/TestVertex.java new file mode 100644 index 0000000..408dc11 --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/TestVertex.java @@ -0,0 +1,17 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import org.joml.Vector3f; + +public record TestVertex(Vector3f pos, int argb32) { + static TestVertex markerWith(int argb32) { + return new TestVertex(new Vector3f(0, 0, 0), argb32); + } + + public String toString() { + return "\nTestVertex.of(" + pos.x + "F, " + pos.y + "F, " + pos.z + "F, " + String.format("0x%08X", argb32) + + ")"; + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/VertexTape.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/VertexTape.java new file mode 100644 index 0000000..05b81d6 --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/VertexTape.java @@ -0,0 +1,82 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import com.midnightbits.scanner.sonar.EchoNugget; +import com.midnightbits.scanner.sonar.graphics.MatrixStack; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.junit.jupiter.api.Assertions; + +import java.util.ArrayList; +import java.util.List; + +public class VertexTape { + private final List items; + private int readIndex = 0; + + public VertexTape(List items) { + this.items = items; + } + + public boolean stillHas(int vertices) { + return vertices <= items.size() - readIndex; + } + + public TestVertex next() { + return items.get(readIndex++); + } + + public List nextN(int n) { + final List section = new ArrayList<>(); + while (n > 0) { + section.add(next()); + --n; + } + return section; + } + + public int pos() { + return readIndex; + } + + public int size() { + return items.size(); + } + + public void assertPlayback(VertexVerifier[] template) { + for (final var verifier : template) { + final int pos = pos(); + final int needed = verifier.verticesNeeded(); + + Assertions.assertTrue(stillHas(needed), + "at: " + pos + ", when asking for " + needed + " (remaining: " + (size() - pos) + ")\n" + + verifier.thisCallstackEntry()); + verifier.assertTape(this, ""); + } + + final int pos = pos(); + Assertions.assertFalse(stillHas(1), "at: " + pos); + } + + public static VertexTape record(List nuggets) { + final var context = new VerticesSink(); + final var matrices = new MatrixStack(new Matrix4f()); + final var camera = new Vector3f(-3, 0, 0); + + final var sorted = EchoNugget.sortForCamera(nuggets, camera); + + for (final var nugget : sorted) { + nugget.draw(context, matrices, camera); + context.items.add(TestVertex.markerWith(0x00000000)); + } + + for (final var nugget : sorted) { + context.items.add(TestVertex.markerWith(0x00FFFFFF)); + nugget.sketch(context, matrices, camera); + } + + return context.items(); + } +} diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/VertexVerifier.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/VertexVerifier.java new file mode 100644 index 0000000..a9d1462 --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/VertexVerifier.java @@ -0,0 +1,25 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import com.midnightbits.scanner.rt.core.ScannerMod; +import com.midnightbits.scanner.sonar.graphics.Pixel; +import org.joml.Vector3f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public interface VertexVerifier { + String TAG = ScannerMod.MOD_ID; + Logger LOGGER = LoggerFactory.getLogger(TAG); + + int verticesNeeded(); + + void assertTape(VertexTape tape, String stack); + + String thisCallstackEntry(); + + static TestVertex apply(Pixel.Vertex v, Vector3f origin, int color) { + return new TestVertex(origin.add(v.x(), v.y(), v.z(), new Vector3f()), color); + } +} \ No newline at end of file diff --git a/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/VerticesSink.java b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/VerticesSink.java new file mode 100644 index 0000000..7e91145 --- /dev/null +++ b/scanner/src/test/java/com/midnightbits/scanner/utils/test/gl/VerticesSink.java @@ -0,0 +1,25 @@ +// Copyright (c) 2024 Marcin Zdun +// This code is licensed under MIT license (see LICENSE for details) + +package com.midnightbits.scanner.utils.test.gl; + +import com.midnightbits.scanner.sonar.graphics.GlProgramConsumer; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.List; + +public class VerticesSink implements GlProgramConsumer { + final List items = new ArrayList<>(); + + @Override + public void vertexColor(Matrix4f matrix, float x, float y, float z, int argb32) { + final var pos = matrix.transformPosition(x, y, z, new Vector3f()); + items.add(new TestVertex(pos, argb32)); + } + + VertexTape items() { + return new VertexTape(items); + } +} diff --git a/tools/ci/code/__main__.py b/tools/ci/code/__main__.py index 2028959..e229b55 100644 --- a/tools/ci/code/__main__.py +++ b/tools/ci/code/__main__.py @@ -72,8 +72,9 @@ def matches(matcher: re.Pattern[str], text: List[str]): def check_staged(): - proc = capture('git','diff','--name-only','--cached') - filenames = proc.stdout.decode("UTF-8").split("\n") + proc = capture('git','diff','--name-status','--cached') + lines = proc.stdout.decode("UTF-8").split("\n") + filenames = [line[1:].strip() for line in lines if line[:1] != 'D'] matcher, _ = build_regexp() files: Set[str] = set()