diff --git a/Plan/build.gradle b/Plan/build.gradle index d595c63787..b2cf88ba49 100644 --- a/Plan/build.gradle +++ b/Plan/build.gradle @@ -11,7 +11,7 @@ plugins { id("base") id "org.sonarqube" version "7.3.1.8318" id "com.gradleup.shadow" version "9.6.1" apply false - id "net.fabricmc.fabric-loom" version "1.17.17" apply false + id "net.fabricmc.fabric-loom-remap" version "1.17.17" apply false id "net.neoforged.moddev" version "2.0.143" apply false } @@ -150,8 +150,24 @@ subprojects { } } + // Fabric + NeoForge on this branch target Minecraft 1.21.1 (Java 21 runtime). + plugins.withId("java") { + if (project.name == "fabric" || project.name == "neoforge") { + java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } + tasks.withType(JavaCompile).configureEach { + options.release = 21 + } + } + } + tasks.withType(JavaCompile).configureEach { - options.release = 25 + if (project.name != "fabric" && project.name != "neoforge") { + options.release = 25 + } options.encoding = "UTF-8" options.fork = true } diff --git a/Plan/fabric/build.gradle b/Plan/fabric/build.gradle index 34bbda9e71..96ba08ecf1 100644 --- a/Plan/fabric/build.gradle +++ b/Plan/fabric/build.gradle @@ -1,6 +1,6 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -apply plugin: "net.fabricmc.fabric-loom" +apply plugin: "net.fabricmc.fabric-loom-remap" apply plugin: "com.gradleup.shadow" configurations { @@ -16,9 +16,10 @@ dependencies { implementation "net.playeranalytics:platform-abstraction-layer-api:$palVersion" - minecraft "com.mojang:minecraft:26.2" - implementation "net.fabricmc:fabric-loader:0.19.3" - implementation("me.lucko:fabric-permissions-api:0.7.0") + minecraft "com.mojang:minecraft:1.21.1" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:0.16.14" + modImplementation("me.lucko:fabric-permissions-api:0.3.3") // Fabric API Set apiModules = [ @@ -31,7 +32,7 @@ dependencies { ] apiModules.forEach { - implementation(fabricApi.module(it, "0.153.0+26.2")) + modImplementation(fabricApi.module(it, "0.116.15+1.21.1")) } testImplementation(testFixtures(project(":common"))) @@ -39,7 +40,7 @@ dependencies { } compileJava { - options.release = 25 + options.release = 21 } processResources { diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/commands/FabricCommandManager.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/commands/FabricCommandManager.java index 1895e02a32..5c639a872f 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/commands/FabricCommandManager.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/commands/FabricCommandManager.java @@ -34,7 +34,6 @@ import net.minecraft.commands.Commands; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; -import net.minecraft.server.permissions.Permissions; import com.djrapitops.plan.PlanPlugin; import net.playeranalytics.plugin.scheduling.RunnableFactory; @@ -56,8 +55,15 @@ public FabricCommandManager(CommandDispatcher dispatcher, Pl public static boolean checkPermission(CommandSourceStack src, String permission) { if (isPermissionsApiAvailable()) { - return me.lucko.fabric.api.permissions.v0.Permissions.check(src, permission, 2); - } else if (src.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER)) { + try { + Class permissions = Class.forName("me.lucko.fabric.api.permissions.v0.Permissions"); + return (Boolean) permissions.getMethod("check", CommandSourceStack.class, String.class, int.class) + .invoke(null, src, permission, 2); + } catch (ReflectiveOperationException e) { + // Fall through to vanilla permission levels + } + } + if (src.hasPermission(2)) { return true; } else { return switch (permission) { diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/commands/use/FabricMessageBuilder.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/commands/use/FabricMessageBuilder.java index 70a8bd16a2..ef32b035c4 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/commands/use/FabricMessageBuilder.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/commands/use/FabricMessageBuilder.java @@ -25,7 +25,6 @@ import net.minecraft.network.chat.Component; import org.apache.commons.text.TextStringBuilder; -import java.net.URI; import java.util.Collection; public class FabricMessageBuilder implements MessageBuilder { @@ -59,31 +58,31 @@ public MessageBuilder newLine() { @Override public MessageBuilder link(String url) { - builder.withStyle(style -> style.withClickEvent(new ClickEvent.OpenUrl(URI.create(url)))); + builder.withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url))); return this; } @Override public MessageBuilder command(String command) { - builder.withStyle(style -> style.withClickEvent(new ClickEvent.RunCommand(command.charAt(0) == '/' ? command : '/' + command))); + builder.withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command.charAt(0) == '/' ? command : '/' + command))); return this; } @Override public MessageBuilder hover(String message) { - builder.withStyle(style -> style.withHoverEvent(new HoverEvent.ShowText(Component.literal(message)))); + builder.withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(message)))); return this; } @Override public MessageBuilder hover(String... lines) { - builder.withStyle(style -> style.withHoverEvent(new HoverEvent.ShowText(Component.literal(new TextStringBuilder().appendWithSeparators(lines, "\n").toString())))); + builder.withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(new TextStringBuilder().appendWithSeparators(lines, "\n").toString())))); return this; } @Override public MessageBuilder hover(Collection lines) { - builder.withStyle(style -> style.withHoverEvent(new HoverEvent.ShowText(Component.literal(new TextStringBuilder().appendWithSeparators(lines, "\n").toString())))); + builder.withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(new TextStringBuilder().appendWithSeparators(lines, "\n").toString())))); return this; } diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/domain/FabricPlayerData.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/domain/FabricPlayerData.java index 1a2c748ffb..ebf254b9b9 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/domain/FabricPlayerData.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/domain/FabricPlayerData.java @@ -46,7 +46,7 @@ public UUID getUUID() { @Override public String getName() { - return player.getGameProfile().name(); + return player.getGameProfile().getName(); } @Override @@ -56,12 +56,12 @@ public Optional getDisplayName() { @Override public Optional isOperator() { - return Optional.of(server.getPlayerList().getOps().get(player.nameAndId()) != null); + return Optional.of(server.getPlayerList().getOps().get(player.getGameProfile()) != null); } @Override public Optional getCurrentWorld() { - return Optional.of(player.level().dimension().identifier().toString()); + return Optional.of(player.level().dimension().location().toString()); } @Override diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/events/PlanFabricEvents.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/events/PlanFabricEvents.java index 1d4d75227c..80ab104c83 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/events/PlanFabricEvents.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/events/PlanFabricEvents.java @@ -22,13 +22,13 @@ import net.minecraft.world.entity.LivingEntity; import net.minecraft.network.protocol.handshake.ClientIntentionPacket; import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; -import net.minecraft.server.players.NameAndId; import net.minecraft.commands.CommandSourceStack; import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.minecraft.server.level.ServerPlayer; import net.minecraft.network.chat.Component; import net.minecraft.world.level.GameType; import com.djrapitops.plan.PlanPlugin; +import com.mojang.authlib.GameProfile; import net.playeranalytics.plan.gathering.FabricPlayerPositionTracker; import java.net.SocketAddress; @@ -173,7 +173,7 @@ public interface OnLogin { * @param profile the profile of the player * @param reason the provided kick reason (null if player is permitted to join) */ - void onLogin(SocketAddress address, NameAndId profile, Component reason); + void onLogin(SocketAddress address, GameProfile profile, Component reason); } @FunctionalInterface diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/DeathEventListener.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/DeathEventListener.java index 1c4b4432b8..5f16c98e8c 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/DeathEventListener.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/DeathEventListener.java @@ -66,7 +66,7 @@ public void register() { return; } - ServerEntityCombatEvents.AFTER_KILLED_OTHER_ENTITY.register((world, killer, killedEntity, damageSource) -> + ServerEntityCombatEvents.AFTER_KILLED_OTHER_ENTITY.register((world, killer, killedEntity) -> { if (!this.isEnabled) { return; @@ -131,7 +131,7 @@ public String findWeapon(Entity killer) { private String getItemInHand(ServerPlayer killer) { ItemStack itemInHand = killer.getMainHandItem(); - return itemInHand.getItemName().getString(); + return itemInHand.getHoverName().getString(); } private Optional getShooter(Projectile projectile) { diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/FabricAFKListener.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/FabricAFKListener.java index b0eef6b984..4d07ff9ff1 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/FabricAFKListener.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/FabricAFKListener.java @@ -20,7 +20,6 @@ import com.djrapitops.plan.settings.config.PlanConfig; import com.djrapitops.plan.utilities.logging.ErrorContext; import com.djrapitops.plan.utilities.logging.ErrorLogger; -import me.lucko.fabric.api.permissions.v0.Permissions; import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.minecraft.server.level.ServerPlayer; @@ -84,7 +83,13 @@ private void event(ServerPlayer player) { private boolean checkPermission(ServerPlayer player, String permission) { if (FabricCommandManager.isPermissionsApiAvailable()) { - return Permissions.check(player, permission); + try { + Class permissions = Class.forName("me.lucko.fabric.api.permissions.v0.Permissions"); + return (Boolean) permissions.getMethod("check", ServerPlayer.class, String.class) + .invoke(null, player, permission); + } catch (ReflectiveOperationException e) { + return false; + } } else { return false; } diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/GameModeChangeListener.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/GameModeChangeListener.java index f58c79b575..b3a0ddf020 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/GameModeChangeListener.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/GameModeChangeListener.java @@ -75,7 +75,7 @@ private void actOnEvent(ServerPlayer player, GameType newGameMode) { UUID uuid = player.getUUID(); long time = System.currentTimeMillis(); String gameMode = newGameMode.name(); - String worldName = player.level().dimension().identifier().toString(); + String worldName = player.level().dimension().location().toString(); dbSystem.getDatabase().executeTransaction(new StoreWorldNameTransaction(serverInfo.getServerUUID(), worldName)); worldAliasSettings.addWorld(worldName); diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/PlayerOnlineListener.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/PlayerOnlineListener.java index c62deb0be5..0be7902c60 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/PlayerOnlineListener.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/PlayerOnlineListener.java @@ -33,7 +33,6 @@ import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.minecraft.network.protocol.handshake.ClientIntent; import net.minecraft.network.protocol.handshake.ClientIntentionPacket; -import net.minecraft.server.players.NameAndId; import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.server.level.ServerPlayer; import net.playeranalytics.plan.gathering.FabricPlayerPositionTracker; @@ -141,9 +140,9 @@ private void onHandshake(ClientIntentionPacket packet) { } } - public void onPlayerLogin(SocketAddress address, NameAndId profile, boolean banned) { + public void onPlayerLogin(SocketAddress address, GameProfile profile, boolean banned) { try { - UUID playerUUID = profile.id(); + UUID playerUUID = profile.getId(); ServerUUID serverUUID = serverInfo.getServerUUID(); String playerJoinAddress = joinAddress.get(); diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/WorldChangeListener.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/WorldChangeListener.java index 1e624795ce..52900ef5d1 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/WorldChangeListener.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/listeners/fabric/WorldChangeListener.java @@ -24,7 +24,7 @@ import com.djrapitops.plan.storage.database.transactions.events.StoreWorldNameTransaction; import com.djrapitops.plan.utilities.logging.ErrorContext; import com.djrapitops.plan.utilities.logging.ErrorLogger; -import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents; import net.minecraft.server.level.ServerPlayer; import net.playeranalytics.plan.gathering.listeners.FabricListener; @@ -70,7 +70,7 @@ private void actOnEvent(ServerPlayer player) { UUID uuid = player.getUUID(); - String worldName = player.level().dimension().identifier().toString(); + String worldName = player.level().dimension().location().toString(); String gameMode = player.gameMode.getGameModeForPlayer().name(); dbSystem.getDatabase().executeTransaction(new StoreWorldNameTransaction(serverInfo.getServerUUID(), worldName)); @@ -86,7 +86,7 @@ public void register() { return; } - ServerEntityLevelChangeEvents.AFTER_PLAYER_CHANGE_LEVEL.register((player, _, _) -> { + ServerEntityWorldChangeEvents.AFTER_PLAYER_CHANGE_WORLD.register((player, origin, destination) -> { if (!this.isEnabled) { return; } diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/mixin/CommandSourceStackMixin.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/mixin/CommandSourceStackMixin.java index 77ed878705..a6a292ec9c 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/mixin/CommandSourceStackMixin.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/mixin/CommandSourceStackMixin.java @@ -59,7 +59,7 @@ public MessageBuilder buildMessage() { @Override public Optional getPlayerName() { - return getPlayer().map(ServerPlayer::getGameProfile).map(GameProfile::name); + return getPlayer().map(ServerPlayer::getGameProfile).map(GameProfile::getName); } @Override diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/mixin/PlayerListMixin.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/mixin/PlayerListMixin.java index 32ffb606f6..cfca2e1497 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/mixin/PlayerListMixin.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/mixin/PlayerListMixin.java @@ -16,7 +16,7 @@ */ package net.playeranalytics.plan.gathering.mixin; -import net.minecraft.server.players.NameAndId; +import com.mojang.authlib.GameProfile; import net.minecraft.server.players.PlayerList; import net.minecraft.network.chat.Component; import net.playeranalytics.plan.gathering.listeners.events.PlanFabricEvents; @@ -31,8 +31,8 @@ public class PlayerListMixin { @Inject(method = "canPlayerLogin", at = @At(value = "TAIL")) - public void onLogin(SocketAddress address, NameAndId nameAndId, CallbackInfoReturnable cir) { - PlanFabricEvents.ON_LOGIN.invoker().onLogin(address, nameAndId, cir.getReturnValue()); + public void onLogin(SocketAddress address, GameProfile profile, CallbackInfoReturnable cir) { + PlanFabricEvents.ON_LOGIN.invoker().onLogin(address, profile, cir.getReturnValue()); } } diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/identification/properties/FabricServerProperties.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/identification/properties/FabricServerProperties.java index 29e7b6aa70..f1b49f30e3 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/identification/properties/FabricServerProperties.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/identification/properties/FabricServerProperties.java @@ -37,7 +37,7 @@ public FabricServerProperties(DedicatedServer server) { FabricLoader.getInstance().getModContainer("fabricloader").map(modContainer -> modContainer.getMetadata().getVersion().getFriendlyString()).orElse("Unknown") + " (loader)", () -> (server.getLocalIp() == null) ? "" : server.getLocalIp(), - server.getProperties().maxPlayers.get() + server.getMaxPlayers() ); } } diff --git a/Plan/fabric/src/main/resources/fabric.mod.json b/Plan/fabric/src/main/resources/fabric.mod.json index be80876b98..027b6e12b2 100644 --- a/Plan/fabric/src/main/resources/fabric.mod.json +++ b/Plan/fabric/src/main/resources/fabric.mod.json @@ -13,13 +13,13 @@ "plan.mixins.json" ], "depends": { - "minecraft": ">=26.1", - "java": ">=25", - "fabricloader": ">=0.18.6", + "minecraft": "~1.21.1", + "java": ">=21", + "fabricloader": ">=0.16.0", "fabric-api": "*" }, "suggests": { - "fabric-permissions-api-v0": "^0.7.0", + "fabric-permissions-api-v0": "*", "luckperms": "*" } } diff --git a/Plan/fabric/src/main/resources/plan.mixins.json b/Plan/fabric/src/main/resources/plan.mixins.json index 7a5e155805..ba311ecd0d 100644 --- a/Plan/fabric/src/main/resources/plan.mixins.json +++ b/Plan/fabric/src/main/resources/plan.mixins.json @@ -2,7 +2,7 @@ "required": true, "minVersion": "0.8", "package": "net.playeranalytics.plan.gathering.mixin", - "compatibilityLevel": "JAVA_25", + "compatibilityLevel": "JAVA_21", "mixins": [ "ClientToServerHandshakePacketMixin", "KickCommandMixin", diff --git a/Plan/neoforge/build.gradle b/Plan/neoforge/build.gradle index 834bc220a8..f906a26dd1 100644 --- a/Plan/neoforge/build.gradle +++ b/Plan/neoforge/build.gradle @@ -4,10 +4,9 @@ apply plugin: "net.neoforged.moddev" apply plugin: "com.gradleup.shadow" ext { - neoVersion = "26.2.0.59" - minecraftVersion = "26.2" - // FFAPI has no 26.2 release yet; 26.1.2 is the stand-in until Sinytra ships 26.2. - ffapiVersion = "0.155.2+26.1.2+3.5.0" + neoVersion = "21.1.244" + minecraftVersion = "1.21.1" + ffapiVersion = "0.116.15+2.3.2+1.21.1" } configurations { @@ -15,6 +14,19 @@ configurations { implementation.extendsFrom shade } +// Minecraft 1.21.1 declares strict versions for shared libraries; Plan common uses newer ones. +// Force MC's versions on the compile classpath so ModDevGradle can resolve. +configurations.configureEach { + resolutionStrategy { + force "org.apache.commons:commons-lang3:3.14.0" + force "org.apache.commons:commons-compress:1.26.0" + force "commons-codec:commons-codec:1.16.0" + force "commons-io:commons-io:2.15.1" + force "com.google.code.gson:gson:2.10.1" + force "org.slf4j:slf4j-api:2.0.9" + } +} + // Reuse Fabric platform code that is not Fabric-Loader specific. sourceSets { main { @@ -48,6 +60,11 @@ sourceSets { neoForge { version = neoVersion + parchment { + mappingsVersion = "2024.11.17" + minecraftVersion = "1.21.1" + } + runs { server { server() @@ -74,15 +91,12 @@ dependencies { // Required at runtime; not shaded into PlanNeoForge.jar implementation "org.sinytra.forgified-fabric-api:forgified-fabric-api:$ffapiVersion" - // Optional permissions API (same package as Fabric); present when a NeoForge port is installed - compileOnly "me.lucko:fabric-permissions-api:0.7.0" - testImplementation(testFixtures(project(":common"))) testImplementation(project(":extensions:adventure")) } compileJava { - options.release = 25 + options.release = 21 } processResources { diff --git a/Plan/neoforge/src/main/java/net/playeranalytics/plan/identification/properties/NeoForgeServerProperties.java b/Plan/neoforge/src/main/java/net/playeranalytics/plan/identification/properties/NeoForgeServerProperties.java index 53712dd817..e33beadc8b 100644 --- a/Plan/neoforge/src/main/java/net/playeranalytics/plan/identification/properties/NeoForgeServerProperties.java +++ b/Plan/neoforge/src/main/java/net/playeranalytics/plan/identification/properties/NeoForgeServerProperties.java @@ -39,7 +39,7 @@ public NeoForgeServerProperties(DedicatedServer server) { .orElse("Unknown") + " (FFAPI)", () -> (server.getLocalIp() == null) ? "" : server.getLocalIp(), - server.getProperties().maxPlayers.get() + server.getMaxPlayers() ); } } diff --git a/README.md b/README.md index 9344a80056..aa9811f5a1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Plan supports multiple different platforms and versions; Spigot, Paper, Fabric, Fabric and NeoForge each ship as separate jars (`PlanFabric-*.jar`, `PlanNeoForge-*.jar`). Other platforms share one jar. +Minecraft **1.21.1** Fabric + NeoForge builds are on the `1.21.1` branch (`PlanFabric-*.jar`, `PlanNeoForge-*.jar`; Java 21, Fabric API / FFAPI 1.21.1). + - [Version 5 Release trailer on Youtube](https://www.youtube.com/watch?v=BS_Ti9zkoRc) ## API