diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c32bad0c8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +insert_final_newline = true +tab_width = 4 +max_line_length = off + +[*.java] +ij_java_class_count_to_use_import_on_demand = 9999 +ij_java_doc_align_exception_comments = false +ij_java_doc_align_param_comments = false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b5a0fd93..3bf011be5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: # See https://github.com/actions/setup-java/commits uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 with: - java-version: 17 + java-version: 21 distribution: temurin - name: Setup Gradle diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 8d59fecfd..09967de48 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -26,7 +26,7 @@ jobs: # See https://github.com/actions/setup-java/commits uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 with: - java-version: 17 + java-version: 21 distribution: temurin - name: Setup Gradle diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 7bb29bf50..376119f2e 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,3 +1,8 @@ +plugins { + id("floodgate.publish-conventions") + id("floodgate.shadow-conventions") +} + dependencies { api(libs.cumulus) api(libs.events) diff --git a/build.gradle.kts b/build.gradle.kts index dc49b9313..780af7daa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,62 +1,3 @@ plugins { - `java-library` - alias(libs.plugins.micronaut) apply false - alias(libs.plugins.lombok) apply false -} - -allprojects { - group = "org.geysermc.floodgate" - description = "Allows Bedrock players to join Java edition servers while keeping the server in online mode" - - apply { - plugin("net.kyori.indra.git") - } - - if (shouldAddBranchName()) { - version = versionWithBranchName() - } -} - -//todo differentiate maven publishing from downloads publishing -val deployProjects = setOf( - projects.api, - projects.coreCommon, - projects.coreNetty4, - projects.isolation, - projects.bungee, - projects.spigot, - projects.velocity, - projects.bungeeBase, - projects.spigotBase, - projects.velocityBase, - projects.universal -).map { it.dependencyProject } - -val shadowProjects = setOf( - projects.api, - projects.coreCommon, - projects.coreNetty4, - projects.bungeeBase, - projects.spigotBase, - projects.velocityBase, - projects.universal -).map { it.dependencyProject } - -//todo re-add checkstyle when we switch back to 2 space indention -// and take a look again at spotbugs someday - -subprojects { - apply { - plugin("java-library") - plugin("io.freefair.lombok") - } - - when (this) { - in deployProjects -> plugins.apply("floodgate.publish-conventions") - else -> plugins.apply("floodgate.base-conventions") - } - - if (this in shadowProjects) { - plugins.apply("floodgate.shadow-conventions") - } + id("floodgate.base-conventions") } \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 91c44bc39..2fc98113c 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -4,12 +4,25 @@ plugins { repositories { gradlePluginPortal() + mavenCentral() + maven("https://maven.architectury.dev/") + maven("https://maven.fabricmc.net/") + maven("https://maven.neoforged.net/releases/") } dependencies { + // Used to access version catalogue from the convention plugins + // this is OK as long as the same version catalog is used in the main build and build-logic + // see https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + implementation(libs.lombok) + implementation(libs.micronaut) implementation(libs.indra.common) implementation(libs.indra.git) implementation(libs.indra.licenser.spotless) implementation(libs.shadow) implementation(libs.gradle.idea.ext) + implementation(libs.architectury.plugin) + implementation(libs.architectury.loom) + //implementation(libs.minotaur) TODO modrinth publishing } diff --git a/buildSrc/src/main/kotlin/LibsAccessor.kt b/buildSrc/src/main/kotlin/LibsAccessor.kt new file mode 100644 index 000000000..7d7d0fb29 --- /dev/null +++ b/buildSrc/src/main/kotlin/LibsAccessor.kt @@ -0,0 +1,6 @@ +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType +import org.gradle.accessors.dm.LibrariesForLibs + +val Project.libs: LibrariesForLibs + get() = rootProject.extensions.getByType() diff --git a/buildSrc/src/main/kotlin/floodgate.base-conventions.gradle.kts b/buildSrc/src/main/kotlin/floodgate.base-conventions.gradle.kts index d8e973eb9..76f2f2da2 100644 --- a/buildSrc/src/main/kotlin/floodgate.base-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/floodgate.base-conventions.gradle.kts @@ -5,8 +5,14 @@ plugins { id("net.kyori.indra.git") // id("net.kyori.indra.licenser.spotless") id("floodgate.depsize") + id("io.freefair.lombok") } +val rootProperties: Map = project.rootProject.properties +group = rootProperties["group"] as String + "." + rootProperties["id"] as String +version = if (shouldAddBranchName()) versionWithBranchName() else rootProperties["version"] as String +description = rootProperties["description"] as String + dependencies { compileOnly("org.checkerframework", "checker-qual", "3.19.0") } @@ -42,8 +48,28 @@ tasks { "version" to fullVersion(), "description" to project.description, "url" to "https://geysermc.org", - "author" to "GeyserMC" + "author" to "GeyserMC", + "minecraft_version" to libs.versions.minecraft.version.get() ) } } -} \ No newline at end of file +} + +repositories { + mavenCentral() + maven("https://repo.opencollab.dev/main/") + + maven("https://maven.fabricmc.net/") + maven("https://maven.neoforged.net/releases") + maven("https://repo.papermc.io/repository/maven-public") + + // Spigot + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots") { + mavenContent { snapshotsOnly() } + } + + maven("https://libraries.minecraft.net") { + name = "minecraft" + mavenContent { releasesOnly() } + } +} diff --git a/buildSrc/src/main/kotlin/floodgate.modded-conventions.gradle.kts b/buildSrc/src/main/kotlin/floodgate.modded-conventions.gradle.kts new file mode 100644 index 000000000..cc8924a29 --- /dev/null +++ b/buildSrc/src/main/kotlin/floodgate.modded-conventions.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("floodgate.base-conventions") + id("floodgate.shadow-conventions") + id("architectury-plugin") + id("dev.architectury.loom") +} + +configurations { + create("includeTransitive").isTransitive = true + create("shadowBundle") { + isCanBeResolved = true + isCanBeConsumed = false + isTransitive = false + } +} + +architectury { + minecraft = libs.versions.minecraft.version.get() +} + +loom { + silentMojangMappingsLicense() +} + +indra { + javaVersions { + target(21) + } +} + +tasks { + shadowJar { + // Mirrors the example fabric project, otherwise tons of dependencies are shaded that shouldn't be + configurations = listOf(project.configurations.getByName("shadowBundle")) + // The remapped shadowJar is the final desired mod jar + archiveVersion.set(project.version.toString()) + archiveClassifier.set("shaded") + } +} + +dependencies { + minecraft(libs.minecraft) + mappings(loom.officialMojangMappings()) +} diff --git a/buildSrc/src/main/kotlin/floodgate.publish-conventions.gradle.kts b/buildSrc/src/main/kotlin/floodgate.publish-conventions.gradle.kts index a07994637..13e45cd14 100644 --- a/buildSrc/src/main/kotlin/floodgate.publish-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/floodgate.publish-conventions.gradle.kts @@ -6,4 +6,11 @@ plugins { indra { publishSnapshotsTo("geysermc", "https://repo.opencollab.dev/maven-snapshots") publishReleasesTo("geysermc", "https://repo.opencollab.dev/maven-releases") -} \ No newline at end of file +} + +// TODO +//publishing { +// // skip shadow jar from publishing. Workaround for https://github.com/johnrengelman/shadow/issues/651 +// val javaComponent = project.components["java"] as AdhocComponentWithVariants +// javaComponent.withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) { skip() } +//} diff --git a/bungee/base/build.gradle.kts b/bungee/base/build.gradle.kts index 95bc90737..4fd5ccf63 100644 --- a/bungee/base/build.gradle.kts +++ b/bungee/base/build.gradle.kts @@ -1,3 +1,8 @@ +plugins { + id("floodgate.shadow-conventions") + id("floodgate.publish-conventions") +} + dependencies { api(projects.coreNetty4) annotationProcessor(projects.coreNetty4) diff --git a/bungee/isolated/build.gradle.kts b/bungee/isolated/build.gradle.kts index 2cac9ec71..b51b363c8 100644 --- a/bungee/isolated/build.gradle.kts +++ b/bungee/isolated/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - java + id("floodgate.base-conventions") } dependencies { diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 8dc4fc3c7..0542d1fd1 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -1,6 +1,8 @@ plugins { + id("floodgate.publish-conventions") id("floodgate.generate-templates") id("floodgate.dependency-hash") + id("floodgate.shadow-conventions") id("io.micronaut.library") } diff --git a/core/netty4/build.gradle.kts b/core/netty4/build.gradle.kts index 01749278e..8d028caf9 100644 --- a/core/netty4/build.gradle.kts +++ b/core/netty4/build.gradle.kts @@ -1,3 +1,8 @@ +plugins { + id("floodgate.publish-conventions") + id("floodgate.shadow-conventions") +} + dependencies { api(projects.coreCommon) annotationProcessor(projects.coreCommon) diff --git a/fabric/base/build.gradle.kts b/fabric/base/build.gradle.kts new file mode 100644 index 000000000..e12d23e93 --- /dev/null +++ b/fabric/base/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("floodgate.modded-conventions") +} + +architectury { + platformSetupLoomIde() + fabric() +} + +// Used to extend runtime/compile classpaths +val common: Configuration by configurations.creating +// Needed to read mixin config in the runServer task, and for the architectury transformer +// (e.g. the @ExpectPlatform annotation) +val developmentFabric: Configuration = configurations.getByName("developmentFabric") +//// Our custom transitive include configuration +//val includeTransitive: Configuration = configurations.getByName("includeTransitive") + +configurations { + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + developmentFabric.extendsFrom(configurations["common"]) +} + +dependencies { + modImplementation(libs.fabric.loader) + modApi(libs.fabric.api) + // "namedElements" configuration should be used to depend on different loom projects + common(project(":mod", configuration = "namedElements")) + // Bundle transformed classes of the common module for production mod jar + shadowBundle(project(path = ":mod", configuration = "transformProductionFabric")) + modImplementation(libs.cloud.fabric) + + compileOnlyApi(projects.isolation) +} diff --git a/fabric/base/src/main/java/org/geysermc/floodgate/mod/util/fabric/ModMixinConfigPluginImpl.java b/fabric/base/src/main/java/org/geysermc/floodgate/mod/util/fabric/ModMixinConfigPluginImpl.java new file mode 100644 index 000000000..0c6e19e13 --- /dev/null +++ b/fabric/base/src/main/java/org/geysermc/floodgate/mod/util/fabric/ModMixinConfigPluginImpl.java @@ -0,0 +1,10 @@ +package org.geysermc.floodgate.mod.util.fabric; + +import net.fabricmc.loader.api.FabricLoader; + +public class ModMixinConfigPluginImpl { + + public static boolean applyProxyFix() { + return FabricLoader.getInstance().isModLoaded("fabricproxy-lite"); + } +} diff --git a/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/FabricFloodgateMod.java b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/FabricFloodgateMod.java new file mode 100644 index 000000000..dde7651d5 --- /dev/null +++ b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/FabricFloodgateMod.java @@ -0,0 +1,50 @@ +package org.geysermc.floodgate.platform.fabric; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.inject.qualifiers.Qualifiers; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.floodgate.isolation.library.LibraryManager; +import org.geysermc.floodgate.mod.ModPlatform; + +import java.nio.file.Path; + +public final class FabricFloodgateMod extends ModPlatform { + + private final ModContainer container; + + public FabricFloodgateMod(LibraryManager manager, ModContainer container) { + super(manager); + this.container = container; + } + + @Override + protected void onContextCreated(ApplicationContext context) { + super.onContextCreated(context); + context.registerSingleton(container) + .registerSingleton( + Path.class, + FabricLoader.getInstance().getConfigDir().resolve("floodgate"), + Qualifiers.byName("dataDirectory") + ); + + ServerLifecycleEvents.SERVER_STARTED.register((server) -> { + context.registerSingleton(server, false); + context.registerSingleton(MinecraftServerAudiences.of(server), false); + }); + } + + @Override + public @Nullable Path resourcePath(String file) { + return container.findPath(file).orElse(null); + } + + @Override + public boolean isClient() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } +} diff --git a/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/listener/FabricEventRegistration.java b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/listener/FabricEventRegistration.java new file mode 100644 index 000000000..ebc857101 --- /dev/null +++ b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/listener/FabricEventRegistration.java @@ -0,0 +1,15 @@ +package org.geysermc.floodgate.platform.fabric.listener; + +import net.fabricmc.fabric.api.networking.v1.ServerLoginConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import org.geysermc.floodgate.core.platform.listener.ListenerRegistration; +import org.geysermc.floodgate.mod.listener.ModEventListener; + +public final class FabricEventRegistration implements ListenerRegistration { + @Override + public void register(ModEventListener listener) { + ServerPlayConnectionEvents.JOIN.register( + (handler, sender, server) -> listener.onPlayerJoin(handler.getPlayer().getUUID()) + ); + } +} diff --git a/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricCommandModule.java b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricCommandModule.java new file mode 100644 index 000000000..2bb3d116e --- /dev/null +++ b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricCommandModule.java @@ -0,0 +1,32 @@ +package org.geysermc.floodgate.platform.fabric.module; + + +import jakarta.inject.Singleton; +import lombok.SneakyThrows; +import net.minecraft.commands.CommandSourceStack; +import org.geysermc.floodgate.core.module.CommandModule; +import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.geysermc.floodgate.core.player.FloodgateCommandPreprocessor; +import org.geysermc.floodgate.core.player.UserAudience; +import org.geysermc.floodgate.core.player.audience.FloodgateSenderMapper; +import org.geysermc.floodgate.mod.util.ModCommandUtil; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.fabric.FabricCommandManager; +import org.incendo.cloud.fabric.FabricServerCommandManager; + +public final class FabricCommandModule extends CommandModule { + @Provides + @Singleton + @SneakyThrows + public CommandManager commandManager(CommandUtil commandUtil) { + FabricCommandManager commandManager = new FabricServerCommandManager<>( + ExecutionCoordinator.simpleCoordinator(), + new FloodgateSenderMapper<>(commandUtil) + ); + commandManager.registerCommandPreProcessor(new FloodgateCommandPreprocessor<>(commandUtil)); + ((ModCommandUtil) commandUtil).setCommandManager(commandManager); + return commandManager; + } + +} diff --git a/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricPlatformModule.java b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricPlatformModule.java new file mode 100644 index 000000000..2f4cf18a4 --- /dev/null +++ b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricPlatformModule.java @@ -0,0 +1,40 @@ +package org.geysermc.floodgate.platform.fabric.module; + +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import org.geysermc.floodgate.core.platform.listener.ListenerRegistration; +import org.geysermc.floodgate.core.platform.pluginmessage.PluginMessageUtils; +import org.geysermc.floodgate.core.pluginmessage.PluginMessageRegistration; +import org.geysermc.floodgate.mod.listener.ModEventListener; +import org.geysermc.floodgate.mod.module.ModPlatformModule; +import org.geysermc.floodgate.platform.fabric.listener.FabricEventRegistration; +import org.geysermc.floodgate.platform.fabric.pluginmessage.FabricPluginMessageRegistration; +import org.geysermc.floodgate.platform.fabric.pluginmessage.FabricPluginMessageUtils; + +public class FabricPlatformModule extends ModPlatformModule { + + @Provides + @Singleton + public ListenerRegistration listenerRegistration() { + return new FabricEventRegistration(); + } + + @Provides + @Singleton + public PluginMessageUtils pluginMessageUtils() { + return new FabricPluginMessageUtils(); + } + + @Provides + @Singleton + public PluginMessageRegistration pluginMessageRegister() { + return new FabricPluginMessageRegistration(); + } + + @Provides + @Named("implementationName") + public String implementationName() { + return "Fabric"; + } +} diff --git a/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageRegistration.java b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageRegistration.java new file mode 100644 index 000000000..58c8eb8ff --- /dev/null +++ b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageRegistration.java @@ -0,0 +1,61 @@ +package org.geysermc.floodgate.platform.fabric.pluginmessage; + +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import org.geysermc.floodgate.core.pluginmessage.PluginMessageChannel; +import org.geysermc.floodgate.core.pluginmessage.PluginMessageRegistration; +import org.geysermc.floodgate.mod.pluginmessage.payloads.FormPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.PacketPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.SkinPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.TransferPayload; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +import java.util.function.Function; + +public final class FabricPluginMessageRegistration implements PluginMessageRegistration { + + @Override + public void register(PluginMessageChannel channel) { + final String id = channel.getIdentifier(); + + switch (id) { + case "floodgate:form" -> + registerBoth(channel, FormPayload.TYPE, FormPayload.STREAM_CODEC, FormPayload::data); + case "floodgate:packet" -> + registerBoth(channel, PacketPayload.TYPE, PacketPayload.STREAM_CODEC, PacketPayload::data); + case "floodgate:skin" -> + registerBoth(channel, SkinPayload.TYPE, SkinPayload.STREAM_CODEC, SkinPayload::data); + case "floodgate:transfer" -> + registerBoth(channel, TransferPayload.TYPE, TransferPayload.STREAM_CODEC, TransferPayload::data); + default -> throw new IllegalArgumentException("Unknown channel: " + id); + } + } + + /** + * Registers payload type/codec for both directions and wires a global receiver that + * forwards to the PluginMessageChannel. + */ + private static void registerBoth( + PluginMessageChannel channel, + CustomPacketPayload.Type type, + StreamCodec codec, + Function dataExtractor + ) { + // Bidirectional registration + PayloadTypeRegistry.playC2S().register(type, codec); + PayloadTypeRegistry.playS2C().register(type, codec); + + // Single handler that delegates to channel.handleServerCall(...) + ServerPlayNetworking.registerGlobalReceiver( + type, + (payload, context) -> channel.handleServerCall( + dataExtractor.apply(payload), + context.player().getUUID(), + context.player().getGameProfile().getName() + ) + ); + } +} diff --git a/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageUtils.java b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageUtils.java new file mode 100644 index 000000000..d1e475e09 --- /dev/null +++ b/fabric/base/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageUtils.java @@ -0,0 +1,44 @@ +package org.geysermc.floodgate.platform.fabric.pluginmessage; + +import io.micronaut.context.BeanProvider; +import jakarta.inject.Inject; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.geysermc.floodgate.core.platform.pluginmessage.PluginMessageUtils; +import org.geysermc.floodgate.mod.pluginmessage.payloads.FormPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.PacketPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.SkinPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.TransferPayload; + +import java.util.Objects; +import java.util.UUID; + +public class FabricPluginMessageUtils extends PluginMessageUtils { + + @Inject + private BeanProvider server; + + @Override + public boolean sendMessage(UUID uuid, String channel, byte[] data) { + try { + ServerPlayer player = server.get().getPlayerList().getPlayer(uuid); + final CustomPacketPayload payload; + switch (channel) { + case "floodgate:form" -> payload = new FormPayload(data); + case "floodgate:packet" -> payload = new PacketPayload(data); + case "floodgate:skin" -> payload = new SkinPayload(data); + case "floodgate:transfer" -> payload = new TransferPayload(data); + default -> throw new IllegalArgumentException("unknown channel: " + channel); + } + + Objects.requireNonNull(player); + ServerPlayNetworking.send(player, payload); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } +} diff --git a/fabric/gradle.properties b/fabric/gradle.properties new file mode 100644 index 000000000..90ee7a259 --- /dev/null +++ b/fabric/gradle.properties @@ -0,0 +1 @@ +loom.platform=fabric \ No newline at end of file diff --git a/fabric/isolated/build.gradle.kts b/fabric/isolated/build.gradle.kts new file mode 100644 index 000000000..e8011c58c --- /dev/null +++ b/fabric/isolated/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("floodgate.modded-conventions") +} + +architectury { + platformSetupLoomIde() + fabric() +} + +dependencies { + // FIXME why does it break when this is set to api scope??? + compileOnlyApi(projects.isolation) + + modImplementation(libs.fabric.loader) + modApi(libs.fabric.api) + include(libs.cloud.fabric) + include(libs.fabric.permissions.api) +} + +tasks { + jar { + dependsOn(":fabric-base:build", configurations.runtimeClasspath) + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + + archiveBaseName = "floodgate-${project.name}" + archiveVersion = "" + archiveClassifier = "" + + val libsDir = project.projects + .fabricBase.dependencyProject + .layout.buildDirectory.dir("libs") + + from(libsDir) { + include("floodgate-fabric-base.jar") + rename("floodgate-fabric-base.jar", "platform-base.jar") + into("bundled/") + } + } +} diff --git a/fabric/isolated/src/main/java/org/geysermc/floodgate/fabric/IsolatedFabricMod.java b/fabric/isolated/src/main/java/org/geysermc/floodgate/fabric/IsolatedFabricMod.java new file mode 100644 index 000000000..7357cf7cf --- /dev/null +++ b/fabric/isolated/src/main/java/org/geysermc/floodgate/fabric/IsolatedFabricMod.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.fabric; + +import java.util.List; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import org.geysermc.floodgate.isolation.loader.PlatformHolder; +import org.geysermc.floodgate.isolation.loader.PlatformLoader; + +public final class IsolatedFabricMod implements ModInitializer { + private static final boolean IS_SERVER = FabricLoader.getInstance().getEnvironmentType().equals(EnvType.SERVER); + + @Override + public void onInitialize() { + PlatformHolder holder; + try { + ModContainer container = FabricLoader.getInstance().getModContainer("floodgate").orElseThrow(); + var libsDirectory = FabricLoader.getInstance().getConfigDir().resolve("floodgate").resolve("libs"); + holder = PlatformLoader.loadDefault(getClass().getClassLoader(), libsDirectory); + holder.init(List.of(ModContainer.class), List.of(container)); + } catch (Exception exception) { + throw new RuntimeException("Failed to load Floodgate", exception); + } + + if (IS_SERVER) { + ServerLifecycleEvents.SERVER_STARTED.register(($) -> { + holder.enable(); + }); + } else { + ClientLifecycleEvents.CLIENT_STOPPING.register(($)-> { + holder.shutdown(); + }); + } + + ServerLifecycleEvents.SERVER_STOPPING.register(($) -> { + if (IS_SERVER) { + holder.shutdown(); + } else { + holder.disable(); + } + }); + } +} diff --git a/fabric/isolated/src/main/resources/fabric.mod.json b/fabric/isolated/src/main/resources/fabric.mod.json new file mode 100644 index 000000000..6ae8d33f1 --- /dev/null +++ b/fabric/isolated/src/main/resources/fabric.mod.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "id": "$id", + "version": "$version", + "name": "$name", + "description": "$description", + "authors": [ + "$author" + ], + "contact": { + "website": "$url", + "repo": "https://github.com/GeyserMC/Floodgate" + }, + "license": "MIT", + "environment": "*", + "entrypoints": { + "main": [ + "org.geysermc.floodgate.fabric.IsolatedFabricMod" + ] + }, + "accessWidener": "floodgate.accesswidener", + "mixins": [ + "floodgate.mixins.json" + ], + "depends": { + "fabricloader": ">=0.15.10", + "fabric-api": "*", + "minecraft": ">=$minecraft_version" + } +} diff --git a/fabric/isolated/src/main/resources/org.geysermc.mainClass b/fabric/isolated/src/main/resources/org.geysermc.mainClass new file mode 100644 index 000000000..6a78c3291 --- /dev/null +++ b/fabric/isolated/src/main/resources/org.geysermc.mainClass @@ -0,0 +1 @@ +org.geysermc.floodgate.fabric.FabricPlatform diff --git a/gradle.properties b/gradle.properties index b08de121c..29b3b0028 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,11 @@ org.gradle.configureondemand=true org.gradle.caching=true org.gradle.parallel=true +# TODO https://github.com/architectury/architectury-plugin/pull/56 +org.gradle.configuration-cache=false +group=org.geysermc +id=floodgate +description="Allows Bedrock players to join Java edition servers while keeping the server in online mode" version=3.0.0-SNAPSHOT -micronautVersion=4.6.0 \ No newline at end of file +micronautVersion=4.6.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae79fe44a..cd55c862c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,4 @@ [versions] -# parent -micronaut-gradle = "4.4.2" -lombok = "8.4" # api cumulus = "2.0.0-SNAPSHOT" @@ -20,6 +17,7 @@ snakeyaml = "2.0" bstats = "3.0.3" adventure = "4.17.0" adventure-platform = "4.3.4" +adventure-platform-modded = "6.6.0" avaje-http = "2.7" avaje-jsonb = "2.1" expiringmap = "0.5.11" @@ -34,11 +32,29 @@ authlib = "5.0.47" # velocity velocity = "3.3.0-SNAPSHOT" +# modded +architectury-plugin = "3.4-SNAPSHOT" +architectury-loom = "1.11-SNAPSHOT" +minecraft-version = "1.21.8" +minotaur = "2.+" +mixin = "0.8.5" +asm = "5.2" + +# fabric +fabric-loader = "0.16.14" +fabric-api = "0.127.1+1.21.6" +fabric-permissions-api = "0.4.1-SNAPSHOT" + +# neoforge +neoforge-version = "21.6.11-beta" + # buildSrc -indra = "3.1.3" +indra = "3.2.0" shadow = "8.3.0" gradle-idea-ext = "1.1.7" checkerframework = "3.42.0" +micronaut-gradle = "4.4.2" +lombok = "8.4" [libraries] # indirectly included @@ -68,8 +84,9 @@ bstats = { module = "org.bstats:bstats-base", version.ref = "bstats" } adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure" } adventure-key = { module = "net.kyori:adventure-key", version.ref = "adventure" } adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } -adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform"} -adventure-platform-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform"} +adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } +adventure-platform-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" } +adventure-platform-modded = { module = "net.kyori:adventure-platform-mod-shared", version.ref = "adventure-platform-modded" } micronaut-inject = { module = "io.micronaut:micronaut-inject" } micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java" } @@ -102,6 +119,21 @@ cloud-velocity = { module = "org.incendo:cloud-velocity", version.ref = "cloud-p velocity-api = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" } velocity-proxy = { module = "com.velocitypowered:velocity-proxy", version.ref = "velocity" } +# modded +mixin = { group = "org.spongepowered", name = "mixin", version.ref = "mixin" } +asm = { group = "org.ow2.asm", name = "asm-debug-all", version.ref = "asm" } +minecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft-version" } + +# fabric +cloud-fabric = { group = "org.incendo", name = "cloud-fabric", version.ref = "cloud-platform" } +fabric-loader = { group = "net.fabricmc", name = "fabric-loader", version.ref = "fabric-loader" } +fabric-api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabric-api" } +fabric-permissions-api = { group = "me.lucko", name = "fabric-permissions-api", version.ref = "fabric-permissions-api" } + +# neoforge +cloud-neoforge = { group = "org.incendo", name = "cloud-neoforge", version.ref = "cloud-platform" } +neoforge = { group = "net.neoforged", name = "neoforge", version.ref = "neoforge-version" } + # buildSrc checker-qual = { module = "org.checkerframework:checker-qual", version.ref = "checkerframework" } # plugins @@ -110,10 +142,11 @@ indra-git = { module = "net.kyori:indra-git", version.ref = "indra" } indra-licenser-spotless = { module = "net.kyori:indra-licenser-spotless", version.ref = "indra" } shadow = { group = "com.gradleup.shadow", name = "com.gradleup.shadow.gradle.plugin", version.ref = "shadow" } gradle-idea-ext = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version.ref = "gradle-idea-ext" } - -[plugins] -micronaut = { id = "io.micronaut.library", version.ref = "micronaut-gradle" } -lombok = { id = "io.freefair.lombok", version.ref = "lombok" } +lombok = { group = "io.freefair.gradle", name = "lombok-plugin", version.ref = "lombok" } +micronaut = { group = "io.micronaut.library", name = "io.micronaut.library.gradle.plugin", version.ref = "micronaut-gradle" } +minotaur = { group = "com.modrinth.minotaur", name = "Minotaur", version.ref = "minotaur" } +architectury-plugin = { group = "architectury-plugin", name = "architectury-plugin.gradle.plugin", version.ref = "architectury-plugin" } +architectury-loom = { group = "dev.architectury.loom", name = "dev.architectury.loom.gradle.plugin", version.ref = "architectury-loom" } [bundles] -fastutil = ["fastutil-short-object-maps", "fastutil-int-object-maps"] \ No newline at end of file +fastutil = ["fastutil-short-object-maps", "fastutil-int-object-maps"] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce5..d4081da47 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/isolation/build.gradle.kts b/isolation/build.gradle.kts index b71c6e162..c1438baa5 100644 --- a/isolation/build.gradle.kts +++ b/isolation/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("floodgate.base-conventions") + id("floodgate.publish-conventions") } dependencies { diff --git a/mod/build.gradle.kts b/mod/build.gradle.kts new file mode 100644 index 000000000..39155cf87 --- /dev/null +++ b/mod/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("floodgate.modded-conventions") +} + +architectury { + common("neoforge", "fabric") +} + +//loom { +// accessWidenerPath = file("src/main/resources/floodgate.accesswidener") +// mixin.defaultRefmapName.set("floodgate-refmap.json") +//} + +dependencies { + api(projects.coreNetty4) + annotationProcessor(projects.coreNetty4) + annotationProcessor(libs.micronaut.inject.java) + compileOnlyApi(projects.isolation) + + modApi(libs.adventure.platform.modded) + + compileOnly(libs.mixin) + compileOnly(libs.asm) + + // Only here to suppress "unknown enum constant EnvType.CLIENT" warnings. + compileOnly(libs.fabric.loader) +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/ModPlatform.java b/mod/src/main/java/org/geysermc/floodgate/mod/ModPlatform.java new file mode 100644 index 000000000..1ea46718a --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/ModPlatform.java @@ -0,0 +1,33 @@ +package org.geysermc.floodgate.mod; + +import io.micronaut.context.ApplicationContext; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.floodgate.core.FloodgatePlatform; + +import java.nio.file.Path; +import org.geysermc.floodgate.isolation.library.LibraryManager; +import org.slf4j.LoggerFactory; + +public abstract class ModPlatform extends FloodgatePlatform { + + protected ApplicationContext context; + + protected ModPlatform(LibraryManager manager) { + super(manager); + } + + @Override + protected void onContextCreated(ApplicationContext context) { + context.registerSingleton(LoggerFactory.getLogger("floodgate")); + this.context = context; + } + + public @Nullable abstract Path resourcePath(String file); + + public abstract boolean isClient(); + + @Override + public boolean isProxy() { + return false; + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/data/ModDataAddon.java b/mod/src/main/java/org/geysermc/floodgate/mod/data/ModDataAddon.java new file mode 100644 index 000000000..b894a1499 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/data/ModDataAddon.java @@ -0,0 +1,59 @@ +package org.geysermc.floodgate.mod.data; + +import io.netty.channel.Channel; +import io.netty.util.AttributeKey; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import org.geysermc.api.connection.Connection; +import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.core.api.SimpleFloodgateApi; +import org.geysermc.floodgate.core.api.inject.InjectorAddon; +import org.geysermc.floodgate.core.config.FloodgateConfig; +import org.geysermc.floodgate.core.connection.DataSeeker; +import org.geysermc.floodgate.core.connection.FloodgateDataHandler; +import org.geysermc.floodgate.core.logger.FloodgateLogger; +import org.geysermc.floodgate.core.util.Utils; + +@Singleton +public final class ModDataAddon implements InjectorAddon { + @Inject + DataSeeker dataSeeker; + + @Inject + FloodgateDataHandler handshakeHandler; + + @Inject + FloodgateConfig config; + + @Inject + FloodgateLogger logger; + + @Inject + @Named("packetHandler") + private String packetHandlerName; + + @Inject + @Named("connectionAttribute") + AttributeKey connectionAttribute; + + @Inject + @Named("kickMessageAttribute") + private AttributeKey kickMessageAttribute; + + @Override + public void onInject(Channel channel, boolean toServer) { + var dataHandler = new ModDataHandler(handshakeHandler, config, kickMessageAttribute, logger); + channel.pipeline().addBefore(packetHandlerName, "floodgate_data_handler", dataHandler); + } + + @Override + public void onRemoveInject(Channel channel) { + Utils.removeHandler(channel.pipeline(), "floodgate_data_handler"); + } + + @Override + public boolean shouldInject() { + return true; + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/data/ModDataHandler.java b/mod/src/main/java/org/geysermc/floodgate/mod/data/ModDataHandler.java new file mode 100644 index 000000000..0e312eb61 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/data/ModDataHandler.java @@ -0,0 +1,172 @@ +package org.geysermc.floodgate.mod.data; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import com.mojang.logging.LogUtils; +import io.micronaut.context.BeanProvider; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.AttributeKey; +import jakarta.inject.Inject; +import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences; +import net.minecraft.DefaultUncaughtExceptionHandler; +import net.minecraft.network.Connection; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.handshake.ClientIntentionPacket; +import net.minecraft.network.protocol.login.ServerboundHelloPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import org.geysermc.floodgate.core.addon.data.CommonNettyDataHandler; +import org.geysermc.floodgate.core.addon.data.PacketBlocker; +import org.geysermc.floodgate.core.config.FloodgateConfig; +import org.geysermc.floodgate.core.connection.DataSeeker; +import org.geysermc.floodgate.core.connection.FloodgateDataHandler; +import org.geysermc.floodgate.core.logger.FloodgateLogger; +import org.geysermc.floodgate.mod.mixin.ClientIntentionPacketMixinInterface; +import org.geysermc.floodgate.mod.mixin.ConnectionMixin; +import org.slf4j.Logger; + +import java.net.InetSocketAddress; + +public final class ModDataHandler extends CommonNettyDataHandler { + private static final Logger LOGGER = LogUtils.getLogger(); + private Connection networkManager; + // stellar, but, won't change this now + private org.geysermc.api.connection.Connection connection; + private boolean proxyData; + + @Inject + BeanProvider server; + + @Inject + BeanProvider audience; + + public ModDataHandler( + DataSeeker dataSeeker, + FloodgateDataHandler dataHandler, + FloodgateConfig config, + FloodgateLogger logger, + AttributeKey connectionAttribute, + AttributeKey kickMessageAttribute) { + super( + dataSeeker, + dataHandler, + config, + logger, + connectionAttribute, + kickMessageAttribute, + new PacketBlocker() + ); + } + + @Override + protected void setNewIp(Channel channel, InetSocketAddress newIp) { + ((ConnectionMixin) this.networkManager).setAddress(newIp); + } + + @Override + protected Object setHostname(Object handshakePacket, String hostname) { + // While it would be ideal to simply create a new handshake packet, the packet constructor + // does not allow us to set the protocol version + ((ClientIntentionPacketMixinInterface) handshakePacket).setAddress(hostname); + return handshakePacket; + } + + @Override + protected boolean shouldRemoveHandler(FloodgateDataHandler.HandleResult result) { + connection = result.joinResult() != null ? result.joinResult().connection() : null; + + if (getKickMessage() != null) { + // we also have to keep this handler if we want to kick then with a disconnect message + return false; + } else if (connection == null) { + // player is not a Floodgate player + return true; + } + + // TODO proxy data handling...? + + // Handler will be removed after the login hello packet is handled + return false; + } + + @Override + protected boolean channelRead(Object packet) { + if (packet instanceof ClientIntentionPacket intentionPacket) { + ctx.pipeline().addAfter("splitter", "floodgate_packet_blocker", blocker); + networkManager = (Connection) ctx.channel().pipeline().get("packet_handler"); + handle(packet, intentionPacket.hostName()); + return false; + } + return !checkAndHandleLogin(packet); + } + + private boolean checkAndHandleLogin(Object packet) { + if (packet instanceof ServerboundHelloPacket) { + var kickMessage = getKickMessage(); + if (kickMessage != null) { + Component message = audience.get().asNative(kickMessage); + // If possible, disconnect using the "proper" packet listener; otherwise there's no proper disconnect message + if (networkManager.getPacketListener() instanceof ServerLoginPacketListenerImpl loginPacketListener) { + loginPacketListener.disconnect(message); + } else { + networkManager.disconnect(message); + } + return true; + } + + // we have to fake the offline player (login) cycle + if (!(networkManager.getPacketListener() instanceof ServerLoginPacketListenerImpl packetListener)) { + // player is not in the login state, abort + ctx.pipeline().remove(this); + return true; + } + + GameProfile gameProfile = new GameProfile(connection.javaUuid(), connection.javaUsername()); + + if (connection.isLinked() && connection.javaUuid().version() == 4) { + verifyLinkedPlayerAsync(packetListener, gameProfile); + } else { + packetListener.startClientVerification(gameProfile); + } + + ctx.pipeline().remove(this); + return true; + } + return false; + } + + /** + * Starts a new thread that fetches the linked player's textures, + * and then starts client verification with the more accurate game profile. + * + * @param packetListener the login packet listener for this connection + * @param gameProfile the player's initial profile. it will NOT be mutated. + */ + private void verifyLinkedPlayerAsync(ServerLoginPacketListenerImpl packetListener, GameProfile gameProfile) { + Thread texturesThread = new Thread("Bedrock Linked Player Texture Download") { + @Override + public void run() { + GameProfile effectiveProfile = gameProfile; + try { + MinecraftSessionService service = server.get().getSessionService(); + effectiveProfile = service.fetchProfile(effectiveProfile.getId(), true).profile(); + } catch (Exception e) { + LOGGER.error("Unable to get Bedrock linked player textures for " + effectiveProfile.getName(), e); + } + packetListener.startClientVerification(effectiveProfile); + } + }; + texturesThread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER)); + texturesThread.start(); + } + + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + if (config.debug()) { + LOGGER.error("Exception caught in FabricDataHandler", cause); + } + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/inject/ModInjector.java b/mod/src/main/java/org/geysermc/floodgate/mod/inject/ModInjector.java new file mode 100644 index 000000000..02a034054 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/inject/ModInjector.java @@ -0,0 +1,59 @@ +package org.geysermc.floodgate.mod.inject; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.core.inject.Netty4PlatformInjector; + +@RequiredArgsConstructor +public final class ModInjector extends Netty4PlatformInjector { + + public static ModInjector INSTANCE = new ModInjector(); + + @Getter private final boolean injected = true; + + @Override + public void inject() throws Exception { + //no-op, mixins go brrrrrr + } + + public void injectClient(ChannelFuture future) { + if (isInjected()) { + return; + } + + future.channel().pipeline().addFirst("floodgate-init", new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) throws Exception { + super.channelRead(ctx, msg); + + if (!(msg instanceof Channel channel)) { + return; + } + + channel.pipeline().addLast(new ChannelInitializer<>() { + @Override + protected void initChannel(@NonNull Channel channel) { + injectAddonsCall(channel, false); + addInjectedClient(channel); + channel.closeFuture().addListener(listener -> { + channelClosedCall(channel); + removeInjectedClient(channel); + }); + } + }); + } + }); + } + + @Override + public void removeInjection() { + //no-op + } + +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/listener/ModEventListener.java b/mod/src/main/java/org/geysermc/floodgate/mod/listener/ModEventListener.java new file mode 100644 index 000000000..c0b956cf4 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/listener/ModEventListener.java @@ -0,0 +1,32 @@ +package org.geysermc.floodgate.mod.listener; + + +import jakarta.inject.Inject; +import org.geysermc.floodgate.core.connection.ConnectionManager; + +import java.util.UUID; +import org.geysermc.floodgate.core.util.LanguageManager; + +public final class ModEventListener { + + @Inject + LanguageManager languageManager; + + @Inject + ConnectionManager connectionManager; + + public void onPlayerJoin(UUID uuid) { + // TODO this might be called late on fabric + var connection = connectionManager.findPendingConnection(uuid); + if (connection == null) { + return; + } + + languageManager.loadLocale(connection.languageCode()); + connectionManager.addAcceptedConnection(connection); + } + + public void onPlayerQuit(UUID uuid) { + connectionManager.removeConnection(uuid); + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ChunkMapMixin.java b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ChunkMapMixin.java new file mode 100644 index 000000000..c959ebfc3 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ChunkMapMixin.java @@ -0,0 +1,12 @@ +package org.geysermc.floodgate.mod.mixin; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.server.level.ChunkMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ChunkMap.class) +public interface ChunkMapMixin { + @Accessor("entityMap") + Int2ObjectMap getEntityMap(); +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ClientIntentionPacketMixin.java b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ClientIntentionPacketMixin.java new file mode 100644 index 000000000..75b5df4de --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ClientIntentionPacketMixin.java @@ -0,0 +1,14 @@ +package org.geysermc.floodgate.mod.mixin; + +import net.minecraft.network.protocol.handshake.ClientIntentionPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(ClientIntentionPacket.class) +public class ClientIntentionPacketMixin { + @ModifyConstant(method = "(Lnet/minecraft/network/FriendlyByteBuf;)V", constant = @Constant(intValue = 255)) + private static int floodgate$setHandshakeLength(int defaultValue) { + return Short.MAX_VALUE; + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ClientIntentionPacketMixinInterface.java b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ClientIntentionPacketMixinInterface.java new file mode 100644 index 000000000..ee0679291 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ClientIntentionPacketMixinInterface.java @@ -0,0 +1,14 @@ +package org.geysermc.floodgate.mod.mixin; + +import net.minecraft.network.protocol.handshake.ClientIntentionPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ClientIntentionPacket.class) +public interface ClientIntentionPacketMixinInterface { + + @Accessor("hostName") + @Mutable + void setAddress(String address); +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ConnectionMixin.java b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ConnectionMixin.java new file mode 100644 index 000000000..e085cd3a3 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ConnectionMixin.java @@ -0,0 +1,13 @@ +package org.geysermc.floodgate.mod.mixin; + +import net.minecraft.network.Connection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.net.SocketAddress; + +@Mixin(Connection.class) +public interface ConnectionMixin { + @Accessor("address") + void setAddress(SocketAddress address); +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/mixin/FloodgateUtilMixin.java b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/FloodgateUtilMixin.java new file mode 100644 index 000000000..8a8db04fa --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/FloodgateUtilMixin.java @@ -0,0 +1,50 @@ +package org.geysermc.floodgate.mod.mixin; + +import org.geysermc.floodgate.core.util.Utils; +import org.geysermc.floodgate.mod.FloodgateMod; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +// +///** +// * Mixins into Floodgate's {@link Utils} class to modify how resources are loaded from the jar. +// * This must be done due to mod platforms sharing a classloader across mods - this leads to Floodgate +// * loading Geyser's language files, as they're not prefixed to avoid conflicts. +// * To resolve this, this mixin replaces those calls with the platform-appropriate methods to load files. +// */ +// TODO +//@Mixin(value = Utils.class, remap = false) +//public class FloodgateUtilMixin { +// +// @Redirect(method = "readProperties", +// at = @At(value = "INVOKE", target = "Ljava/lang/ClassLoader;getResourceAsStream(Ljava/lang/String;)Ljava/io/InputStream;")) +// private static InputStream floodgate$redirectInputStream(ClassLoader instance, String string) { +// Path path = FloodgateMod.INSTANCE.resourcePath(string); +// try { +// return path == null ? null : Files.newInputStream(path); +// } catch (IOException e) { +// throw new IllegalStateException(e); +// } +// } +// +// @Redirect(method = "getGeneratedClassesForAnnotation(Ljava/lang/String;)Ljava/util/Set;", +// at = @At(value = "INVOKE", target = "Ljava/lang/ClassLoader;getResourceAsStream(Ljava/lang/String;)Ljava/io/InputStream;")) +// private static InputStream floodgate$redirectInputStreamAnnotation(ClassLoader instance, String string) { +// Path path = FloodgateMod.INSTANCE.resourcePath(string); +// +// if (path == null) { +// throw new IllegalStateException("Unable to find classes marked by annotation class! " + string); +// } +// +// try { +// return Files.newInputStream(path); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } +// } +//} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/mixin/GeyserModInjectorMixin.java b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/GeyserModInjectorMixin.java new file mode 100644 index 000000000..6ec3c158c --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/GeyserModInjectorMixin.java @@ -0,0 +1,26 @@ +package org.geysermc.floodgate.mod.mixin; + +import io.netty.channel.ChannelFuture; +import org.geysermc.floodgate.mod.inject.ModInjector; +import org.geysermc.geyser.GeyserBootstrap; +import org.geysermc.geyser.platform.mod.GeyserModInjector; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +// TODO +//@Mixin(value = GeyserModInjector.class, remap = false) +//public class GeyserModInjectorMixin { +// +// @Shadow +// private List allServerChannels; +// +// @Inject(method = "initializeLocalChannel0", at = @At(value = "INVOKE_ASSIGN", target = "Ljava/util/List;add(Ljava/lang/Object;)Z")) +// public void floodgate$onChannelAdd(GeyserBootstrap bootstrap, CallbackInfo ci) { +// ModInjector.INSTANCE.injectClient(this.allServerChannels.get(this.allServerChannels.size() - 1)); +// } +//} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ServerConnectionListenerMixin.java b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ServerConnectionListenerMixin.java new file mode 100644 index 000000000..e094da2b2 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/ServerConnectionListenerMixin.java @@ -0,0 +1,25 @@ +package org.geysermc.floodgate.mod.mixin; + +import io.netty.channel.ChannelFuture; +import net.minecraft.server.network.ServerConnectionListener; +import org.geysermc.floodgate.mod.inject.ModInjector; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.net.InetAddress; +import java.util.List; + +@Mixin(ServerConnectionListener.class) +public abstract class ServerConnectionListenerMixin { + + @Shadow @Final private List channels; + + @Inject(method = "startTcpServerListener", at = @At(value = "INVOKE_ASSIGN", target = "Ljava/util/List;add(Ljava/lang/Object;)Z")) + public void floodgate$onChannelAdd(InetAddress address, int port, CallbackInfo ci) { + ModInjector.INSTANCE.injectClient(this.channels.get(this.channels.size() - 1)); + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/module/ModAddonModule.java b/mod/src/main/java/org/geysermc/floodgate/mod/module/ModAddonModule.java new file mode 100644 index 000000000..fdd9f5778 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/module/ModAddonModule.java @@ -0,0 +1,35 @@ +package org.geysermc.floodgate.mod.module; + +import com.google.inject.AbstractModule; +import com.google.inject.Singleton; +import com.google.inject.multibindings.ProvidesIntoSet; +import org.geysermc.floodgate.api.inject.InjectorAddon; +import org.geysermc.floodgate.core.addon.AddonManagerAddon; +import org.geysermc.floodgate.core.addon.DebugAddon; +import org.geysermc.floodgate.core.register.AddonRegister; +import org.geysermc.floodgate.mod.data.ModDataAddon; + +public final class ModAddonModule extends AbstractModule { + @Override + protected void configure() { + bind(AddonRegister.class).asEagerSingleton(); + } + + @Singleton + @ProvidesIntoSet + public InjectorAddon managerAddon() { + return new AddonManagerAddon(); + } + + @Singleton + @ProvidesIntoSet + public InjectorAddon dataAddon() { + return new ModDataAddon(); + } + + @Singleton + @ProvidesIntoSet + public InjectorAddon debugAddon() { + return new DebugAddon(); + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/module/ModListenerModule.java b/mod/src/main/java/org/geysermc/floodgate/mod/module/ModListenerModule.java new file mode 100644 index 000000000..98d4532ee --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/module/ModListenerModule.java @@ -0,0 +1,21 @@ +package org.geysermc.floodgate.mod.module; + +import com.google.inject.AbstractModule; +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.ProvidesIntoSet; +import org.geysermc.floodgate.core.register.ListenerRegister; +import org.geysermc.floodgate.mod.listener.ModEventListener; + +public final class ModListenerModule extends AbstractModule { + @Override + protected void configure() { + bind(new TypeLiteral>() {}).asEagerSingleton(); + } + + @Singleton + @ProvidesIntoSet + public ModEventListener modEventListener() { + return new ModEventListener(); + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/module/ModPlatformModule.java b/mod/src/main/java/org/geysermc/floodgate/mod/module/ModPlatformModule.java new file mode 100644 index 000000000..806373011 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/module/ModPlatformModule.java @@ -0,0 +1,29 @@ +package org.geysermc.floodgate.mod.module; + +import io.micronaut.context.annotation.Bean; +import jakarta.inject.Named; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public abstract class ModPlatformModule { + + @Provides + @Named("packetEncoder") + public String packetEncoder() { + return FloodgateMod.INSTANCE.isClient() ? "encoder" : "outbound_config"; + } + + @Provides + @Named("packetDecoder") + public String packetDecoder() { + return FloodgateMod.INSTANCE.isClient() ? "inbound_config" : "decoder" ; + } + + @Bean + @Named("packetHandler") + public String packetHandler() { + return "packet_handler"; + } + + // TODO implementation name +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/ModSkinApplier.java b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/ModSkinApplier.java new file mode 100644 index 000000000..3f17ae28a --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/ModSkinApplier.java @@ -0,0 +1,65 @@ +package org.geysermc.floodgate.mod.pluginmessage; + +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import io.micronaut.context.BeanProvider; +import jakarta.inject.Inject; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerPlayer; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.api.connection.Connection; +import org.geysermc.floodgate.core.skin.SkinApplier; +import org.geysermc.floodgate.mod.mixin.ChunkMapMixin; + +import java.util.Collections; + +import static org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; + +public final class ModSkinApplier implements SkinApplier { + + @Inject + BeanProvider server; + + @Override + public void applySkin(@NonNull Connection connection, @NonNull SkinData skinData) { + server.get().execute(() -> { + ServerPlayer bedrockPlayer = server.get().getPlayerList() + .getPlayer(connection.javaUuid()); + if (bedrockPlayer == null) { + // TODO apply skins with delay??? + // Disconnected probably? + return; + } + + // Apply the new skin internally + PropertyMap properties = bedrockPlayer.getGameProfile().getProperties(); + + properties.removeAll("textures"); + properties.put("textures", new Property("textures", skinData.value(), skinData.signature())); + + ChunkMap tracker = bedrockPlayer.level().getChunkSource().chunkMap; + ChunkMap.TrackedEntity entry = ((ChunkMapMixin) tracker).getEntityMap().get(bedrockPlayer.getId()); + // Skin is applied - now it's time to refresh the player for everyone. + for (ServerPlayer otherPlayer : server.get().getPlayerList().getPlayers()) { + boolean samePlayer = otherPlayer == bedrockPlayer; + if (!samePlayer) { + // TrackedEntity#broadcastRemoved doesn't actually remove them from seenBy + entry.removePlayer(otherPlayer); + } + + otherPlayer.connection.send(new ClientboundPlayerInfoRemovePacket(Collections.singletonList(bedrockPlayer.getUUID()))); + otherPlayer.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(Collections.singletonList(bedrockPlayer))); + if (samePlayer) { + continue; + } + + if (bedrockPlayer.level() == otherPlayer.level()) { + entry.updatePlayer(otherPlayer); + } + } + }); + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/FormPayload.java b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/FormPayload.java new file mode 100644 index 000000000..fa7b727bb --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/FormPayload.java @@ -0,0 +1,27 @@ +package org.geysermc.floodgate.mod.pluginmessage.payloads; + +import io.netty.buffer.ByteBufUtil; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.checkerframework.checker.nullness.qual.NonNull; + +public record FormPayload(byte[] data) implements CustomPacketPayload { + public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(FormPayload::write, FormPayload::new); + public static final CustomPacketPayload.Type TYPE = new Type<>(ResourceLocation.parse("floodgate:form")); + + private FormPayload(FriendlyByteBuf friendlyByteBuf) { + this(ByteBufUtil.getBytes(friendlyByteBuf)); + friendlyByteBuf.readerIndex(friendlyByteBuf.readerIndex() + this.data.length); + } + + private void write(FriendlyByteBuf friendlyByteBuf) { + friendlyByteBuf.writeBytes(this.data); + } + + @Override + public CustomPacketPayload.@NonNull Type type() { + return TYPE; + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/PacketPayload.java b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/PacketPayload.java new file mode 100644 index 000000000..944ea8626 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/PacketPayload.java @@ -0,0 +1,27 @@ +package org.geysermc.floodgate.mod.pluginmessage.payloads; + +import io.netty.buffer.ByteBufUtil; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.checkerframework.checker.nullness.qual.NonNull; + +public record PacketPayload(byte[] data) implements CustomPacketPayload { + public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(PacketPayload::write, PacketPayload::new); + public static final CustomPacketPayload.Type TYPE = new Type<>(ResourceLocation.parse("floodgate:packet")); + + private PacketPayload(FriendlyByteBuf friendlyByteBuf) { + this(ByteBufUtil.getBytes(friendlyByteBuf)); + friendlyByteBuf.readerIndex(friendlyByteBuf.readerIndex() + this.data.length); + } + + private void write(FriendlyByteBuf friendlyByteBuf) { + friendlyByteBuf.writeBytes(this.data); + } + + @Override + public CustomPacketPayload.@NonNull Type type() { + return TYPE; + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/SkinPayload.java b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/SkinPayload.java new file mode 100644 index 000000000..16fda96b2 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/SkinPayload.java @@ -0,0 +1,27 @@ +package org.geysermc.floodgate.mod.pluginmessage.payloads; + +import io.netty.buffer.ByteBufUtil; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.checkerframework.checker.nullness.qual.NonNull; + +public record SkinPayload(byte[] data) implements CustomPacketPayload { + public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(SkinPayload::write, SkinPayload::new); + public static final CustomPacketPayload.Type TYPE = new Type<>(ResourceLocation.parse("floodgate:skin")); + + private SkinPayload(FriendlyByteBuf friendlyByteBuf) { + this(ByteBufUtil.getBytes(friendlyByteBuf)); + friendlyByteBuf.readerIndex(friendlyByteBuf.readerIndex() + this.data.length); + } + + private void write(FriendlyByteBuf friendlyByteBuf) { + friendlyByteBuf.writeBytes(this.data); + } + + @Override + public CustomPacketPayload.@NonNull Type type() { + return TYPE; + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/TransferPayload.java b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/TransferPayload.java new file mode 100644 index 000000000..597634e02 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/payloads/TransferPayload.java @@ -0,0 +1,27 @@ +package org.geysermc.floodgate.mod.pluginmessage.payloads; + +import io.netty.buffer.ByteBufUtil; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.checkerframework.checker.nullness.qual.NonNull; + +public record TransferPayload(byte[] data) implements CustomPacketPayload { + public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(TransferPayload::write, TransferPayload::new); + public static final CustomPacketPayload.Type TYPE = new Type<>(ResourceLocation.parse("floodgate:transfer")); + + private TransferPayload(FriendlyByteBuf friendlyByteBuf) { + this(ByteBufUtil.getBytes(friendlyByteBuf)); + friendlyByteBuf.readerIndex(friendlyByteBuf.readerIndex() + this.data.length); + } + + private void write(FriendlyByteBuf friendlyByteBuf) { + friendlyByteBuf.writeBytes(this.data); + } + + @Override + public CustomPacketPayload.@NonNull Type type() { + return TYPE; + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/util/ModCommandUtil.java b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModCommandUtil.java new file mode 100644 index 000000000..f555ac905 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModCommandUtil.java @@ -0,0 +1,120 @@ +package org.geysermc.floodgate.mod.util; + +import com.mojang.authlib.GameProfile; +import io.micronaut.context.BeanProvider; +import jakarta.inject.Inject; +import lombok.Setter; +import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.UserWhiteListEntry; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.api.GeyserApiBase; +import org.geysermc.floodgate.core.connection.audience.UserAudience; +import org.geysermc.floodgate.core.logger.FloodgateLogger; +import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.geysermc.floodgate.core.util.LanguageManager; +import org.incendo.cloud.CommandManager; + +import java.util.Collection; +import java.util.UUID; + +public final class ModCommandUtil extends CommandUtil { + private UserAudience console; + @Setter + private CommandManager commandManager; + + BeanProvider server; + BeanProvider audience; + + @Inject + public ModCommandUtil( + LanguageManager manager, + GeyserApiBase api, + BeanProvider server, + BeanProvider audience) { + super(manager, api); + this.server = server; + this.audience = audience; + } + + @Override + public @NonNull UserAudience getUserAudience(final @NonNull Object sourceObj) { + if (!(sourceObj instanceof CommandSourceStack stack)) { + throw new IllegalArgumentException("Source has to be a CommandSourceStack"); + } + if (stack.getEntity() == null) { + if (console != null) { + return console; + } + return console = new UserAudience.ConsoleAudience(stack, this); + } + ServerPlayer player = stack.getPlayer(); + //Locale locale = PlayerLocales.locale(player); + return new UserAudience.PlayerAudience(player.getUUID(), player.getGameProfile().getName(), "en_US", + stack, this, true); + } + + @Override + protected String getUsernameFromSource(@NonNull Object source) { + return ((ServerPlayer) source).getGameProfile().getName(); + } + + @Override + protected UUID getUuidFromSource(@NonNull Object source) { + return ((ServerPlayer) source).getUUID(); + } + + @Override + public Object getPlayerByUuid(@NonNull UUID uuid) { + ServerPlayer player = server.get().getPlayerList().getPlayer(uuid); + return player != null ? player : uuid; + } + + @Override + public Object getPlayerByUsername(@NonNull String username) { + ServerPlayer player = server.get().getPlayerList().getPlayerByName(username); + return player != null ? player : username; + } + + @Override + protected Collection getOnlinePlayers() { + return server.get().getPlayerList().getPlayers(); + } + + @Override + public boolean hasPermission(Object source, String permission) { + return commandManager.hasPermission(getUserAudience(source), permission); + } + + @Override + public void sendMessage(Object target, net.kyori.adventure.text.Component message) { + CommandSourceStack commandSource = (CommandSourceStack) target; + if (commandSource.getEntity() instanceof ServerPlayer) { + server.get().execute(() -> ((ServerPlayer) commandSource.getEntity()) + .displayClientMessage(audience.get().asNative(message), false)); + } + } + + @Override + public void kickPlayer(Object o, net.kyori.adventure.text.Component message) { + if (o instanceof ServerPlayer player) { + player.connection.disconnect(audience.get().asNative(message)); + } + } + + @Override + public boolean whitelistPlayer(UUID uuid, String username) { + GameProfile profile = new GameProfile(uuid, username); + server.get().getPlayerList().getWhiteList().add(new UserWhiteListEntry(profile)); + return true; + } + + @Override + public boolean removePlayerFromWhitelist(UUID uuid, String username) { + GameProfile profile = new GameProfile(uuid, username); + server.get().getPlayerList().getWhiteList().remove(profile); + return true; + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/util/ModMixinConfigPlugin.java b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModMixinConfigPlugin.java new file mode 100644 index 000000000..47945b7f3 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModMixinConfigPlugin.java @@ -0,0 +1,59 @@ +package org.geysermc.floodgate.mod.util; + +import dev.architectury.injectables.annotations.ExpectPlatform; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +public class ModMixinConfigPlugin implements IMixinConfigPlugin { + + @Override + public void onLoad(String mixinPackage) { + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + if (mixinClassName.equals("org.geysermc.floodgate.mod.mixin.ClientIntentionPacketMixin")) { + return applyProxyFix(); + } + if (mixinClassName.equals("org.geysermc.floodgate.mod.mixin.GeyserModInjectorMixin")) { + return isGeyserLoaded(); + } + return true; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + @ExpectPlatform + public static boolean isGeyserLoaded() { + throw new IllegalStateException("isGeyserLoaded is not implemented!"); + } + + @ExpectPlatform + public static boolean applyProxyFix() { + throw new IllegalStateException("applyProxyFix is not implemented!"); + } +} \ No newline at end of file diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/util/ModPlatformUtils.java b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModPlatformUtils.java new file mode 100644 index 000000000..75f460a5c --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModPlatformUtils.java @@ -0,0 +1,29 @@ +package org.geysermc.floodgate.mod.util; + +import io.micronaut.context.BeanProvider; +import jakarta.inject.Inject; +import net.minecraft.SharedConstants; +import net.minecraft.server.MinecraftServer; +import org.geysermc.floodgate.core.platform.util.PlatformUtils; + +public class ModPlatformUtils extends PlatformUtils { + + @Inject + BeanProvider minecraftServer; + + @Override + public AuthType authType() { + // TODO proxied auth type + return minecraftServer.get().usesAuthentication() ? AuthType.ONLINE : AuthType.OFFLINE; + } + + @Override + public String minecraftVersion() { + return SharedConstants.getCurrentVersion().name(); + } + + @Override + public String serverImplementationName() { + return minecraftServer.get().getServerModName(); + } +} diff --git a/mod/src/main/resources/floodgate.accesswidener b/mod/src/main/resources/floodgate.accesswidener new file mode 100644 index 000000000..f3860e9f3 --- /dev/null +++ b/mod/src/main/resources/floodgate.accesswidener @@ -0,0 +1,8 @@ +accessWidener v1 named + +# For setting gameprofile and starting connection verification +accessible method net/minecraft/server/network/ServerLoginPacketListenerImpl startClientVerification (Lcom/mojang/authlib/GameProfile;)V +# For player skin refreshing +accessible class net/minecraft/server/level/ChunkMap$TrackedEntity +# To access skins +accessible field net/minecraft/world/entity/Entity level Lnet/minecraft/world/level/Level; \ No newline at end of file diff --git a/mod/src/main/resources/floodgate.mixins.json b/mod/src/main/resources/floodgate.mixins.json new file mode 100644 index 000000000..d3597fb18 --- /dev/null +++ b/mod/src/main/resources/floodgate.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.geysermc.floodgate.mod.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "ChunkMapMixin", + "ClientIntentionPacketMixin", + "ClientIntentionPacketMixinInterface", + "ConnectionMixin", + "FloodgateUtilMixin", + "GeyserModInjectorMixin", + "ServerConnectionListenerMixin" + ], + "plugin": "org.geysermc.floodgate.mod.util.ModMixinConfigPlugin", + "injectors": { + "defaultRequire": 1 + } +} diff --git a/mod/src/main/resources/org.geysermc.floodgate.core.util.AutoBind b/mod/src/main/resources/org.geysermc.floodgate.core.util.AutoBind new file mode 100644 index 000000000..7d85275b5 --- /dev/null +++ b/mod/src/main/resources/org.geysermc.floodgate.core.util.AutoBind @@ -0,0 +1,3 @@ +org.geysermc.floodgate.core.util.Metrics +org.geysermc.floodgate.core.news.NewsChecker +org.geysermc.floodgate.core.util.PostEnableMessages diff --git a/neoforge/base/build.gradle.kts b/neoforge/base/build.gradle.kts new file mode 100644 index 000000000..6cabf52e0 --- /dev/null +++ b/neoforge/base/build.gradle.kts @@ -0,0 +1,61 @@ +architectury { + platformSetupLoomIde() + neoForge() +} + +provided("com.google.guava", "failureaccess") + +// Used to extend runtime/compile classpaths +val common: Configuration by configurations.creating +// Needed to read mixin config in the runServer task, and for the architectury transformer +// (e.g. the @ExpectPlatform annotation) +val developmentNeoForge: Configuration = configurations.getByName("developmentNeoForge") +// Our custom transitive include configuration +val includeTransitive: Configuration = configurations.getByName("includeTransitive") + +configurations { + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + developmentNeoForge.extendsFrom(configurations["common"]) +} + +dependencies { + // See https://github.com/google/guava/issues/6618 + modules { + module("com.google.guava:listenablefuture") { + replacedBy("com.google.guava:guava", "listenablefuture is part of guava") + } + } + + neoForge(libs.neoforge) + // "namedElements" configuration should be used to depend on different loom projects + common(project(":mod", configuration = "namedElements")) { isTransitive = false } + // Bundle transformed classes of the common module for production mod jar + shadow(project(path = ":mod", configuration = "transformProductionNeoForge")) { isTransitive = false } + + includeTransitive(libs.floodgate.core) + + implementation(libs.floodgate.core) + implementation(libs.guice) + + modImplementation(libs.cloud.neoforge) + include(libs.cloud.neoforge) +} + +tasks { + processResources { + from(project(":mod").file("src/main/resources/floodgate.accesswidener")) { + into("/assets/") + } + } + + remapJar { + dependsOn(processResources) + atAccessWideners.add("floodgate.accesswidener") + archiveBaseName.set("floodgate-neoforge") + } + +// modrinth { +// loaders.add("neoforge") +// } +} diff --git a/neoforge/base/src/main/java/org/geysermc/floodgate/mod/util/neoforge/ModMixinConfigPluginImpl.java b/neoforge/base/src/main/java/org/geysermc/floodgate/mod/util/neoforge/ModMixinConfigPluginImpl.java new file mode 100644 index 000000000..27159f6fc --- /dev/null +++ b/neoforge/base/src/main/java/org/geysermc/floodgate/mod/util/neoforge/ModMixinConfigPluginImpl.java @@ -0,0 +1,13 @@ +package org.geysermc.floodgate.mod.util.neoforge; + +import net.neoforged.fml.loading.LoadingModList; + +public class ModMixinConfigPluginImpl { + public static boolean isGeyserLoaded() { + return LoadingModList.get().getModFileById("geyser_neoforge") != null; + } + + public static boolean applyProxyFix() { + return false; + } +} diff --git a/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/NeoForgeFloodgateMod.java b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/NeoForgeFloodgateMod.java new file mode 100644 index 000000000..9268bbd58 --- /dev/null +++ b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/NeoForgeFloodgateMod.java @@ -0,0 +1,80 @@ +package org.geysermc.floodgate.platform.neoforge; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.GameShuttingDownEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.floodgate.core.module.PluginMessageModule; +import org.geysermc.floodgate.core.module.ServerCommonModule; +import org.geysermc.floodgate.mod.FloodgateMod; +import org.geysermc.floodgate.mod.util.ModTemplateReader; +import org.geysermc.floodgate.platform.neoforge.module.NeoForgeCommandModule; +import org.geysermc.floodgate.platform.neoforge.module.NeoForgePlatformModule; +import org.geysermc.floodgate.platform.neoforge.pluginmessage.NeoForgePluginMessageRegistration; + +import java.nio.file.Path; + +@Mod("floodgate") +public final class NeoForgeFloodgateMod extends FloodgateMod { + + private final ModContainer container; + + public NeoForgeFloodgateMod(IEventBus modEventBus, ModContainer container) { + this.container = container; + init( + new ServerCommonModule( + FMLPaths.CONFIGDIR.get().resolve("floodgate"), + new ModTemplateReader() + ), + new NeoForgePlatformModule(), + new NeoForgeCommandModule() + ); + + modEventBus.addListener(this::onRegisterPackets); + NeoForge.EVENT_BUS.addListener(this::onServerStarted); + if (FMLLoader.getDist().isClient()) { + NeoForge.EVENT_BUS.addListener(this::onClientStop); + } else { + NeoForge.EVENT_BUS.addListener(this::onServerStop); + } + } + + private void onServerStarted(ServerStartedEvent event) { + this.enable(event.getServer()); + } + + private void onClientStop(GameShuttingDownEvent ignored) { + this.disable(); + } + + private void onServerStop(ServerStoppingEvent ignored) { + this.disable(); + } + + private void onRegisterPackets(final RegisterPayloadHandlersEvent event) { + // Set the registrar once we're given it - NeoForgePluginMessageRegistration was created earlier in NeoForgePlatformModule + NeoForgePluginMessageRegistration pluginMessageRegistration = injector.getInstance(NeoForgePluginMessageRegistration.class); + pluginMessageRegistration.setRegistrar(event.registrar("floodgate").optional()); + + // We can now trigger the registering of our plugin message channels + enable(new PluginMessageModule()); + } + + @Override + public @Nullable Path resourcePath(String file) { + return container.getModInfo().getOwningFile().getFile().findResource(file); + } + + @Override + public boolean isClient() { + return FMLLoader.getDist().isClient(); + } + +} diff --git a/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/listener/NeoForgeEventRegistration.java b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/listener/NeoForgeEventRegistration.java new file mode 100644 index 000000000..77afc9145 --- /dev/null +++ b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/listener/NeoForgeEventRegistration.java @@ -0,0 +1,20 @@ +package org.geysermc.floodgate.platform.neoforge.listener; + +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import org.geysermc.floodgate.core.platform.listener.ListenerRegistration; +import org.geysermc.floodgate.mod.listener.ModEventListener; + +public final class NeoForgeEventRegistration implements ListenerRegistration { + private ModEventListener listener; + + @Override + public void register(ModEventListener listener) { + NeoForge.EVENT_BUS.addListener(this::onPlayerJoin); + this.listener = listener; + } + + private void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) { + listener.onPlayerJoin(event.getEntity().getUUID()); + } +} diff --git a/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgeCommandModule.java b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgeCommandModule.java new file mode 100644 index 000000000..5b06fe88e --- /dev/null +++ b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgeCommandModule.java @@ -0,0 +1,29 @@ +package org.geysermc.floodgate.platform.neoforge.module; + +import com.google.inject.Provides; +import com.google.inject.Singleton; +import lombok.SneakyThrows; +import org.geysermc.floodgate.core.module.CommandModule; +import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.geysermc.floodgate.core.player.FloodgateCommandPreprocessor; +import org.geysermc.floodgate.core.player.UserAudience; +import org.geysermc.floodgate.core.player.audience.FloodgateSenderMapper; +import org.geysermc.floodgate.mod.util.ModCommandUtil; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.neoforge.NeoForgeServerCommandManager; + +public class NeoForgeCommandModule extends CommandModule { + @Provides + @Singleton + @SneakyThrows + public CommandManager commandManager(CommandUtil commandUtil) { + CommandManager commandManager = new NeoForgeServerCommandManager<>( + ExecutionCoordinator.simpleCoordinator(), + new FloodgateSenderMapper<>(commandUtil) + ); + commandManager.registerCommandPreProcessor(new FloodgateCommandPreprocessor<>(commandUtil)); + ((ModCommandUtil) commandUtil).setCommandManager(commandManager); + return commandManager; + } +} diff --git a/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgePlatformModule.java b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgePlatformModule.java new file mode 100644 index 000000000..f224ad0f3 --- /dev/null +++ b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgePlatformModule.java @@ -0,0 +1,46 @@ +package org.geysermc.floodgate.platform.neoforge.module; + +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import org.geysermc.floodgate.core.platform.listener.ListenerRegistration; +import org.geysermc.floodgate.core.platform.pluginmessage.PluginMessageUtils; +import org.geysermc.floodgate.core.pluginmessage.PluginMessageRegistration; +import org.geysermc.floodgate.mod.listener.ModEventListener; +import org.geysermc.floodgate.mod.module.ModPlatformModule; +import org.geysermc.floodgate.platform.neoforge.listener.NeoForgeEventRegistration; +import org.geysermc.floodgate.platform.neoforge.pluginmessage.NeoForgePluginMessageRegistration; +import org.geysermc.floodgate.platform.neoforge.pluginmessage.NeoForgePluginMessageUtils; + +public class NeoForgePlatformModule extends ModPlatformModule { + + @Override + protected void configure() { + super.configure(); + + // We retrieve using NeoForgePluginMessageRegistration.class from our the mod class. + // We do this to ensure that injector#getInstance with either class returns the same singleton + bind(PluginMessageRegistration.class).to(NeoForgePluginMessageRegistration.class).in(Scopes.SINGLETON); + bind(NeoForgePluginMessageRegistration.class).toInstance(new NeoForgePluginMessageRegistration()); + } + + @Provides + @Singleton + public ListenerRegistration listenerRegistration() { + return new NeoForgeEventRegistration(); + } + + @Provides + @Singleton + public PluginMessageUtils pluginMessageUtils() { + return new NeoForgePluginMessageUtils(); + } + + @Provides + @Named("implementationName") + public String implementationName() { + return "NeoForge"; + } + +} diff --git a/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageRegistration.java b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageRegistration.java new file mode 100644 index 000000000..bd73bb3fb --- /dev/null +++ b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageRegistration.java @@ -0,0 +1,39 @@ +package org.geysermc.floodgate.platform.neoforge.pluginmessage; + +import lombok.Setter; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import org.geysermc.floodgate.core.pluginmessage.PluginMessageChannel; +import org.geysermc.floodgate.core.pluginmessage.PluginMessageRegistration; +import org.geysermc.floodgate.mod.pluginmessage.payloads.FormPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.PacketPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.SkinPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.TransferPayload; + +public class NeoForgePluginMessageRegistration implements PluginMessageRegistration { + + @Setter + private PayloadRegistrar registrar; + + @Override + public void register(PluginMessageChannel channel) { + switch (channel.getIdentifier()) { + case "floodgate:form" -> + registrar.playBidirectional(FormPayload.TYPE, FormPayload.STREAM_CODEC, (payload, context) -> + channel.handleServerCall(payload.data(), context.player().getUUID(), + context.player().getGameProfile().getName())); + case "floodgate:packet" -> + registrar.playBidirectional(PacketPayload.TYPE, PacketPayload.STREAM_CODEC, (payload, context) -> + channel.handleServerCall(payload.data(), context.player().getUUID(), + context.player().getGameProfile().getName())); + case "floodgate:skin" -> + registrar.playBidirectional(SkinPayload.TYPE, SkinPayload.STREAM_CODEC, (payload, context) -> + channel.handleServerCall(payload.data(), context.player().getUUID(), + context.player().getGameProfile().getName())); + case "floodgate:transfer" -> + registrar.playBidirectional(TransferPayload.TYPE, TransferPayload.STREAM_CODEC, (payload, context) -> + channel.handleServerCall(payload.data(), context.player().getUUID(), + context.player().getGameProfile().getName())); + default -> throw new IllegalArgumentException("unknown channel: " + channel); + } + } +} diff --git a/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageUtils.java b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageUtils.java new file mode 100644 index 000000000..5faf4d5bf --- /dev/null +++ b/neoforge/base/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageUtils.java @@ -0,0 +1,37 @@ +package org.geysermc.floodgate.platform.neoforge.pluginmessage; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.network.PacketDistributor; +import org.geysermc.floodgate.core.platform.pluginmessage.PluginMessageUtils; +import org.geysermc.floodgate.mod.MinecraftServerHolder; +import org.geysermc.floodgate.mod.pluginmessage.payloads.FormPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.PacketPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.SkinPayload; +import org.geysermc.floodgate.mod.pluginmessage.payloads.TransferPayload; + +import java.util.Objects; +import java.util.UUID; + +public class NeoForgePluginMessageUtils extends PluginMessageUtils { + public boolean sendMessage(UUID uuid, String channel, byte[] data) { + try { + ServerPlayer player = MinecraftServerHolder.get().getPlayerList().getPlayer(uuid); + final CustomPacketPayload payload; + switch (channel) { + case "floodgate:form" -> payload = new FormPayload(data); + case "floodgate:packet" -> payload = new PacketPayload(data); + case "floodgate:skin" -> payload = new SkinPayload(data); + case "floodgate:transfer" -> payload = new TransferPayload(data); + default -> throw new IllegalArgumentException("unknown channel: " + channel); + } + + Objects.requireNonNull(player); + PacketDistributor.sendToPlayer(player, payload); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } +} diff --git a/neoforge/base/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/base/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 000000000..7d7373f5d --- /dev/null +++ b/neoforge/base/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,25 @@ +modLoader="javafml" +loaderVersion="[1,)" +license="MIT" +[[mods]] +modId="$id" +version="$version" +displayName="$name" +displayURL="$url" +logoFile= "../assets/floodgate/icon.png" +authors="$author" +description="$description" +[[mixins]] +config = "floodgate.mixins.json" +[[dependencies.floodgate]] +modId="neoforge" +type="required" +versionRange="[21.0.0-beta,)" +ordering="NONE" +side="BOTH" +[[dependencies.floodgate]] +modId="minecraft" +type="required" +versionRange="[$minecraft_version,)" +ordering="NONE" +side="BOTH" diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties new file mode 100644 index 000000000..7da18ea6f --- /dev/null +++ b/neoforge/gradle.properties @@ -0,0 +1 @@ +loom.platform=neoforge diff --git a/neoforge/isolated/build.gradle.kts b/neoforge/isolated/build.gradle.kts new file mode 100644 index 000000000..31674df67 --- /dev/null +++ b/neoforge/isolated/build.gradle.kts @@ -0,0 +1,57 @@ +architectury { + platformSetupLoomIde() + neoForge() +} + +provided("com.google.guava", "failureaccess") + +// Used to extend runtime/compile classpaths +val common: Configuration by configurations.creating +// Needed to read mixin config in the runServer task, and for the architectury transformer +// (e.g. the @ExpectPlatform annotation) +val developmentNeoForge: Configuration = configurations.getByName("developmentNeoForge") +// Our custom transitive include configuration +val includeTransitive: Configuration = configurations.getByName("includeTransitive") + +configurations { + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + developmentNeoForge.extendsFrom(configurations["common"]) +} + +dependencies { + // See https://github.com/google/guava/issues/6618 + modules { + module("com.google.guava:listenablefuture") { + replacedBy("com.google.guava:guava", "listenablefuture is part of guava") + } + } + + neoForge(libs.neoforge) + // "namedElements" configuration should be used to depend on different loom projects + common(project(":mod", configuration = "namedElements")) + // Bundle transformed classes of the common module for production mod jar + shadow(project(path = ":mod", configuration = "transformProductionNeoForge")) { isTransitive = false } + + includeTransitive(libs.floodgate.core) + + implementation(libs.floodgate.core) + implementation(libs.guice) + + modImplementation(libs.cloud.neoforge) + include(libs.cloud.neoforge) +} + +tasks { + processResources { + from(project(":mod").file("src/main/resources/floodgate.accesswidener")) { + into("/assets/") + } + } + + remapJar { + dependsOn(processResources) + atAccessWideners.add("floodgate.accesswidener") + archiveBaseName.set("floodgate-neoforge") + } +} diff --git a/neoforge/isolated/src/main/java/org/geysermc/floodgate/neoforge/IsolatedNeoForgeMod.java b/neoforge/isolated/src/main/java/org/geysermc/floodgate/neoforge/IsolatedNeoForgeMod.java new file mode 100644 index 000000000..fedb94041 --- /dev/null +++ b/neoforge/isolated/src/main/java/org/geysermc/floodgate/neoforge/IsolatedNeoForgeMod.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.spigot; + +import java.util.List; +import org.geysermc.floodgate.isolation.loader.PlatformHolder; +import org.geysermc.floodgate.isolation.loader.PlatformLoader; + +@Mod("floodgate") +public final class IsolatedNeoForgeMod { + private PlatformHolder holder; + + public IsolatedNeoForgeMod(IEventBus modEventBus, ModContainer container) { + try { + var libsDirectory = getDataFolder().toPath().resolve("libs"); + holder = PlatformLoader.loadDefault(getClass().getClassLoader(), libsDirectory); + holder.init(List.of(ModContainer.class), List.of(this)); + } catch (Exception exception) { + throw new RuntimeException("Failed to load Floodgate", exception); + } + + // TODO move to base + //modEventBus.addListener(this::onRegisterPackets); + NeoForge.EVENT_BUS.addListener(this::onServerStarted); + if (FMLLoader.getDist().isClient()) { + NeoForge.EVENT_BUS.addListener(this::onClientStop); + } else { + NeoForge.EVENT_BUS.addListener(this::onServerStop); + } + } + + private void onServerStarted(ServerStartedEvent event) { + holder.enable(); + } + + private void onClientStop(GameShuttingDownEvent ignored) { + holder.shutdown(); + } + + private void onServerStop(ServerStoppingEvent ignored) { + if (FMLLoader.getDist().isClient()) { + holder.disable(); + } else { + holder.shutdown(); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 802bb2725..00095dca0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,49 +1,19 @@ @file:Suppress("UnstableApiUsage") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -dependencyResolutionManagement { - repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS - repositories { - mavenLocal() - - // Geyser, Cumulus etc. - maven("https://repo.opencollab.dev/maven-releases") { - mavenContent { releasesOnly() } - } - maven("https://repo.opencollab.dev/maven-snapshots") { - mavenContent { snapshotsOnly() } - } - - // Paper, Velocity -// maven("https://repo.papermc.io/repository/maven-releases") { -// mavenContent { releasesOnly() } -// } -// maven("https://repo.papermc.io/repository/maven-snapshots") { -// mavenContent { snapshotsOnly() } -// } - maven("https://repo.papermc.io/repository/maven-public") - // Spigot - maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots") { - mavenContent { snapshotsOnly() } - } - - // BungeeCord - maven("https://oss.sonatype.org/content/repositories/snapshots") { - mavenContent { snapshotsOnly() } - } - - maven("https://libraries.minecraft.net") { - name = "minecraft" - mavenContent { releasesOnly() } - } - - mavenCentral() - } -} - pluginManagement { repositories { gradlePluginPortal() + maven("https://repo.opencollab.dev/main/") + maven("https://jitpack.io") { + content { + includeGroupByRegex("com\\.github\\..*") + } + } + + maven("https://maven.architectury.dev/") + maven("https://maven.neoforged.net/releases") + maven("https://maven.fabricmc.net/") } plugins { id("net.kyori.indra") @@ -56,6 +26,7 @@ rootProject.name = "floodgate-parent" include(":api") include(":universal") include(":isolation") +include(":mod") arrayOf("common", "netty4").forEach { val id = ":core-$it" @@ -63,7 +34,7 @@ arrayOf("common", "netty4").forEach { project(id).projectDir = file("core/$it") } -arrayOf("bungee", "spigot", "velocity").forEach { platform -> +arrayOf("bungee", "spigot", "velocity", "fabric").forEach { platform -> arrayOf("base", "isolated").forEach { var id = ":$platform-$it" // isolated is the new default diff --git a/spigot/base/build.gradle.kts b/spigot/base/build.gradle.kts index 0e27097de..2a24e4bc5 100644 --- a/spigot/base/build.gradle.kts +++ b/spigot/base/build.gradle.kts @@ -1,3 +1,8 @@ +plugins { + id("floodgate.publish-conventions") + id("floodgate.shadow-conventions") +} + dependencies { api(projects.coreNetty4) annotationProcessor(projects.coreNetty4) diff --git a/spigot/isolated/build.gradle.kts b/spigot/isolated/build.gradle.kts index 670f15353..2f30520b5 100644 --- a/spigot/isolated/build.gradle.kts +++ b/spigot/isolated/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - java + id("floodgate.base-conventions") } dependencies { diff --git a/universal/build.gradle.kts b/universal/build.gradle.kts index c4665b2c3..bf59faffc 100644 --- a/universal/build.gradle.kts +++ b/universal/build.gradle.kts @@ -1,5 +1,7 @@ plugins { + id("floodgate.publish-conventions") id("floodgate.generate-templates") + id("floodgate.shadow-conventions") } provided(libs.bungee) diff --git a/velocity/base/build.gradle.kts b/velocity/base/build.gradle.kts index efc0959c6..84019d584 100644 --- a/velocity/base/build.gradle.kts +++ b/velocity/base/build.gradle.kts @@ -1,3 +1,8 @@ +plugins { + id("floodgate.publish-conventions") + id("floodgate.shadow-conventions") +} + dependencies { api(projects.coreNetty4) annotationProcessor(projects.coreNetty4) diff --git a/velocity/isolated/build.gradle.kts b/velocity/isolated/build.gradle.kts index 2a2d9f6bc..9fe00bf1c 100644 --- a/velocity/isolated/build.gradle.kts +++ b/velocity/isolated/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - java + id("floodgate.base-conventions") } dependencies {