From 62e3603555e09bc147083c5341fbd6a43ccd2a58 Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Sat, 26 Jul 2025 08:27:18 -0600 Subject: [PATCH 1/3] modrinth: prioritize target loader when resolving project versions --- .../helpers/modrinth/ModrinthApiClient.java | 52 ++- .../modrinth/ModrinthApiClientTest.java | 88 ++-- .../modrinth/projects-entityculling.json | 390 ++++++++++++++++++ .../versions-entityculling-forge.json | 44 ++ ...ions-entityculling-neoforge-not-forge.json | 1 + 5 files changed, 537 insertions(+), 38 deletions(-) create mode 100644 src/test/resources/__files/modrinth/projects-entityculling.json create mode 100644 src/test/resources/__files/modrinth/versions-entityculling-forge.json create mode 100644 src/test/resources/__files/modrinth/versions-entityculling-neoforge-not-forge.json diff --git a/src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java b/src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java index 224a7c0e..76295b3e 100644 --- a/src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java +++ b/src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java @@ -7,6 +7,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -181,31 +182,52 @@ public Mono downloadMrPack(VersionFile versionFile) { public Mono> getVersionsForProject(String projectIdOrSlug, @Nullable Loader loader, String gameVersion ) { - return sharedFetch.fetch( - uriBuilder.resolve("/v2/project/{id|slug}/version", - queryParameters() - .addStringArray("loaders", expandCompatibleLoaders(loader)) - .addStringArray("game_versions", gameVersion), - projectIdOrSlug + return getJustVersionsForProject(projectIdOrSlug, gameVersion, + loader != null ? Collections.singletonList(loader.toString()) : null + ) + .switchIfEmpty( + getJustVersionsForProject(projectIdOrSlug, gameVersion, + expandCompatibleLoaders(loader) ) ) - .toObjectList(Version.class) - .assemble() - .flatMap(versions -> - versions.isEmpty() ? - getProject(projectIdOrSlug) - .flatMap(project -> Mono.error(new NoFilesAvailableException(project, loader, gameVersion))) - : Mono.just(versions) - ); + .switchIfEmpty( + getProject(projectIdOrSlug) + .flatMap(project -> Mono.error(new NoFilesAvailableException(project, loader, gameVersion))) + ); + } + + /** + * @return the non-empty list of versions or an empty mono + */ + private Mono> getJustVersionsForProject(String projectIdOrSlug, String gameVersion, + Collection loaderNames + ) { + return + loaderNames == null || loaderNames.isEmpty() ? + Mono.empty() : + sharedFetch.fetch( + uriBuilder.resolve("/v2/project/{id|slug}/version", + queryParameters() + .addStringArray("loaders", loaderNames) + .addStringArray("game_versions", gameVersion), + projectIdOrSlug + ) + ) + .toObjectList(Version.class) + .assemble() + .filter(versions -> !versions.isEmpty()); } + /** + * @param loader the target loader + * @return a list of the compatible loaders + */ private List expandCompatibleLoaders(@Nullable Loader loader) { if (loader == null) { return null; } final ArrayList expanded = new ArrayList<>(); - expanded.add(loader.toString()); Loader compatibleWith = loader; while ((compatibleWith = compatibleWith.getCompatibleWith()) != null) { expanded.add(compatibleWith.toString()); diff --git a/src/test/java/me/itzg/helpers/modrinth/ModrinthApiClientTest.java b/src/test/java/me/itzg/helpers/modrinth/ModrinthApiClientTest.java index f0c9177c..869928ea 100644 --- a/src/test/java/me/itzg/helpers/modrinth/ModrinthApiClientTest.java +++ b/src/test/java/me/itzg/helpers/modrinth/ModrinthApiClientTest.java @@ -45,33 +45,75 @@ void getBulkProjectsWithUnknownServerSide(WireMockRuntimeInfo wmInfo) { } } - @Test - void getVersionsForProject(WireMockRuntimeInfo wmInfo) { - - stubFor(get(urlPathMatching("/v2/project/(BITzwT7B|clickvillagers)/version")) - .withQueryParam("loaders", equalTo("[\"purpur\",\"paper\",\"spigot\"]")) - .withQueryParam("game_versions", equalTo("[\"1.20.1\"]")) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBodyFile("modrinth/project-BITzwT7B-version-resp.json") - ) - ); + @Nested + class getVersionsForProject { + @Test + void exactLoader(WireMockRuntimeInfo wmInfo) { - try (ModrinthApiClient client = new ModrinthApiClient(wmInfo.getHttpBaseUrl(), "modrinth", Options.builder().build())) { - final List result = client.getVersionsForProject("BITzwT7B", Loader.purpur, "1.20.1") - .block(); + stubFor(get(urlPathMatching("/v2/project/(BITzwT7B|clickvillagers)/version")) + .withQueryParam("loaders", equalTo("[\"purpur\"]")) + .withQueryParam("game_versions", equalTo("[\"1.20.1\"]")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBodyFile("modrinth/project-BITzwT7B-version-resp.json") + ) + ); + + try (ModrinthApiClient client = new ModrinthApiClient(wmInfo.getHttpBaseUrl(), "modrinth", Options.builder().build())) { + final List result = client.getVersionsForProject("BITzwT7B", Loader.purpur, "1.20.1") + .block(); + + assertThat(result) + .hasSize(3) + .extracting(Version::getId) + .containsExactly( + "O9nndrTu", + "DfUyEmsH", + "oUJMLDhz" + ); + } + } + + @Test + void fallbackToCompatibleLoader(WireMockRuntimeInfo wmInfo) { + stubFor(get(urlPathMatching("/v2/project/entityculling/version")) + .withQueryParam("loaders", equalTo("[\"neoforge\"]")) + .withQueryParam("game_versions", equalTo("[\"1.12.2\"]")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBodyFile("modrinth/versions-entityculling-neoforge-not-forge.json") + ) + ); + stubFor(get(urlPathMatching("/v2/project/entityculling/version")) + .withQueryParam("loaders", equalTo("[\"forge\"]")) + .withQueryParam("game_versions", equalTo("[\"1.12.2\"]")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBodyFile("modrinth/versions-entityculling-forge.json") + ) + ); - assertThat(result) - .hasSize(3) - .extracting(Version::getId) - .containsExactly( - "O9nndrTu", - "DfUyEmsH", - "oUJMLDhz" - ); + try (ModrinthApiClient client = new ModrinthApiClient(wmInfo.getHttpBaseUrl(), "modrinth", + Options.builder().build() + )) { + final List result = client.getVersionsForProject( + "entityculling", + Loader.neoforge, + "1.12.2" + ) + .block(); + + assertThat(result) + .extracting(Version::getId) + .containsExactly( + "knltv3Vh" + ); + } } } + + @Nested class resolveProjectVersion { @@ -142,7 +184,7 @@ void noFiles(WireMockRuntimeInfo wmInfo) { @Test void noApplicableVersionsOfType(WireMockRuntimeInfo wmInfo) { stubFor(get(urlPathMatching("/v2/project/(3wmN97b8|multiverse-core)/version")) - .withQueryParam("loaders", equalTo("[\"purpur\",\"paper\",\"spigot\"]")) + .withQueryParam("loaders", equalTo("[\"purpur\"]")) .withQueryParam("game_versions", equalTo("[\"1.21.1\"]")) .willReturn(aResponse() .withHeader("Content-Type", "application/json") diff --git a/src/test/resources/__files/modrinth/projects-entityculling.json b/src/test/resources/__files/modrinth/projects-entityculling.json new file mode 100644 index 00000000..0c096c25 --- /dev/null +++ b/src/test/resources/__files/modrinth/projects-entityculling.json @@ -0,0 +1,390 @@ +[ + { + "client_side": "required", + "server_side": "unsupported", + "game_versions": [ + "b1.7.3", + "1.7.10", + "1.8.8", + "1.8.9", + "1.12.2", + "1.16", + "1.16.1", + "1.16.2", + "1.16.3", + "1.16.4", + "1.16.5", + "1.17", + "1.17.1", + "1.18", + "1.18.1", + "1.18.2", + "1.19", + "1.19.1", + "1.19.2", + "1.19.3-pre1", + "1.19.3-pre2", + "1.19.3", + "1.19.4", + "1.20", + "1.20.1", + "1.20.2", + "1.20.4", + "1.20.5", + "1.20.6", + "1.21", + "1.21.1", + "1.21.3", + "1.21.4", + "1.21.5", + "1.21.6", + "1.21.7", + "1.21.8" + ], + "id": "NNAgCjsB", + "slug": "entityculling", + "project_type": "mod", + "team": "hLPrl6Gq", + "organization": null, + "title": "Entity Culling", + "description": "Using async path-tracing to hide Block-/Entities that are not visible", + "body": "![Entity Culling Banner](https://tr7zw.github.io/uikit/banner/header_entity_culling.png)\n\n

\n \"Discord\"\n \"GitHub\"\n \"GitHub\"\n \"GitHub\"\n \"Ko-fi\"\n

\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n\"About\"\n\nModern Minecraft rendering is fast - but not always smart. Why render block entities and mobs hidden behind walls or ceilings when you can skip them entirely?\n\nThis mod introduces **asynchronous path-tracing** to efficiently determine what's actually visible to the player. By leveraging multiple CPU cores, it calculates line-of-sight visibility in real-time and eliminates unnecessary draw calls for hidden block entities and entities.\n\nThe result? \nSmarter rendering. Less overhead. More performance.\n\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Features](https://tr7zw.github.io/uikit/headlines/large/Features.png)\n\n\nUnlock performance by only rendering what matters. This mod goes beyond the basics to bring next-level visibility optimization to your game.\n\n### Multithreaded Path-Tracing\n- Uses spare CPU threads to calculate visibility fast\n- Runs alongside the main game thread without blocking\n- Updates visibility data in real-time\n\n### Smart Occlusion Culling\n- Skips block entities and mobs hidden behind terrain or structures\n- Works like Minecraft's back-face culling, but smarter\n- Reduces GPU load without sacrificing visual fidelity\n\n### Entity Tick Optimization\n- Reduces client impact from unseen entities\n- Keeps maintenance to a minimum\n- Fully configurable and compatible with most mods\n\n
\n\n[![Essential](https://tr7zw.github.io/uikit/banner/essential_1.png)](https://essential.gg/x/tr7zw)

\n![Need a 24/7 Server? Check this out!](https://tr7zw.github.io/uikit/banner/shockbyte_divider.png)\n[![Shockbyte](https://tr7zw.github.io/uikit/banner/shockbyte_small.png?v=1)](http://bit.ly/4bczSJY)\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Compatibility & Dependancys](https://tr7zw.github.io/uikit/headlines/medium/Compatibility%20&%20Dependancys.png?v=1)\n\n| Minecraft | Loader | Status | Version | Note |\n| ------------- | --------------------- | ---------------- | -------- | ------------------------------------- |\n| 1.19.4+ | Fabric/Forge/NeoForge | ✅ Supported | Latest | |\n| 1.16.5-1.19.2 | Fabric/Forge | ⚠️ planned update | Outdated | Will be moved into supported later on |\n| 1.12.2/1.8.9 | Forge | ❌ Not supported | Outdated | Might get new updates at some point |\n| 1.7.10 | Forge | ❌ Not supported | Outdated | No updates planned |\n| b1.7.3 | Babric | ❌ Not supported | Outdated | No updates planned |\n\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Screenshots / Media](https://tr7zw.github.io/uikit/headlines/medium/Screenshots%20Media.png)\n\n\n![Stage View](https://tr7zw.github.io/uikit/screens/entityculling_compare.png) \n*Direct compare with/without EntityCulling active. Place: Scarland Hermitcraft9, Mods: Sodium, Iris, ImmediatleyFast, 16 Render distance*\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Known Issues](https://tr7zw.github.io/uikit/headlines/medium/Known%20Issues.png)\n\nClientside entities, usually used by for example magic mods for their animations, don't work as expected. Whitelist the entity via the config screen for tick culling and/or entity culling.\n\nBlock entities that render far outside their bounds need to be whitelisted too. An example for that is the vanilla Beacon, Creates pulleys or certain Botania blocks.\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![FAQ](https://tr7zw.github.io/uikit/headlines/medium/FAQ.png)\n\n### Does this need to be installed on the server?\n\nNo. This is a fully client-side mod and does **not** need to be installed on the server.\n\n### Will this affect mob behavior or farms?\n\nNo. The mod only skips rendering-not simulation. Mobs will still spawn, move, and drop items as usual. Your farms and other gameplay mechanics remain unaffected.\n\n### I have \"Use Entity Culling\" enabled in Sodium - does this still help?\n\nYes. Sodium performs a basic visibility check based on loaded chunks. This mod goes further by analyzing actual line-of-sight, skipping entities that are technically in visible chunks but **not** actually visible to the player. It’s a much more aggressive and accurate approach.\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Credits & license](https://tr7zw.github.io/uikit/headlines/medium/Credits%20&%20License.png)\n\n👤 Thanks to RoboTricker for his Transport-Pipes plugin, which created the foundation for this mod.

\n👤 Thanks to vicisacat for the Babric Beta1.7.3 backport.

\n👤 Thanks to Pelotrio for the Forge 1.7.10 backport.

\n👤 Thanks to the awesome translators and contributors on Github! \n\n \"Link\"\n

\n📄 License: tr7zw Protective License
\nFeel free to use this mod in your Modrinth/Curseforge hosted modpacks without asking for permission. Do not redistribute the jar files anywhere else!", + "body_url": null, + "published": "2022-05-25T10:09:09.784330Z", + "updated": "2025-07-17T19:31:48.306725Z", + "approved": "2022-05-25T10:09:09.784330Z", + "queued": null, + "status": "approved", + "requested_status": null, + "moderator_message": null, + "license": { + "id": "LicenseRef-tr7zw-Protective-License", + "name": "", + "url": "https://github.com/tr7zw/EntityCulling/blob/1.18/LICENSE-EntityCulling" + }, + "downloads": 42472580, + "followers": 10698, + "categories": [ + "optimization" + ], + "additional_categories": [], + "loaders": [ + "babric", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "versions": [ + "WiIRU3UP", + "AXYavMt5", + "Qek7k92S", + "2WBkUuat", + "2mzFyULh", + "mlP64Wt9", + "4GD4dwQK", + "LQ3vI7aU", + "WdlVJYKE", + "R0KPBo5r", + "1Vc3QD0W", + "b2cBCjB4", + "bRBO9ZzX", + "Wq0up4B4", + "bE2TQ25z", + "reIgRPky", + "N7HJbtfe", + "AdkBUIRs", + "WR6z6MYC", + "twdGLDHv", + "jFORrKV6", + "d3CbGntl", + "UvJN5Cy4", + "NB6TPjI8", + "374z4jcq", + "d20sUcYn", + "17Hi340V", + "BDwHAdWc", + "1MBmcSf1", + "LwvhfrTE", + "tUVHg6HN", + "pVb6ajsR", + "DkksI437", + "KSu61vcB", + "Ek0VIX9q", + "mBYyh1qv", + "HSirwtwV", + "2I1m95aJ", + "M5k1QJL1", + "knltv3Vh", + "iC2A1KnJ", + "McVB9vXT", + "7JR5qJ8f", + "dmSSEQmZ", + "l1SLPhB6", + "bpznplxY", + "sXviAk4G", + "SLiFyIon", + "bketoxVs", + "frGGaFrx", + "gO8hxcyv", + "ugIRhLoi", + "2LyjkgAv", + "PG8gMKcT", + "4Nq7hEtl", + "hyejM5za", + "SzEdoick", + "UHcskdEO", + "92bPVm5F", + "DHBIGCNn", + "F3RXDl1W", + "TOyoOLwn", + "7ZgGD02s", + "KRGWwoZW", + "1LVdHUaX", + "3VAtIiWX", + "cj8nR3eG", + "VgDjay7P", + "p2L3IXut", + "6U5rh26a", + "Gggee1My", + "LXLEbjiC", + "Bu3hSiJb", + "KGMvyJTe", + "mahLIqpj", + "haJ4L3vJ", + "lil4k3tE", + "7F6Kp0Ay", + "HZeMEZEy", + "vg0SC4Y7", + "xWH6Uo7w", + "iqty9WKv", + "opQrd8FA", + "17GdZTv7", + "iVUgWfyD", + "VgprkY4E", + "uh3DtMnM", + "9OiMQqUE", + "rWxSnbQW", + "USSckz12", + "nLlovYRU", + "dlg7nMa6", + "1JrKE0F6", + "q0jula9L", + "uYs3FOuQ", + "DbsD7DRt", + "2GnM6hql", + "SMIhog68", + "hyG3PpKt", + "bTufFvSs", + "RlFFB3Eb", + "EDlaQvPy", + "dJTijYHW", + "RpWp3udp", + "kPbJu8eF", + "hyvZeH2S", + "vJd4SVYM", + "HEmWV8r3", + "HTvBVCkY", + "MriILdtF", + "QVn7Zkkn", + "vpVMTgho", + "SkGUnTlv", + "yFTJuVix", + "H1MsVESS", + "5VdBcDhP", + "suQzhN9O", + "oBKKk7wM", + "7XH6aw0F", + "78EFhW55", + "gRnWPDZr", + "aQk7X2Um", + "hD1f5amG", + "BfPqO59c", + "Dohm7yVT", + "8RrU4f3Q", + "984y5O9Q", + "O4LDHBYA", + "a47vgutS", + "HKmDd12e", + "yyU9Ifp0", + "naHGCw23", + "abLzsv7N", + "VtXA9pjB", + "XQl3JgdV", + "7RXnzzpg", + "YG4HSXiu", + "rvBTLEjw", + "zUmLd6Ay", + "z1HWSU0r", + "qyLJvqbT", + "kVHgBQjB", + "8BvicUz9", + "MQuJQtw8", + "IiizWoaa", + "OsSGAtrF", + "J3KKcEph", + "vY2ZrSKE", + "CMaJype5", + "NkBXk0Ye", + "UiNyTZ2B", + "IMngHgbZ", + "ljh2gd1z", + "A4kW8ZGE", + "SdwRMvNg", + "zdOMtwS8", + "ATY4UapL", + "SaTAbHqD", + "clMavz5z", + "beCKgkki", + "qGUpVtAF", + "ksDFnBlC", + "4VVnjqtK", + "pNCpW9zH", + "5AHrqDAC", + "qPGBO8P3", + "4HW5j3ty", + "ssEdOyua", + "xTSXggU9", + "kHLcnFaX", + "j1us2Hr4", + "q1e5epY7", + "zhaIPVOm", + "p8LaakE9", + "awuP447k", + "PGpP6jLM", + "xuiHEnFJ", + "kMC7OLoZ", + "b80qT9D6", + "5q7U4mNZ", + "WMF8hBIk", + "8GWUYuCH", + "9dd2MtG9", + "5ojDlUV1", + "WiyavApn", + "CV9OuZWX", + "5GFbYP8u", + "eKX1ek3L", + "bSihRPs8", + "jNAhVci4", + "IHP61uNg", + "MD0tUwMF", + "jX7qgwbL", + "W4olzuUc", + "oxrgXur4", + "aGPgF33y", + "29GV7fju", + "so0QWknA", + "F5yQEwdG", + "DD7q7vgN", + "pOQxZDeP", + "JrOhNEba", + "DQD98FEX", + "GqUjinse", + "kh7IqiuI", + "8nU55hPD", + "1sz2Jus9", + "n9Ya0saf", + "PSpKP3ek", + "UaznDXn8", + "8CQCj5Pr", + "74bGyUlZ", + "wZSIbKbB", + "u5kVRErA", + "OHiFDase", + "SzXXrL9v", + "D7kYv61h", + "ceRdzWgy", + "z2R65zWm", + "2BwJZLax", + "7x6nFVLO", + "gOriLpko", + "31XzMzi1", + "EpyMGO7z", + "pzOrNdiZ", + "BRJboQUV", + "lLgVfM4T", + "f7xgzjZ7", + "AKMg2PfU", + "7CYEXwpe", + "UOhdZxPT", + "yaZLTmz5", + "TM62koET", + "4B7PkcP0", + "NL7QORF1", + "QmxSWoRc", + "qt6sX76t", + "NEVsXcOA", + "dXKsH3zt", + "rRAVfdAH", + "84LNy1pR", + "ec013Tlo", + "Z8Figf1c", + "mkaSHVxm", + "ltpVQ8V5", + "lSsw7xU4", + "5oZ3eZcA", + "9RTEYkay", + "zTcfQzqQ", + "o9bmJ5sd", + "bMbYTvrj", + "dFjJiu7S", + "wxuS4Die", + "NeKSdd0F", + "oJ5voewi", + "Qu62cqxc", + "Y5EvlGcX", + "3TCgPw5u", + "zinofwiX", + "XPmLiwzh", + "vXfgeq62", + "N8Pc5ShO", + "fV7BWjfG", + "OBmlWFWJ", + "L6ZZ5I6h", + "DLyOaeSV", + "o23M6jaJ", + "emidtnQg", + "ivkfruZP", + "DutsY8u5", + "11PvLmko", + "9YSa2XLY", + "z4BvnYaN", + "k0VmRjMB", + "DfqQiPRw", + "zkVXtfi7", + "o8ZjoqnL", + "S79XoZhx", + "wX3eEE9h", + "UtZ4LBGg", + "5wVZFo2d", + "ev6PFt64", + "rFvPqrY3" + ], + "icon_url": "https://cdn.modrinth.com/data/NNAgCjsB/7873452d6cede4daed12da3d7d8c193ab88b4fd6_96.webp", + "issues_url": "https://github.com/tr7zw/EntityCulling/issues", + "source_url": "https://github.com/tr7zw/EntityCulling", + "wiki_url": null, + "discord_url": "https://discord.gg/fWtMAkFaZF", + "donation_urls": [ + { + "id": "ko-fi", + "platform": "Ko-fi", + "url": "https://ko-fi.com/tr7zw" + } + ], + "gallery": [ + { + "url": "https://cdn.modrinth.com/data/NNAgCjsB/images/65781fbd7cda31b8c8e4a8def40bf445c10a1562.gif", + "raw_url": "https://cdn.modrinth.com/data/NNAgCjsB/images/65781fbd7cda31b8c8e4a8def40bf445c10a1562.gif", + "featured": true, + "title": "Example of F3 stats", + "description": null, + "created": "2023-01-21T19:13:36.454610Z", + "ordering": 0 + } + ], + "color": 6052955, + "thread_id": "NNAgCjsB", + "monetization_status": "monetized" + } +] \ No newline at end of file diff --git a/src/test/resources/__files/modrinth/versions-entityculling-forge.json b/src/test/resources/__files/modrinth/versions-entityculling-forge.json new file mode 100644 index 00000000..3a6db38a --- /dev/null +++ b/src/test/resources/__files/modrinth/versions-entityculling-forge.json @@ -0,0 +1,44 @@ +[ + { + "game_versions": [ + "1.12.2" + ], + "loaders": [ + "forge" + ], + "id": "knltv3Vh", + "project_id": "NNAgCjsB", + "author_id": "Qnt13hO8", + "featured": false, + "name": "1.6.3-1.12.2 - Forge", + "version_number": "1.6.3-1.12.2", + "changelog": "Initial backport to 1.12.2.", + "changelog_url": null, + "date_published": "2024-03-09T20:04:35.562295Z", + "downloads": 293074, + "version_type": "release", + "status": "listed", + "requested_status": null, + "files": [ + { + "hashes": { + "sha512": "97c4def2a2979a0ba61c0fb016762756cf8361cdff651558dbb91cbe3d8627c388436d31ec2b93ff62df99a7ff589033184db4e6f298f4b8fdd4acf4c83ba202", + "sha1": "fb22f03b297ca7b1bb7c6d5948e46c12e0a24446" + }, + "url": "https://cdn.modrinth.com/data/NNAgCjsB/versions/knltv3Vh/entityculling-1.12.2-1.6.3.jar", + "filename": "entityculling-1.12.2-1.6.3.jar", + "primary": true, + "size": 41478, + "file_type": null + } + ], + "dependencies": [ + { + "version_id": null, + "project_id": "G1ckZuWK", + "file_name": null, + "dependency_type": "required" + } + ] + } +] \ No newline at end of file diff --git a/src/test/resources/__files/modrinth/versions-entityculling-neoforge-not-forge.json b/src/test/resources/__files/modrinth/versions-entityculling-neoforge-not-forge.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/src/test/resources/__files/modrinth/versions-entityculling-neoforge-not-forge.json @@ -0,0 +1 @@ +[] \ No newline at end of file From 084c7328b956efd01a30421ccfd0efe31730606a Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Sat, 26 Jul 2025 08:28:17 -0600 Subject: [PATCH 2/3] removed extra test file --- .../modrinth/projects-entityculling.json | 390 ------------------ 1 file changed, 390 deletions(-) delete mode 100644 src/test/resources/__files/modrinth/projects-entityculling.json diff --git a/src/test/resources/__files/modrinth/projects-entityculling.json b/src/test/resources/__files/modrinth/projects-entityculling.json deleted file mode 100644 index 0c096c25..00000000 --- a/src/test/resources/__files/modrinth/projects-entityculling.json +++ /dev/null @@ -1,390 +0,0 @@ -[ - { - "client_side": "required", - "server_side": "unsupported", - "game_versions": [ - "b1.7.3", - "1.7.10", - "1.8.8", - "1.8.9", - "1.12.2", - "1.16", - "1.16.1", - "1.16.2", - "1.16.3", - "1.16.4", - "1.16.5", - "1.17", - "1.17.1", - "1.18", - "1.18.1", - "1.18.2", - "1.19", - "1.19.1", - "1.19.2", - "1.19.3-pre1", - "1.19.3-pre2", - "1.19.3", - "1.19.4", - "1.20", - "1.20.1", - "1.20.2", - "1.20.4", - "1.20.5", - "1.20.6", - "1.21", - "1.21.1", - "1.21.3", - "1.21.4", - "1.21.5", - "1.21.6", - "1.21.7", - "1.21.8" - ], - "id": "NNAgCjsB", - "slug": "entityculling", - "project_type": "mod", - "team": "hLPrl6Gq", - "organization": null, - "title": "Entity Culling", - "description": "Using async path-tracing to hide Block-/Entities that are not visible", - "body": "![Entity Culling Banner](https://tr7zw.github.io/uikit/banner/header_entity_culling.png)\n\n

\n \"Discord\"\n \"GitHub\"\n \"GitHub\"\n \"GitHub\"\n \"Ko-fi\"\n

\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n\"About\"\n\nModern Minecraft rendering is fast - but not always smart. Why render block entities and mobs hidden behind walls or ceilings when you can skip them entirely?\n\nThis mod introduces **asynchronous path-tracing** to efficiently determine what's actually visible to the player. By leveraging multiple CPU cores, it calculates line-of-sight visibility in real-time and eliminates unnecessary draw calls for hidden block entities and entities.\n\nThe result? \nSmarter rendering. Less overhead. More performance.\n\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Features](https://tr7zw.github.io/uikit/headlines/large/Features.png)\n\n\nUnlock performance by only rendering what matters. This mod goes beyond the basics to bring next-level visibility optimization to your game.\n\n### Multithreaded Path-Tracing\n- Uses spare CPU threads to calculate visibility fast\n- Runs alongside the main game thread without blocking\n- Updates visibility data in real-time\n\n### Smart Occlusion Culling\n- Skips block entities and mobs hidden behind terrain or structures\n- Works like Minecraft's back-face culling, but smarter\n- Reduces GPU load without sacrificing visual fidelity\n\n### Entity Tick Optimization\n- Reduces client impact from unseen entities\n- Keeps maintenance to a minimum\n- Fully configurable and compatible with most mods\n\n
\n\n[![Essential](https://tr7zw.github.io/uikit/banner/essential_1.png)](https://essential.gg/x/tr7zw)

\n![Need a 24/7 Server? Check this out!](https://tr7zw.github.io/uikit/banner/shockbyte_divider.png)\n[![Shockbyte](https://tr7zw.github.io/uikit/banner/shockbyte_small.png?v=1)](http://bit.ly/4bczSJY)\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Compatibility & Dependancys](https://tr7zw.github.io/uikit/headlines/medium/Compatibility%20&%20Dependancys.png?v=1)\n\n| Minecraft | Loader | Status | Version | Note |\n| ------------- | --------------------- | ---------------- | -------- | ------------------------------------- |\n| 1.19.4+ | Fabric/Forge/NeoForge | ✅ Supported | Latest | |\n| 1.16.5-1.19.2 | Fabric/Forge | ⚠️ planned update | Outdated | Will be moved into supported later on |\n| 1.12.2/1.8.9 | Forge | ❌ Not supported | Outdated | Might get new updates at some point |\n| 1.7.10 | Forge | ❌ Not supported | Outdated | No updates planned |\n| b1.7.3 | Babric | ❌ Not supported | Outdated | No updates planned |\n\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Screenshots / Media](https://tr7zw.github.io/uikit/headlines/medium/Screenshots%20Media.png)\n\n\n![Stage View](https://tr7zw.github.io/uikit/screens/entityculling_compare.png) \n*Direct compare with/without EntityCulling active. Place: Scarland Hermitcraft9, Mods: Sodium, Iris, ImmediatleyFast, 16 Render distance*\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Known Issues](https://tr7zw.github.io/uikit/headlines/medium/Known%20Issues.png)\n\nClientside entities, usually used by for example magic mods for their animations, don't work as expected. Whitelist the entity via the config screen for tick culling and/or entity culling.\n\nBlock entities that render far outside their bounds need to be whitelisted too. An example for that is the vanilla Beacon, Creates pulleys or certain Botania blocks.\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![FAQ](https://tr7zw.github.io/uikit/headlines/medium/FAQ.png)\n\n### Does this need to be installed on the server?\n\nNo. This is a fully client-side mod and does **not** need to be installed on the server.\n\n### Will this affect mob behavior or farms?\n\nNo. The mod only skips rendering-not simulation. Mobs will still spawn, move, and drop items as usual. Your farms and other gameplay mechanics remain unaffected.\n\n### I have \"Use Entity Culling\" enabled in Sodium - does this still help?\n\nYes. Sodium performs a basic visibility check based on loaded chunks. This mod goes further by analyzing actual line-of-sight, skipping entities that are technically in visible chunks but **not** actually visible to the player. It’s a much more aggressive and accurate approach.\n\n
![Divider](https://tr7zw.github.io/uikit/divider_faded/Divider_01.png)\n\n
![Credits & license](https://tr7zw.github.io/uikit/headlines/medium/Credits%20&%20License.png)\n\n👤 Thanks to RoboTricker for his Transport-Pipes plugin, which created the foundation for this mod.

\n👤 Thanks to vicisacat for the Babric Beta1.7.3 backport.

\n👤 Thanks to Pelotrio for the Forge 1.7.10 backport.

\n👤 Thanks to the awesome translators and contributors on Github! \n\n \"Link\"\n

\n📄 License: tr7zw Protective License
\nFeel free to use this mod in your Modrinth/Curseforge hosted modpacks without asking for permission. Do not redistribute the jar files anywhere else!", - "body_url": null, - "published": "2022-05-25T10:09:09.784330Z", - "updated": "2025-07-17T19:31:48.306725Z", - "approved": "2022-05-25T10:09:09.784330Z", - "queued": null, - "status": "approved", - "requested_status": null, - "moderator_message": null, - "license": { - "id": "LicenseRef-tr7zw-Protective-License", - "name": "", - "url": "https://github.com/tr7zw/EntityCulling/blob/1.18/LICENSE-EntityCulling" - }, - "downloads": 42472580, - "followers": 10698, - "categories": [ - "optimization" - ], - "additional_categories": [], - "loaders": [ - "babric", - "fabric", - "forge", - "neoforge", - "quilt" - ], - "versions": [ - "WiIRU3UP", - "AXYavMt5", - "Qek7k92S", - "2WBkUuat", - "2mzFyULh", - "mlP64Wt9", - "4GD4dwQK", - "LQ3vI7aU", - "WdlVJYKE", - "R0KPBo5r", - "1Vc3QD0W", - "b2cBCjB4", - "bRBO9ZzX", - "Wq0up4B4", - "bE2TQ25z", - "reIgRPky", - "N7HJbtfe", - "AdkBUIRs", - "WR6z6MYC", - "twdGLDHv", - "jFORrKV6", - "d3CbGntl", - "UvJN5Cy4", - "NB6TPjI8", - "374z4jcq", - "d20sUcYn", - "17Hi340V", - "BDwHAdWc", - "1MBmcSf1", - "LwvhfrTE", - "tUVHg6HN", - "pVb6ajsR", - "DkksI437", - "KSu61vcB", - "Ek0VIX9q", - "mBYyh1qv", - "HSirwtwV", - "2I1m95aJ", - "M5k1QJL1", - "knltv3Vh", - "iC2A1KnJ", - "McVB9vXT", - "7JR5qJ8f", - "dmSSEQmZ", - "l1SLPhB6", - "bpznplxY", - "sXviAk4G", - "SLiFyIon", - "bketoxVs", - "frGGaFrx", - "gO8hxcyv", - "ugIRhLoi", - "2LyjkgAv", - "PG8gMKcT", - "4Nq7hEtl", - "hyejM5za", - "SzEdoick", - "UHcskdEO", - "92bPVm5F", - "DHBIGCNn", - "F3RXDl1W", - "TOyoOLwn", - "7ZgGD02s", - "KRGWwoZW", - "1LVdHUaX", - "3VAtIiWX", - "cj8nR3eG", - "VgDjay7P", - "p2L3IXut", - "6U5rh26a", - "Gggee1My", - "LXLEbjiC", - "Bu3hSiJb", - "KGMvyJTe", - "mahLIqpj", - "haJ4L3vJ", - "lil4k3tE", - "7F6Kp0Ay", - "HZeMEZEy", - "vg0SC4Y7", - "xWH6Uo7w", - "iqty9WKv", - "opQrd8FA", - "17GdZTv7", - "iVUgWfyD", - "VgprkY4E", - "uh3DtMnM", - "9OiMQqUE", - "rWxSnbQW", - "USSckz12", - "nLlovYRU", - "dlg7nMa6", - "1JrKE0F6", - "q0jula9L", - "uYs3FOuQ", - "DbsD7DRt", - "2GnM6hql", - "SMIhog68", - "hyG3PpKt", - "bTufFvSs", - "RlFFB3Eb", - "EDlaQvPy", - "dJTijYHW", - "RpWp3udp", - "kPbJu8eF", - "hyvZeH2S", - "vJd4SVYM", - "HEmWV8r3", - "HTvBVCkY", - "MriILdtF", - "QVn7Zkkn", - "vpVMTgho", - "SkGUnTlv", - "yFTJuVix", - "H1MsVESS", - "5VdBcDhP", - "suQzhN9O", - "oBKKk7wM", - "7XH6aw0F", - "78EFhW55", - "gRnWPDZr", - "aQk7X2Um", - "hD1f5amG", - "BfPqO59c", - "Dohm7yVT", - "8RrU4f3Q", - "984y5O9Q", - "O4LDHBYA", - "a47vgutS", - "HKmDd12e", - "yyU9Ifp0", - "naHGCw23", - "abLzsv7N", - "VtXA9pjB", - "XQl3JgdV", - "7RXnzzpg", - "YG4HSXiu", - "rvBTLEjw", - "zUmLd6Ay", - "z1HWSU0r", - "qyLJvqbT", - "kVHgBQjB", - "8BvicUz9", - "MQuJQtw8", - "IiizWoaa", - "OsSGAtrF", - "J3KKcEph", - "vY2ZrSKE", - "CMaJype5", - "NkBXk0Ye", - "UiNyTZ2B", - "IMngHgbZ", - "ljh2gd1z", - "A4kW8ZGE", - "SdwRMvNg", - "zdOMtwS8", - "ATY4UapL", - "SaTAbHqD", - "clMavz5z", - "beCKgkki", - "qGUpVtAF", - "ksDFnBlC", - "4VVnjqtK", - "pNCpW9zH", - "5AHrqDAC", - "qPGBO8P3", - "4HW5j3ty", - "ssEdOyua", - "xTSXggU9", - "kHLcnFaX", - "j1us2Hr4", - "q1e5epY7", - "zhaIPVOm", - "p8LaakE9", - "awuP447k", - "PGpP6jLM", - "xuiHEnFJ", - "kMC7OLoZ", - "b80qT9D6", - "5q7U4mNZ", - "WMF8hBIk", - "8GWUYuCH", - "9dd2MtG9", - "5ojDlUV1", - "WiyavApn", - "CV9OuZWX", - "5GFbYP8u", - "eKX1ek3L", - "bSihRPs8", - "jNAhVci4", - "IHP61uNg", - "MD0tUwMF", - "jX7qgwbL", - "W4olzuUc", - "oxrgXur4", - "aGPgF33y", - "29GV7fju", - "so0QWknA", - "F5yQEwdG", - "DD7q7vgN", - "pOQxZDeP", - "JrOhNEba", - "DQD98FEX", - "GqUjinse", - "kh7IqiuI", - "8nU55hPD", - "1sz2Jus9", - "n9Ya0saf", - "PSpKP3ek", - "UaznDXn8", - "8CQCj5Pr", - "74bGyUlZ", - "wZSIbKbB", - "u5kVRErA", - "OHiFDase", - "SzXXrL9v", - "D7kYv61h", - "ceRdzWgy", - "z2R65zWm", - "2BwJZLax", - "7x6nFVLO", - "gOriLpko", - "31XzMzi1", - "EpyMGO7z", - "pzOrNdiZ", - "BRJboQUV", - "lLgVfM4T", - "f7xgzjZ7", - "AKMg2PfU", - "7CYEXwpe", - "UOhdZxPT", - "yaZLTmz5", - "TM62koET", - "4B7PkcP0", - "NL7QORF1", - "QmxSWoRc", - "qt6sX76t", - "NEVsXcOA", - "dXKsH3zt", - "rRAVfdAH", - "84LNy1pR", - "ec013Tlo", - "Z8Figf1c", - "mkaSHVxm", - "ltpVQ8V5", - "lSsw7xU4", - "5oZ3eZcA", - "9RTEYkay", - "zTcfQzqQ", - "o9bmJ5sd", - "bMbYTvrj", - "dFjJiu7S", - "wxuS4Die", - "NeKSdd0F", - "oJ5voewi", - "Qu62cqxc", - "Y5EvlGcX", - "3TCgPw5u", - "zinofwiX", - "XPmLiwzh", - "vXfgeq62", - "N8Pc5ShO", - "fV7BWjfG", - "OBmlWFWJ", - "L6ZZ5I6h", - "DLyOaeSV", - "o23M6jaJ", - "emidtnQg", - "ivkfruZP", - "DutsY8u5", - "11PvLmko", - "9YSa2XLY", - "z4BvnYaN", - "k0VmRjMB", - "DfqQiPRw", - "zkVXtfi7", - "o8ZjoqnL", - "S79XoZhx", - "wX3eEE9h", - "UtZ4LBGg", - "5wVZFo2d", - "ev6PFt64", - "rFvPqrY3" - ], - "icon_url": "https://cdn.modrinth.com/data/NNAgCjsB/7873452d6cede4daed12da3d7d8c193ab88b4fd6_96.webp", - "issues_url": "https://github.com/tr7zw/EntityCulling/issues", - "source_url": "https://github.com/tr7zw/EntityCulling", - "wiki_url": null, - "discord_url": "https://discord.gg/fWtMAkFaZF", - "donation_urls": [ - { - "id": "ko-fi", - "platform": "Ko-fi", - "url": "https://ko-fi.com/tr7zw" - } - ], - "gallery": [ - { - "url": "https://cdn.modrinth.com/data/NNAgCjsB/images/65781fbd7cda31b8c8e4a8def40bf445c10a1562.gif", - "raw_url": "https://cdn.modrinth.com/data/NNAgCjsB/images/65781fbd7cda31b8c8e4a8def40bf445c10a1562.gif", - "featured": true, - "title": "Example of F3 stats", - "description": null, - "created": "2023-01-21T19:13:36.454610Z", - "ordering": 0 - } - ], - "color": 6052955, - "thread_id": "NNAgCjsB", - "monetization_status": "monetized" - } -] \ No newline at end of file From cb0c622cdc635613ef1ab4a88bbba212111180fe Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Sat, 26 Jul 2025 08:53:44 -0600 Subject: [PATCH 3/3] Corrected fallback handling for existing cases --- .../itzg/helpers/modrinth/ModrinthApiClient.java | 14 +++++++++----- .../itzg/helpers/modrinth/ModrinthCommandTest.java | 11 ++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java b/src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java index 76295b3e..f455bd04 100644 --- a/src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java +++ b/src/main/java/me/itzg/helpers/modrinth/ModrinthApiClient.java @@ -183,11 +183,13 @@ public Mono> getVersionsForProject(String projectIdOrSlug, @Nullable Loader loader, String gameVersion ) { return getJustVersionsForProject(projectIdOrSlug, gameVersion, - loader != null ? Collections.singletonList(loader.toString()) : null + loader != null ? Collections.singletonList(loader.toString()) : null, + false ) .switchIfEmpty( getJustVersionsForProject(projectIdOrSlug, gameVersion, - expandCompatibleLoaders(loader) + expandCompatibleLoaders(loader), + true ) ) .switchIfEmpty( @@ -199,11 +201,13 @@ public Mono> getVersionsForProject(String projectIdOrSlug, /** * @return the non-empty list of versions or an empty mono */ - private Mono> getJustVersionsForProject(String projectIdOrSlug, String gameVersion, - Collection loaderNames + private Mono> getJustVersionsForProject( + String projectIdOrSlug, String gameVersion, + Collection loaderNames, + boolean skipEmptyLoaders ) { return - loaderNames == null || loaderNames.isEmpty() ? + skipEmptyLoaders && (loaderNames == null || loaderNames.isEmpty()) ? Mono.empty() : sharedFetch.fetch( uriBuilder.resolve("/v2/project/{id|slug}/version", diff --git a/src/test/java/me/itzg/helpers/modrinth/ModrinthCommandTest.java b/src/test/java/me/itzg/helpers/modrinth/ModrinthCommandTest.java index 48ea9a08..8e495f4b 100644 --- a/src/test/java/me/itzg/helpers/modrinth/ModrinthCommandTest.java +++ b/src/test/java/me/itzg/helpers/modrinth/ModrinthCommandTest.java @@ -14,8 +14,8 @@ import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; -import java.nio.file.Path; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.function.Consumer; import me.itzg.helpers.LatchingExecutionExceptionHandler; @@ -167,7 +167,8 @@ void failsWhenNoDependenciesForModLoader(@TempDir Path tempDir) throws JsonProce .put("project_id", requiredDepProjectId) .put("dependency_type", "required"); }); - stubVersionRequestEmptyResponse(requiredDepProjectId); + stubVersionRequestEmptyResponse(requiredDepProjectId, "paper"); + stubVersionRequestEmptyResponse(requiredDepProjectId, "spigot"); stubGetProject(requiredDepProjectId, new Project().setProjectType(ProjectType.resourcepack)); stubDownload(); @@ -462,7 +463,7 @@ private void stubVersionRequest(String projectId, String versionId, Consumer