diff --git a/CLAUDE.md b/CLAUDE.md index 0aae4eeb..60402cd8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,10 @@ Dynmap is a dynamic web mapping plugin/mod for Minecraft servers. It's a multi-p # Build outputs go to /target directory # Build specific module (for faster iteration, but NOT for PR submissions) -./gradlew :fabric-1.18:build +./gradlew :DynmapCore:build + +# Run unit tests (DynmapCore only — JUnit 4) +./gradlew :DynmapCore:test # Forge 1.12.2 (requires JDK 8 - set JAVA_HOME accordingly) cd oldgradle @@ -27,20 +30,24 @@ cd oldgradle - Forge 1.12.2 (oldgradle): JDK 8 strictly required - Runtime targets: JDK 8 (1.16-), JDK 16 (1.17.x), JDK 17 (1.18-1.20.4), JDK 21 (1.20.5+) +**Build notes:** +- `gradle.properties` sets `org.gradle.parallel=false` and `org.gradle.daemon=false` — do not change these +- `snakeyaml` is pinned at 1.23 intentionally — newer versions break on Windows-encoded config files + ## Architecture -### Module Structure (71 modules total) +### Module Structure **Core Shared Modules:** -- `DynmapCoreAPI/` - Stable public API for external plugins/mods (markers, mod support, rendering) +- `DynmapCoreAPI/` - Stable public API for external plugins/mods (markers, mod support, rendering). Published to `repo.mikeprimm.com`. The `org.dynmap.renderer` package here defines `DynmapBlockState` — the central block state abstraction used everywhere. - `DynmapCore/` - Internal shared implementation (NOT stable - subject to breaking changes) - `dynmap-api/` - Bukkit-specific public API **Platform Implementations:** -- `spigot/` - Bukkit/PaperMC implementation -- `bukkit-helper-*` - Version-specific NMS code (25 versions: 1.13-1.21) -- `fabric-*` - Fabric mod implementations (14 versions: 1.14.4-1.21.11) -- `forge-*` - Forge mod implementations (14 versions: 1.14.4-1.21.11) +- `spigot/` - Bukkit/PaperMC implementation (`DynmapPlugin.java`) +- `bukkit-helper-*` - Version-specific NMS code (one per MC version: 1.13-1.21) +- `fabric-*` - Fabric mod implementations (1.14.4-1.21.x) +- `forge-*` - Forge mod implementations (1.14.4-1.21.x); `forge-1.12.2` lives in `oldgradle/` ### Dependency Flow ``` @@ -54,31 +61,47 @@ Platform-specific modules (Spigot, Fabric, Forge) ``` ### Key Components in DynmapCore -- `MapManager` - Tile/map rendering orchestration -- `DynmapCore.java` - Main coordination hub (~3,100 lines) -- `storage/` - Storage backends (FileTree, MySQL, PostgreSQL, SQLite, S3) -- `hdmap/` - HD map rendering (block models, shaders, textures) -- `web/` - Embedded Jetty server with servlets -- `markers/` - Marker system implementation + +- `DynmapCore.java` — Main coordination hub (~3,100 lines); bootstrapped by each platform +- `MapManager.java` — Tile rendering orchestration; owns the render thread pool and `FullWorldRenderState` queue +- `hdmap/` — HD map rendering pipeline: + - `IsoHDPerspective` — Isometric raytrace engine (the hot rendering path) + - `HDBlockModels` / `HDScaledBlockModels` — Block geometry (patch/volumetric/scaled models) + - `TexturePack` / `TexturePackLoader` — Texture resolution from resource packs + - `hdmap/renderer/` — Custom block renderers (stairs, fences, doors, etc.) implementing `CustomRenderer` + - Shaders (`DefaultHDShader`, `CaveHDShader`, `TopoHDShader`, etc.) — post-process pixel color + - Lighting (`DefaultHDLighting`, `ShadowHDLighting`, etc.) — light level calculation +- `storage/` — Storage backends (FileTree, MySQL, MariaDB, PostgreSQL, SQLite, MSSQL, AWS S3) +- `web/` — Embedded Jetty 9 server with custom HTTP routing (no standard servlet container) +- `markers/impl/` — Full marker system implementation; public interface is in `DynmapCoreAPI` +- `utils/MapChunkCache` + `utils/MapIterator` — Abstract interfaces that each platform implements to feed world data into the renderer + +### Platform Integration Pattern + +Each platform module (Spigot `bukkit-helper-*`, Fabric, Forge) must implement: +- `MapChunkCache` — Loads and caches chunk data for a tile's required chunks +- `MapIterator` — Block-by-block iteration over the loaded chunk cache +- A platform entry point (e.g., `DynmapPlugin` for Spigot) that bootstraps `DynmapCore` + +The `bukkit-helper-*` modules contain version-specific NMS code; `spigot/` delegates to the appropriate helper at runtime via reflection. + +## Testing + +Unit tests exist in `DynmapCore/src/test/` (JUnit 4) covering `Matrix3D`, `Vector3D`, `IpAddressMatcher`, `DynIntHashMap`, and `BufferInputStream`. Run with `./gradlew :DynmapCore:test`. + +Full verification requires: +1. Building all platforms: `./gradlew setup build` AND `cd oldgradle && ./gradlew setup build` +2. Manual testing on target Minecraft server platforms ## Critical Contribution Rules **PRs must build and test on ALL platforms including oldgradle. Changes to DynmapCore/DynmapCoreAPI require testing on all platforms.** -- **Java 8 compatibility required** - Code must compile and run on Java 8 -- **Java only** - No Kotlin, Scala, or other JVM languages -- **No dependency updates** - Library versions are tied to platform compatibility -- **No platform-specific code** - Must work on Windows, Linux (x86/ARM), macOS, Docker -- **Small PRs only** - One feature per PR, no style/formatting changes -- **No mod-specific code** - Use Dynmap APIs instead; external mods should depend on DynmapCoreAPI -- **Apache License v2** - All code must be compatible - -## Testing - -No automated tests exist. Verification is done by: -1. Building all platforms successfully (`./gradlew setup build` AND `cd oldgradle && ./gradlew setup build`) -2. Manual testing on target Minecraft server platforms - -## Storage Backends - -Dynmap supports: FileTree (default), MySQL/MariaDB, PostgreSQL, SQLite, MS SQL Server, AWS S3 +- **Java 8 compatibility required** — Code must compile and run on Java 8 +- **Java only** — No Kotlin, Scala, or other JVM languages +- **No dependency updates** — Library versions are tied to platform compatibility +- **No platform-specific code** — Must work on Windows, Linux (x86/ARM), macOS, Docker +- **Small PRs only** — One feature per PR, no style/formatting changes +- **No mod-specific code** — Use Dynmap APIs instead; external mods should depend on DynmapCoreAPI +- **Apache License v2** — All code must be compatible +- **DynmapCoreAPI is the only stable API** — Do not add external dependencies on DynmapCore internals diff --git a/DynmapCore/src/main/java/org/dynmap/hdmap/HDMapTile.java b/DynmapCore/src/main/java/org/dynmap/hdmap/HDMapTile.java index 7de9f818..e5ac719e 100644 --- a/DynmapCore/src/main/java/org/dynmap/hdmap/HDMapTile.java +++ b/DynmapCore/src/main/java/org/dynmap/hdmap/HDMapTile.java @@ -52,7 +52,13 @@ public class HDMapTile extends MapTile { @Override public int hashCode() { - return tx ^ ty ^ perspective.hashCode() ^ world.hashCode() ^ boostzoom ^ tilescale; + int h = perspective.hashCode(); + h = h * 31 + world.hashCode(); + h = h * 31 + tx; + h = h * 31 + ty; + h = h * 31 + boostzoom; + h = h * 31 + tilescale; + return h; } @Override @@ -87,19 +93,24 @@ public class HDMapTile extends MapTile { @Override public boolean isBlockTypeDataNeeded() { return MapManager.mapman.hdmapman.isBlockTypeDataNeeded(this); } + @Override public boolean render(MapChunkCache cache, String mapname) { return perspective.render(cache, this, mapname); } + @Override public List getRequiredChunks() { return perspective.getRequiredChunks(this); } + @Override public MapTile[] getAdjecentTiles() { return perspective.getAdjecentTiles(this); } + @Override public int tileOrdinalX() { return tx; } + @Override public int tileOrdinalY() { return ty; } } diff --git a/DynmapCore/src/main/java/org/dynmap/hdmap/IsoHDPerspective.java b/DynmapCore/src/main/java/org/dynmap/hdmap/IsoHDPerspective.java index 465b88f0..d487ae47 100644 --- a/DynmapCore/src/main/java/org/dynmap/hdmap/IsoHDPerspective.java +++ b/DynmapCore/src/main/java/org/dynmap/hdmap/IsoHDPerspective.java @@ -90,6 +90,7 @@ public class IsoHDPerspective implements HDPerspective { /* Scaled models for non-cube blocks */ private final HDScaledBlockModels scalemodels; private final int modscale; + private final int modscale2; /* modscale * modscale, precomputed for raytraceSubblock */ /* Section-level raytrace variables */ int sx, sy, sz; @@ -153,6 +154,7 @@ public class IsoHDPerspective implements HDPerspective { custom_meshes = new DynLongHashMap(4096); custom_fluid_meshes = new DynLongHashMap(4096); modscale = basemodscale << scaled; + modscale2 = modscale * modscale; scalemodels = HDBlockModels.getModelsForScale(basemodscale << scaled); } @@ -250,18 +252,22 @@ public class IsoHDPerspective implements HDPerspective { /** * Get pixel X coordinate */ + @Override public final int getPixelX() { return px; } /** * Get pixel Y coordinate */ + @Override public final int getPixelY() { return py; } /** * Get map iterator */ + @Override public final MapIterator getMapIterator() { return mapiter; } /** * Return submodel alpha value (-1 if no submodel rendered) */ + @Override public int getSubmodelAlpha() { return subalpha; } @@ -283,9 +289,9 @@ public class IsoHDPerspective implements HDPerspective { n = 1; /* Initial section coord */ - sx = fastFloor(top.x/16.0); - sy = fastFloor(top.y/16.0); - sz = fastFloor(top.z/16.0); + sx = fastFloor(top.x * 0.0625); + sy = fastFloor(top.y * 0.0625); + sz = fastFloor(top.z * 0.0625); /* Compute parametric step (dt) per step on each axis */ sdt_dx = 16.0 / dx; sdt_dy = 16.0 / dy; @@ -302,7 +308,7 @@ public class IsoHDPerspective implements HDPerspective { else if (bottom.x > top.x) { x_inc = 1; n += fastFloor(bottom.x) - x; - st_next_x = (fastFloor(top.x/16.0) + 1 - (top.x/16.0)) * sdt_dx; + st_next_x = (fastFloor(top.x * 0.0625) + 1 - (top.x * 0.0625)) * sdt_dx; stepx = BlockStep.X_PLUS; mxout = modscale; } @@ -310,7 +316,7 @@ public class IsoHDPerspective implements HDPerspective { else { x_inc = -1; n += x - fastFloor(bottom.x); - st_next_x = ((top.x/16.0) - fastFloor(top.x/16.0)) * sdt_dx; + st_next_x = ((top.x * 0.0625) - fastFloor(top.x * 0.0625)) * sdt_dx; stepx = BlockStep.X_MINUS; mxout = -1; } @@ -325,7 +331,7 @@ public class IsoHDPerspective implements HDPerspective { else if (bottom.y > top.y) { y_inc = 1; n += fastFloor(bottom.y) - y; - st_next_y = (fastFloor(top.y/16.0) + 1 - (top.y/16.0)) * sdt_dy; + st_next_y = (fastFloor(top.y * 0.0625) + 1 - (top.y * 0.0625)) * sdt_dy; stepy = BlockStep.Y_PLUS; myout = modscale; } @@ -333,7 +339,7 @@ public class IsoHDPerspective implements HDPerspective { else { y_inc = -1; n += y - fastFloor(bottom.y); - st_next_y = ((top.y/16.0) - fastFloor(top.y/16.0)) * sdt_dy; + st_next_y = ((top.y * 0.0625) - fastFloor(top.y * 0.0625)) * sdt_dy; stepy = BlockStep.Y_MINUS; myout = -1; } @@ -348,7 +354,7 @@ public class IsoHDPerspective implements HDPerspective { else if (bottom.z > top.z) { z_inc = 1; n += fastFloor(bottom.z) - z; - st_next_z = (fastFloor(top.z/16.0) + 1 - (top.z/16.0)) * sdt_dz; + st_next_z = (fastFloor(top.z * 0.0625) + 1 - (top.z * 0.0625)) * sdt_dz; stepz = BlockStep.Z_PLUS; mzout = modscale; } @@ -356,7 +362,7 @@ public class IsoHDPerspective implements HDPerspective { else { z_inc = -1; n += z - fastFloor(bottom.z); - st_next_z = ((top.z/16.0) - fastFloor(top.z/16.0)) * sdt_dz; + st_next_z = ((top.z * 0.0625) - fastFloor(top.z * 0.0625)) * sdt_dz; stepz = BlockStep.Z_MINUS; mzout = -1; } @@ -810,7 +816,7 @@ public class IsoHDPerspective implements HDPerspective { while(mt <= mtend) { if(!skip) { try { - int blkalpha = model[modscale*modscale*my + modscale*mz + mx]; + int blkalpha = model[modscale2*my + modscale*mz + mx]; if(blkalpha > 0) { subalpha = blkalpha; return false; @@ -857,6 +863,7 @@ public class IsoHDPerspective implements HDPerspective { return true; } + @Override public final int[] getSubblockCoord() { if(cur_patch >= 0) { /* If patch hit */ double tt = cur_patch_t; @@ -885,6 +892,7 @@ public class IsoHDPerspective implements HDPerspective { } // Is the hit on a cullable face? + @Override public final boolean isOnFace() { double tt; if(cur_patch >= 0) { /* If patch hit */ @@ -1294,11 +1302,14 @@ public class IsoHDPerspective implements HDPerspective { miny = tile.getDynmapWorld().minY; } - for(int x = 0; x < tileSize * sizescale; x++) { + final int tilePixelSize = tileSize * sizescale; + final double invSizescale = 1.0 / sizescale; + for(int x = 0; x < tilePixelSize; x++) { ps.px = x; - for(int y = 0; y < tileSize * sizescale; y++) { - ps.top.x = ps.bottom.x = xbase + (x + 0.5) / sizescale; /* Start at center of pixel at Y=height+0.5, bottom at Y=-0.5 */ - ps.top.y = ps.bottom.y = ybase + (y + 0.5) / sizescale; + final double px_center = xbase + (x + 0.5) * invSizescale; + for(int y = 0; y < tilePixelSize; y++) { + ps.top.x = ps.bottom.x = px_center; + ps.top.y = ps.bottom.y = ybase + (y + 0.5) * invSizescale; ps.top.z = height + 0.5; ps.bottom.z = miny - 0.5; map_to_world.transform(ps.top); /* Transform to world coordinates */ map_to_world.transform(ps.bottom); @@ -1314,6 +1325,7 @@ public class IsoHDPerspective implements HDPerspective { Log.severe("Error while raytracing tile: perspective=" + this.name + ", coord=" + mapiter.getX() + "," + mapiter.getY() + "," + mapiter.getZ() + ", blockid=" + mapiter.getBlockType() + ", lighting=" + mapiter.getBlockSkyLight() + ":" + mapiter.getBlockEmittedLight() + ", biome=" + mapiter.getBiome().toString(), ex); ex.printStackTrace(); } + final int rowOffset = (tilePixelSize - y - 1) * tilePixelSize + x; for(int i = 0; i < numshaders; i++) { if(shaderdone[i] == false) { shaderstate[i].rayFinished(ps); @@ -1325,21 +1337,11 @@ public class IsoHDPerspective implements HDPerspective { shaderstate[i].getRayColor(rslt, 0); int c_argb = rslt.getARGB(); if (c_argb != 0) rendered[i] = true; - if (isOpaque[i] && (c_argb == 0)) { - argb_buf[i][(tileSize*sizescale-y-1)*tileSize*sizescale + x] = bgnight[i]; - } - else { - argb_buf[i][(tileSize*sizescale-y-1)*tileSize*sizescale + x] = c_argb; - } + argb_buf[i][rowOffset] = (isOpaque[i] && (c_argb == 0)) ? bgnight[i] : c_argb; if (day_argb_buf[i] != null) { shaderstate[i].getRayColor(rslt, 1); c_argb = rslt.getARGB(); - if (isOpaque[i] && (c_argb == 0)) { - day_argb_buf[i][(tileSize*sizescale-y-1)*tileSize*sizescale + x] = bgday[i]; - } - else { - day_argb_buf[i][(tileSize*sizescale-y-1)*tileSize*sizescale + x] = c_argb; - } + day_argb_buf[i][rowOffset] = (isOpaque[i] && (c_argb == 0)) ? bgday[i] : c_argb; } } } diff --git a/DynmapCore/src/main/java/org/dynmap/hdmap/TexturePackHDShader.java b/DynmapCore/src/main/java/org/dynmap/hdmap/TexturePackHDShader.java index 8536d323..d76429f3 100644 --- a/DynmapCore/src/main/java/org/dynmap/hdmap/TexturePackHDShader.java +++ b/DynmapCore/src/main/java/org/dynmap/hdmap/TexturePackHDShader.java @@ -181,6 +181,7 @@ public class TexturePackHDShader implements HDShader { * Process next ray step - called for each block on route * @return true if ray is done, false if ray needs to continue */ + @Override public boolean processBlock(HDPerspectiveState ps) { DynmapBlockState blocktype = ps.getBlockState(); if ((hiddenids != null) && hiddenids.get(blocktype.globalStateIndex)) { @@ -265,7 +266,7 @@ public class TexturePackHDShader implements HDShader { if(color[0].isTransparent()) { for(int i = 0; i < color.length; i++) color[i].setColor(tmpcolor[i]); - return (color[0].getAlpha() == 255); + return (tmpcolor[0].getAlpha() == 255); } /* Else, blend and generate new alpha */ else { @@ -273,14 +274,18 @@ public class TexturePackHDShader implements HDShader { int alpha2 = tmpcolor[0].getAlpha() * (255-alpha) / 255; int talpha = alpha + alpha2; if(talpha > 0) - for(int i = 0; i < color.length; i++) - color[i].setRGBA((tmpcolor[i].getRed()*alpha2 + color[i].getRed()*alpha) / talpha, - (tmpcolor[i].getGreen()*alpha2 + color[i].getGreen()*alpha) / talpha, - (tmpcolor[i].getBlue()*alpha2 + color[i].getBlue()*alpha) / talpha, talpha); + for(int i = 0; i < color.length; i++) { + int tc = tmpcolor[i].getARGB(); + int cc = color[i].getARGB(); + color[i].setARGB((talpha << 24) + | (((((tc >> 16) & 0xFF) * alpha2 + ((cc >> 16) & 0xFF) * alpha) / talpha) << 16) + | (((((tc >> 8) & 0xFF) * alpha2 + ((cc >> 8) & 0xFF) * alpha) / talpha) << 8) + | ((( tc & 0xFF) * alpha2 + ( cc & 0xFF) * alpha) / talpha)); + } else for(int i = 0; i < color.length; i++) color[i].setTransparent(); - + return (talpha >= 254); /* If only one short, no meaningful contribution left */ } } @@ -290,6 +295,7 @@ public class TexturePackHDShader implements HDShader { /** * Ray ended - used to report that ray has exited map (called if renderer has not reported complete) */ + @Override public void rayFinished(HDPerspectiveState ps) { } /** @@ -297,12 +303,14 @@ public class TexturePackHDShader implements HDShader { * @param c - object to store color value in * @param index - index of color to request (renderer specific - 0=default, 1=day for night/day renderer */ + @Override public void getRayColor(Color c, int index) { c.setColor(color[index]); } /** * Clean up state object - called after last ray completed */ + @Override public void cleanup() { if (ctm_cache != null) { ctm_cache.clear(); @@ -338,11 +346,13 @@ public class TexturePackHDShader implements HDShader { * @param scale - scale of perspective * @return state object to use for all rays in tile */ + @Override public HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale) { return new ShaderState(mapiter, map, cache, scale); } /* Add shader's contributions to JSON for map object */ + @Override public void addClientConfiguration(JSONObject mapObject) { s(mapObject, "shader", name); }