diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..c32bad0c --- /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/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..08b4c8ba --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,31 @@ +name: Publish +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Setup Gradle + uses: GeyserMC/actions/setup-gradle-composite@master + with: + setup-java_java-version: 21 + + - name: Build Floodgate-Modded + run: ./gradlew build + + - name: Publish to Modrinth + if: ${{ success() && github.repository == 'GeyserMC/Floodgate-Modded' && github.ref_name == 'master' }} + uses: gradle/gradle-build-action@3bfe3a46584a206fb8361cdedd0647b0c4204232 + env: + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + with: + arguments: modrinth + gradle-home-cache-cleanup: true + + - name: Archive Artifacts + uses: GeyserMC/actions/upload-multi-artifact@master + if: success() + with: + artifacts: | + Floodgate-Fabric:fabric/build/libs/floodgate-fabric.jar + Floodgate-NeoForge:neoforge/build/libs/floodgate-neoforge.jar diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 00000000..47900794 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +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.indra) + implementation(libs.shadow) + implementation(libs.architectury.plugin) + implementation(libs.architectury.loom) + implementation(libs.minotaur) +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 00000000..63bde189 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,11 @@ +@file:Suppress("UnstableApiUsage") + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" \ No newline at end of file diff --git a/build-logic/src/main/kotlin/LibsAccessor.kt b/build-logic/src/main/kotlin/LibsAccessor.kt new file mode 100644 index 00000000..2a0c09eb --- /dev/null +++ b/build-logic/src/main/kotlin/LibsAccessor.kt @@ -0,0 +1,6 @@ +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +val Project.libs: LibrariesForLibs + get() = rootProject.extensions.getByType() \ No newline at end of file diff --git a/build-logic/src/main/kotlin/extensions.kt b/build-logic/src/main/kotlin/extensions.kt new file mode 100644 index 00000000..f4c2269e --- /dev/null +++ b/build-logic/src/main/kotlin/extensions.kt @@ -0,0 +1,36 @@ +import org.gradle.api.Project +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.provider.Provider + +val providedDependencies = mutableMapOf>() + +fun Project.provided(pattern: String, name: String, excludedOn: Int = 0b110) { + providedDependencies.getOrPut(project.name) { mutableSetOf() } + .add("${calcExclusion(pattern, 0b100, excludedOn)}:${calcExclusion(name, 0b10, excludedOn)}") +} + +fun Project.provided(dependency: ProjectDependency) = + provided(dependency.group!!, dependency.name) + +fun Project.provided(dependency: MinimalExternalModuleDependency) = + provided(dependency.module.group, dependency.module.name) + +fun Project.provided(provider: Provider) = + provided(provider.get()) + +fun getProvidedDependenciesForProject(projectName: String): MutableSet { + return providedDependencies.getOrDefault(projectName, emptySet()).toMutableSet() +} + +private fun calcExclusion(section: String, bit: Int, excludedOn: Int): String = + if (excludedOn and bit > 0) section else "" + +fun projectVersion(project: Project): String = + project.version.toString().replace("SNAPSHOT", "b" + buildNumber()) + +fun versionName(project: Project): String = + "Floodgate-" + project.name.replaceFirstChar { it.uppercase() } + "-" + projectVersion(project) + +fun buildNumber(): Int = + (System.getenv("GITHUB_RUN_NUMBER"))?.let { Integer.parseInt(it) } ?: -1 diff --git a/build-logic/src/main/kotlin/floodgate-modded.base-conventions.gradle.kts b/build-logic/src/main/kotlin/floodgate-modded.base-conventions.gradle.kts new file mode 100644 index 00000000..77ef1e80 --- /dev/null +++ b/build-logic/src/main/kotlin/floodgate-modded.base-conventions.gradle.kts @@ -0,0 +1,37 @@ +plugins { + `java-library` + id("net.kyori.indra") +} + +dependencies { + compileOnly("org.checkerframework", "checker-qual", "3.19.0") +} + +indra { + github("GeyserMC", "floodgate-modded") { + ci(true) + issues(true) + scm(true) + } + mitLicense() + + javaVersions { + target(21) + } +} + +tasks { + processResources { + filesMatching(listOf("fabric.mod.json", "META-INF/neoforge.mods.toml")) { + expand( + "id" to "floodgate", + "name" to "Floodgate", + "version" to project.version, + "description" to project.description, + "url" to "https://geysermc.org", + "author" to "GeyserMC", + "minecraft_version" to libs.versions.minecraft.version.get() + ) + } + } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/floodgate-modded.build-logic.gradle.kts b/build-logic/src/main/kotlin/floodgate-modded.build-logic.gradle.kts new file mode 100644 index 00000000..ff9b5674 --- /dev/null +++ b/build-logic/src/main/kotlin/floodgate-modded.build-logic.gradle.kts @@ -0,0 +1,14 @@ +repositories { + // mavenLocal() + mavenCentral() + maven("https://maven.fabricmc.net/") + maven("https://maven.neoforged.net/releases") + maven("https://repo.opencollab.dev/main/") + maven("https://jitpack.io") { + content { + includeGroupByRegex("com.github.*") + } + } + maven("https://oss.sonatype.org/content/repositories/snapshots/") + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/floodgate-modded.platform-conventions.gradle.kts b/build-logic/src/main/kotlin/floodgate-modded.platform-conventions.gradle.kts new file mode 100644 index 00000000..e2a1d829 --- /dev/null +++ b/build-logic/src/main/kotlin/floodgate-modded.platform-conventions.gradle.kts @@ -0,0 +1,138 @@ +plugins { + id("floodgate-modded.publish-conventions") + id("architectury-plugin") + id("dev.architectury.loom") + id("com.modrinth.minotaur") +} + +// These are all provided by Minecraft/server platforms +provided("com.google.code.gson", "gson") +provided("org.slf4j", ".*") +provided("com.google.guava", "guava") +provided("org.ow2.asm", "asm") +provided("com.nukkitx.fastutil", ".*") + +// these we just don't want to include +provided("org.checkerframework", ".*") +provided("com.google.errorprone", ".*") +provided("com.github.spotbugs", "spotbugs-annotations") +provided("com.google.code.findbugs", ".*") + +// cloud-fabric/cloud-neoforge jij's all cloud depends already +provided("org.incendo", ".*") +provided("io.leangen.geantyref", "geantyref") + +architectury { + minecraft = libs.versions.minecraft.version.get() +} + +loom { + silentMojangMappingsLicense() +} + +configurations { + create("includeTransitive").isTransitive = true +} + +dependencies { + minecraft(libs.minecraft) + mappings(loom.officialMojangMappings()) + + // These are under our own namespace + shadow(libs.floodgate.api) { isTransitive = false } + shadow(libs.floodgate.core) { isTransitive = false } + + // Requires relocation + shadow(libs.bstats) { isTransitive = false } + + // Shadow & relocate these since the (indirectly) depend on quite old dependencies + shadow(libs.guice) { isTransitive = false } + shadow(libs.configutils) { + exclude("org.checkerframework") + exclude("com.google.errorprone") + exclude("com.github.spotbugs") + exclude("com.nukkitx.fastutil") + } + +} + +tasks { + sourcesJar { + archiveClassifier.set("sources") + from(sourceSets.main.get().allSource) + } + + shadowJar { + // Mirrors the example fabric project, otherwise tons of dependencies are shaded that shouldn't be + configurations = listOf(project.configurations.shadow.get()) + + // Relocate these + relocate("org.bstats", "org.geysermc.floodgate.shadow.bstats") + relocate("com.google.inject", "org.geysermc.floodgate.shadow.google.inject") + relocate("org.yaml", "org.geysermc.floodgate.shadow.org.yaml") + + // The remapped shadowJar is the final desired mod jar + archiveVersion.set(project.version.toString()) + archiveClassifier.set("shaded") + } + + remapJar { + dependsOn(shadowJar) + inputFile.set(shadowJar.get().archiveFile) + archiveClassifier.set("") + archiveVersion.set("") + } + + register("renameTask") { + dependsOn(remapJar) + + val modrinthFileName = "${versionName(project)}.jar" + val libsFile = remapJar.get().destinationDirectory.get().asFile + + from(remapJar.get().archiveFile) + rename { modrinthFileName } + into(libsFile) + + outputs.file(libsFile.resolve(modrinthFileName)) + } + + // Readme sync + modrinth.get().dependsOn(tasks.modrinthSyncBody) + modrinth.get().dependsOn(tasks.getByName("renameTask")) +} + +afterEvaluate { + val providedDependencies = getProvidedDependenciesForProject(project.name) + + // These are shaded, no need to JiJ them + configurations["shadow"].resolvedConfiguration.resolvedArtifacts.forEach {shadowed -> + val string = "${shadowed.moduleVersion.id.group}:${shadowed.moduleVersion.id.name}" + println("Not including shadowed dependency: $string") + providedDependencies.add(string) + } + + configurations["includeTransitive"].resolvedConfiguration.resolvedArtifacts.forEach { dep -> + if (!providedDependencies.contains("${dep.moduleVersion.id.group}:${dep.moduleVersion.id.name}") + and !providedDependencies.contains("${dep.moduleVersion.id.group}:.*")) { + println("Including dependency via JiJ: ${dep.id}") + dependencies.add("include", dep.moduleVersion.id.toString()) + } else { + println("Not including ${dep.id} for ${project.name}!") + } + } +} + +modrinth { + token.set(System.getenv("MODRINTH_TOKEN")) // Even though this is the default value, apparently this prevents GitHub Actions caching the token? + projectId.set("bWrNNfkb") + versionName.set(versionName(project)) + versionNumber.set(projectVersion(project)) + versionType.set("release") + changelog.set("A changelog can be found at https://github.com/GeyserMC/Floodgate-Modded/commits") + + syncBodyFrom.set(rootProject.file("README.md").readText()) + + uploadFile.set(tasks.remapJar.get().destinationDirectory.get().asFile.resolve("${versionName(project)}.jar")) + gameVersions.add(libs.minecraft.get().version as String) + failSilently.set(false) +} diff --git a/build-logic/src/main/kotlin/floodgate-modded.publish-conventions.gradle.kts b/build-logic/src/main/kotlin/floodgate-modded.publish-conventions.gradle.kts new file mode 100644 index 00000000..fdfa4ef6 --- /dev/null +++ b/build-logic/src/main/kotlin/floodgate-modded.publish-conventions.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("floodgate-modded.shadow-conventions") + id("net.kyori.indra.publishing") +} + +indra { + publishSnapshotsTo("geysermc", "https://repo.opencollab.dev/maven-snapshots") + publishReleasesTo("geysermc", "https://repo.opencollab.dev/maven-releases") +} + +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() } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/floodgate-modded.shadow-conventions.gradle.kts b/build-logic/src/main/kotlin/floodgate-modded.shadow-conventions.gradle.kts new file mode 100644 index 00000000..e4affd71 --- /dev/null +++ b/build-logic/src/main/kotlin/floodgate-modded.shadow-conventions.gradle.kts @@ -0,0 +1,37 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id("floodgate-modded.base-conventions") + id("com.github.johnrengelman.shadow") +} + +tasks { + named("jar") { + archiveClassifier.set("unshaded") + from(project.rootProject.file("LICENSE")) + } + val shadowJar = named("shadowJar") { + archiveBaseName.set(project.name) + archiveVersion.set("") + archiveClassifier.set("") + + val sJar: ShadowJar = this + + doFirst { + providedDependencies[project.name]?.forEach { string -> + sJar.dependencies { + println("Excluding $string from ${project.name}") + exclude(dependency(string)) + } + } + + sJar.dependencies { + exclude(dependency("org.checkerframework:checker-qual:.*")) + exclude(dependency("org.jetbrains:annotations:.*")) + } + } + } + named("build") { + dependsOn(shadowJar) + } +} \ No newline at end of file diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts new file mode 100644 index 00000000..559f6bab --- /dev/null +++ b/fabric/build.gradle.kts @@ -0,0 +1,47 @@ +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")) { isTransitive = false } + // Bundle transformed classes of the common module for production mod jar + shadow(project(path = ":mod", configuration = "transformProductionFabric")) { + isTransitive = false + } + + includeTransitive(libs.floodgate.core) + implementation(libs.floodgate.core) + implementation(libs.guice) + + modImplementation(libs.cloud.fabric) + include(libs.cloud.fabric) + include(libs.fabric.permissions.api) +} + +tasks { + remapJar { + archiveBaseName.set("floodgate-fabric") + } + + modrinth { + loaders.add("fabric") + } +} diff --git a/fabric/gradle.properties b/fabric/gradle.properties new file mode 100644 index 00000000..90ee7a25 --- /dev/null +++ b/fabric/gradle.properties @@ -0,0 +1 @@ +loom.platform=fabric \ No newline at end of file diff --git a/fabric/src/main/java/org/geysermc/floodgate/mod/util/fabric/ModMixinConfigPluginImpl.java b/fabric/src/main/java/org/geysermc/floodgate/mod/util/fabric/ModMixinConfigPluginImpl.java new file mode 100644 index 00000000..ffd3fe3e --- /dev/null +++ b/fabric/src/main/java/org/geysermc/floodgate/mod/util/fabric/ModMixinConfigPluginImpl.java @@ -0,0 +1,14 @@ +package org.geysermc.floodgate.mod.util.fabric; + +import net.fabricmc.loader.api.FabricLoader; + +public class ModMixinConfigPluginImpl { + + public static boolean isGeyserLoaded() { + return FabricLoader.getInstance().isModLoaded("geyser-fabric"); + } + + public static boolean applyProxyFix() { + return FabricLoader.getInstance().isModLoaded("fabricproxy-lite"); + } +} diff --git a/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/FabricFloodgateMod.java b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/FabricFloodgateMod.java new file mode 100644 index 00000000..ed1a98cf --- /dev/null +++ b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/FabricFloodgateMod.java @@ -0,0 +1,54 @@ +package org.geysermc.floodgate.platform.fabric; + +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.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.fabric.module.FabricCommandModule; +import org.geysermc.floodgate.platform.fabric.module.FabricPlatformModule; + +import java.nio.file.Path; + +public final class FabricFloodgateMod extends FloodgateMod implements ModInitializer { + + private ModContainer container; + + @Override + public void onInitialize() { + container = FabricLoader.getInstance().getModContainer("floodgate").orElseThrow(); + init( + new ServerCommonModule( + FabricLoader.getInstance().getConfigDir().resolve("floodgate"), + new ModTemplateReader() + ), + new FabricPlatformModule(), + new FabricCommandModule(), + new PluginMessageModule() + ); + + ServerLifecycleEvents.SERVER_STARTED.register(this::enable); + + if (isClient()) { + ClientLifecycleEvents.CLIENT_STOPPING.register($ -> this.disable()); + } else { + ServerLifecycleEvents.SERVER_STOPPING.register($ -> this.disable()); + } + } + + @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/src/main/java/org/geysermc/floodgate/platform/fabric/listener/FabricEventRegistration.java b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/listener/FabricEventRegistration.java new file mode 100644 index 00000000..80e486d2 --- /dev/null +++ b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/listener/FabricEventRegistration.java @@ -0,0 +1,14 @@ +package org.geysermc.floodgate.platform.fabric.listener; + +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/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricCommandModule.java b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricCommandModule.java new file mode 100644 index 00000000..4d9ccb81 --- /dev/null +++ b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricCommandModule.java @@ -0,0 +1,32 @@ +package org.geysermc.floodgate.platform.fabric.module; + +import com.google.inject.Provides; +import com.google.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/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricPlatformModule.java b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/module/FabricPlatformModule.java new file mode 100644 index 00000000..2f4cf18a --- /dev/null +++ b/fabric/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/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageRegistration.java b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageRegistration.java new file mode 100644 index 00000000..d9da35c1 --- /dev/null +++ b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageRegistration.java @@ -0,0 +1,55 @@ +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; + +public class FabricPluginMessageRegistration implements PluginMessageRegistration { + @Override + public void register(PluginMessageChannel channel) { + switch (channel.getIdentifier()) { + case "floodgate:form" -> { + PayloadTypeRegistry.playC2S().register(FormPayload.TYPE, FormPayload.STREAM_CODEC); + PayloadTypeRegistry.playS2C().register(FormPayload.TYPE, FormPayload.STREAM_CODEC); + ServerPlayNetworking.registerGlobalReceiver(FormPayload.TYPE, + ((payload, context) -> channel.handleServerCall( + payload.data(), + context.player().getUUID(), + context.player().getGameProfile().getName()))); + } + case "floodgate:packet" -> { + PayloadTypeRegistry.playC2S().register(PacketPayload.TYPE, PacketPayload.STREAM_CODEC); + PayloadTypeRegistry.playS2C().register(PacketPayload.TYPE, PacketPayload.STREAM_CODEC); + ServerPlayNetworking.registerGlobalReceiver(PacketPayload.TYPE, + ((payload, context) -> channel.handleServerCall( + payload.data(), + context.player().getUUID(), + context.player().getGameProfile().getName()))); + } + case "floodgate:skin" -> { + PayloadTypeRegistry.playC2S().register(SkinPayload.TYPE, SkinPayload.STREAM_CODEC); + PayloadTypeRegistry.playS2C().register(SkinPayload.TYPE, SkinPayload.STREAM_CODEC); + ServerPlayNetworking.registerGlobalReceiver(SkinPayload.TYPE, + ((payload, context) -> channel.handleServerCall( + payload.data(), + context.player().getUUID(), + context.player().getGameProfile().getName()))); + } + case "floodgate:transfer" -> { + PayloadTypeRegistry.playC2S().register(TransferPayload.TYPE, TransferPayload.STREAM_CODEC); + PayloadTypeRegistry.playS2C().register(TransferPayload.TYPE, TransferPayload.STREAM_CODEC); + ServerPlayNetworking.registerGlobalReceiver(TransferPayload.TYPE, + ((payload, context) -> channel.handleServerCall( + payload.data(), + context.player().getUUID(), + context.player().getGameProfile().getName()))); + } + default -> throw new IllegalArgumentException("unknown channel: " + channel); + } + } +} diff --git a/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageUtils.java b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageUtils.java new file mode 100644 index 00000000..9aa1094f --- /dev/null +++ b/fabric/src/main/java/org/geysermc/floodgate/platform/fabric/pluginmessage/FabricPluginMessageUtils.java @@ -0,0 +1,39 @@ +package org.geysermc.floodgate.platform.fabric.pluginmessage; + +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +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 FabricPluginMessageUtils extends PluginMessageUtils { + + @Override + 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); + ServerPlayNetworking.send(player, payload); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } +} diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..ba341e71 --- /dev/null +++ b/fabric/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-Modded" + }, + "license": "MIT", + "environment": "*", + "entrypoints": { + "main": [ + "org.geysermc.floodgate.platform.fabric.FabricFloodgateMod" + ] + }, + "accessWidener": "floodgate.accesswidener", + "mixins": [ + "floodgate.mixins.json" + ], + "depends": { + "fabricloader": ">=0.15.10", + "fabric-api": "*", + "minecraft": ">=$minecraft_version" + } +} diff --git a/mod/build.gradle.kts b/mod/build.gradle.kts new file mode 100644 index 00000000..3dfb8039 --- /dev/null +++ b/mod/build.gradle.kts @@ -0,0 +1,33 @@ +architectury { + common("neoforge", "fabric") +} + +loom { + accessWidenerPath = file("src/main/resources/floodgate.accesswidener") + mixin.defaultRefmapName.set("floodgate-refmap.json") +} + +dependencies { + api(libs.floodgate.core) + api(libs.floodgate.api) + api(libs.guice) + + compileOnly(libs.mixin) + compileOnly(libs.asm) + modCompileOnly(libs.geyser.mod) { isTransitive = false } + modCompileOnly(libs.geyser.core) { isTransitive = false } + + // Only here to suppress "unknown enum constant EnvType.CLIENT" warnings. + compileOnly(libs.fabric.loader) +} + +afterEvaluate { + // We don't need these + tasks.named("renameTask").configure { + enabled = false + } + + tasks.named("modrinth").configure { + enabled = false + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/FloodgateMod.java b/mod/src/main/java/org/geysermc/floodgate/mod/FloodgateMod.java new file mode 100644 index 00000000..b765b4f0 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/FloodgateMod.java @@ -0,0 +1,59 @@ +package org.geysermc.floodgate.mod; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import net.minecraft.server.MinecraftServer; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.core.FloodgatePlatform; +import org.geysermc.floodgate.mod.module.ModAddonModule; +import org.geysermc.floodgate.mod.module.ModListenerModule; + +import java.nio.file.Path; + +public abstract class FloodgateMod { + public static FloodgateMod INSTANCE; + + private boolean started; + private FloodgatePlatform platform; + protected Injector injector; + + protected void init(Module... modules) { + INSTANCE = this; + injector = Guice.createInjector(modules); + platform = injector.getInstance(FloodgatePlatform.class); + } + + protected void enable(MinecraftServer server) { + long ctm = System.currentTimeMillis(); + + // Stupid hack, see the class for more information + // This can probably be Guice-i-fied but that is beyond me + MinecraftServerHolder.set(server); + + if (!started) { + platform.enable( + new ModAddonModule(), + new ModListenerModule() + ); + started = true; + } + + long endCtm = System.currentTimeMillis(); + injector.getInstance(FloodgateLogger.class) + .translatedInfo("floodgate.core.finish", endCtm - ctm); + } + + protected void disable() { + platform.disable(); + } + + protected void enable(Module... module) { + platform.enable(module); + } + + public @Nullable abstract Path resourcePath(String file); + + public abstract boolean isClient(); +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/MinecraftServerHolder.java b/mod/src/main/java/org/geysermc/floodgate/mod/MinecraftServerHolder.java new file mode 100644 index 00000000..85be6d28 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/MinecraftServerHolder.java @@ -0,0 +1,20 @@ +package org.geysermc.floodgate.mod; + +import net.minecraft.server.MinecraftServer; + +public final class MinecraftServerHolder { + // Static because commands *need* to be initialized before the server is available + // Otherwise it would be a class variable + private static MinecraftServer INSTANCE; + + public static MinecraftServer get() { + return INSTANCE; + } + + static void set(MinecraftServer instance) { + INSTANCE = instance; + } + + private MinecraftServerHolder() { + } +} 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 00000000..6f0bb8eb --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/data/ModDataAddon.java @@ -0,0 +1,58 @@ +package org.geysermc.floodgate.mod.data; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.netty.channel.Channel; +import io.netty.util.AttributeKey; +import org.geysermc.floodgate.api.inject.InjectorAddon; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.core.api.SimpleFloodgateApi; +import org.geysermc.floodgate.core.config.FloodgateConfig; +import org.geysermc.floodgate.core.player.FloodgateHandshakeHandler; +import org.geysermc.floodgate.core.util.Utils; + +public final class ModDataAddon implements InjectorAddon { + @Inject private FloodgateHandshakeHandler handshakeHandler; + @Inject private FloodgateConfig config; + @Inject private SimpleFloodgateApi api; + @Inject private FloodgateLogger logger; + + @Inject + @Named("packetHandler") + private String packetHandlerName; + + @Inject + @Named("kickMessageAttribute") + private AttributeKey kickMessageAttribute; + + @Inject + @Named("playerAttribute") + private AttributeKey playerAttribute; + + @Override + public void onInject(Channel channel, boolean toServer) { + channel.pipeline().addBefore( + packetHandlerName, "floodgate_data_handler", + new ModDataHandler(handshakeHandler, config, kickMessageAttribute, logger) + ); + } + + @Override + public void onChannelClosed(Channel channel) { + FloodgatePlayer player = channel.attr(playerAttribute).get(); + if (player != null && api.setPendingRemove(player)) { + logger.translatedInfo("floodgate.ingame.disconnect_name", player.getCorrectUsername()); + } + } + + @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 00000000..eded69cc --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/data/ModDataHandler.java @@ -0,0 +1,157 @@ +package org.geysermc.floodgate.mod.data; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import com.mojang.logging.LogUtils; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.AttributeKey; +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.network.ServerLoginPacketListenerImpl; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.core.addon.data.CommonDataHandler; +import org.geysermc.floodgate.core.addon.data.PacketBlocker; +import org.geysermc.floodgate.core.config.FloodgateConfig; +import org.geysermc.floodgate.core.player.FloodgateHandshakeHandler; +import org.geysermc.floodgate.core.player.FloodgateHandshakeHandler.HandshakeResult; +import org.geysermc.floodgate.mod.MinecraftServerHolder; +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 CommonDataHandler { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final FloodgateLogger logger; + private Connection networkManager; + private FloodgatePlayer player; + + public ModDataHandler( + FloodgateHandshakeHandler handshakeHandler, + FloodgateConfig config, + AttributeKey kickMessageAttribute, FloodgateLogger logger) { + super(handshakeHandler, config, kickMessageAttribute, new PacketBlocker()); + this.logger = logger; + } + + @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(HandshakeResult result) { + player = result.getFloodgatePlayer(); + + if (getKickMessage() != null) { + // we also have to keep this handler if we want to kick then with a disconnect message + return false; + } else if (player == null) { + // player is not a Floodgate player + return true; + } + + if (result.getResultType() == FloodgateHandshakeHandler.ResultType.SUCCESS) { + logger.info("Floodgate player who is logged in as {} {} joined", + player.getCorrectUsername(), player.getCorrectUniqueId()); + } + + // 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) { + String kickMessage = getKickMessage(); + if (kickMessage != null) { + Component message = Component.nullToEmpty(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(player.getCorrectUniqueId(), player.getCorrectUsername()); + + if (player.isLinked() && player.getCorrectUniqueId().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 = MinecraftServerHolder.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.isDebug()) { + 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 00000000..dcd2d232 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/inject/ModInjector.java @@ -0,0 +1,64 @@ +package org.geysermc.floodgate.mod.inject; + +import com.google.inject.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.api.logger.FloodgateLogger; +import org.geysermc.floodgate.core.inject.CommonPlatformInjector; + +@RequiredArgsConstructor +public final class ModInjector extends CommonPlatformInjector { + + public static ModInjector INSTANCE = new ModInjector(); + + @Getter private final boolean injected = true; + + @Inject private FloodgateLogger logger; + + @Override + public void inject() throws Exception { + //no-op + } + + public void injectClient(ChannelFuture future) { + if (future.channel().pipeline().names().contains("floodgate-init")) { + logger.debug("Tried to inject twice!"); + 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() throws Exception { + //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 00000000..6f116e3d --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/listener/ModEventListener.java @@ -0,0 +1,26 @@ +package org.geysermc.floodgate.mod.listener; + +import com.google.inject.Inject; +import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.core.util.LanguageManager; + +import java.util.UUID; + +public final class ModEventListener { + @Inject private FloodgateApi api; + @Inject private FloodgateLogger logger; + @Inject private LanguageManager languageManager; + + public void onPlayerJoin(UUID uuid) { + FloodgatePlayer player = api.getPlayer(uuid); + if (player != null) { + logger.translatedInfo( + "floodgate.ingame.login_name", + player.getCorrectUsername(), player.getCorrectUniqueId() + ); + languageManager.loadLocale(player.getLanguageCode()); + } + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/logger/Log4jFloodgateLogger.java b/mod/src/main/java/org/geysermc/floodgate/mod/logger/Log4jFloodgateLogger.java new file mode 100644 index 00000000..b3eb4814 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/logger/Log4jFloodgateLogger.java @@ -0,0 +1,69 @@ +package org.geysermc.floodgate.mod.logger; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.config.Configurator; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.core.config.FloodgateConfig; +import org.geysermc.floodgate.core.util.LanguageManager; + +import static org.geysermc.floodgate.core.util.MessageFormatter.format; + +@Singleton +public final class Log4jFloodgateLogger implements FloodgateLogger { + @Inject + @Named("logger") + private Logger logger; + private LanguageManager languageManager; + + @Inject + private void init(LanguageManager languageManager, FloodgateConfig config) { + this.languageManager = languageManager; + if (config.isDebug() && !logger.isDebugEnabled()) { + Configurator.setLevel(logger.getName(), Level.DEBUG); + } + } + + @Override + public void error(String message, Object... args) { + logger.error(message, args); + } + + @Override + public void error(String message, Throwable throwable, Object... args) { + logger.error(format(message, args), throwable); + } + + @Override + public void warn(String message, Object... args) { + logger.warn(message, args); + } + + @Override + public void info(String message, Object... args) { + logger.info(message, args); + } + + @Override + public void translatedInfo(String message, Object... args) { + logger.info(languageManager.getLogString(message, args)); + } + + @Override + public void debug(String message, Object... args) { + logger.debug(message, args); + } + + @Override + public void trace(String message, Object... args) { + logger.trace(message, args); + } + + @Override + public boolean isDebug() { + return logger.isDebugEnabled(); + } +} 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 00000000..c959ebfc --- /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 00000000..75b5df4d --- /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 00000000..ee067929 --- /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 00000000..e085cd3a --- /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 00000000..d2511825 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/FloodgateUtilMixin.java @@ -0,0 +1,49 @@ +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. + */ +@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 00000000..39ab73d7 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/mixin/GeyserModInjectorMixin.java @@ -0,0 +1,25 @@ +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; + +@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 00000000..e094da2b --- /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 00000000..fdd9f577 --- /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 00000000..98d4532e --- /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 00000000..534849a1 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/module/ModPlatformModule.java @@ -0,0 +1,77 @@ +package org.geysermc.floodgate.mod.module; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import lombok.RequiredArgsConstructor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.core.inject.CommonPlatformInjector; +import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.geysermc.floodgate.core.platform.util.PlatformUtils; +import org.geysermc.floodgate.core.skin.SkinApplier; +import org.geysermc.floodgate.core.util.LanguageManager; +import org.geysermc.floodgate.mod.FloodgateMod; +import org.geysermc.floodgate.mod.inject.ModInjector; +import org.geysermc.floodgate.mod.logger.Log4jFloodgateLogger; +import org.geysermc.floodgate.mod.pluginmessage.ModSkinApplier; +import org.geysermc.floodgate.mod.util.ModCommandUtil; +import org.geysermc.floodgate.mod.util.ModPlatformUtils; + +@RequiredArgsConstructor +public abstract class ModPlatformModule extends AbstractModule { + + @Provides + @Singleton + public CommandUtil commandUtil( + FloodgateApi api, + FloodgateLogger logger, + LanguageManager languageManager) { + return new ModCommandUtil(languageManager, api, logger); + } + + @Override + protected void configure() { + bind(PlatformUtils.class).to(ModPlatformUtils.class); + bind(Logger.class).annotatedWith(Names.named("logger")).toInstance(LogManager.getLogger("floodgate")); + bind(FloodgateLogger.class).to(Log4jFloodgateLogger.class); + } + + /* + DebugAddon / PlatformInjector + */ + + @Provides + @Singleton + public CommonPlatformInjector platformInjector() { + return ModInjector.INSTANCE; + } + + @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" ; + } + + @Provides + @Named("packetHandler") + public String packetHandler() { + return "packet_handler"; + } + + @Provides + @Singleton + public SkinApplier skinApplier() { + return new ModSkinApplier(); + } +} 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 00000000..45371729 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/pluginmessage/ModSkinApplier.java @@ -0,0 +1,60 @@ +package org.geysermc.floodgate.mod.pluginmessage; + +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.core.skin.SkinApplier; +import org.geysermc.floodgate.mod.MinecraftServerHolder; +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 { + + @Override + public void applySkin(@NonNull FloodgatePlayer floodgatePlayer, @NonNull SkinData skinData) { + MinecraftServerHolder.get().execute(() -> { + ServerPlayer bedrockPlayer = MinecraftServerHolder.get().getPlayerList() + .getPlayer(floodgatePlayer.getCorrectUniqueId()); + if (bedrockPlayer == null) { + // 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 = ((ServerLevel) 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 : MinecraftServerHolder.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 00000000..fa7b727b --- /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 00000000..944ea862 --- /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 00000000..16fda96b --- /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 00000000..597634e0 --- /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 00000000..c5685d29 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModCommandUtil.java @@ -0,0 +1,113 @@ +package org.geysermc.floodgate.mod.util; + +import com.mojang.authlib.GameProfile; +import lombok.Setter; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.UserWhiteListEntry; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.core.platform.command.CommandUtil; +import org.geysermc.floodgate.core.player.UserAudience; +import org.geysermc.floodgate.core.util.LanguageManager; +import org.geysermc.floodgate.mod.MinecraftServerHolder; +import org.incendo.cloud.CommandManager; + +import java.util.Collection; +import java.util.UUID; + +public final class ModCommandUtil extends CommandUtil { + private final FloodgateLogger logger; + private UserAudience console; + @Setter + private CommandManager commandManager; + + public ModCommandUtil(LanguageManager manager, FloodgateApi api, FloodgateLogger logger) { + super(manager, api); + this.logger = logger; + } + + @Override + public @NonNull UserAudience getUserAudience(final @NonNull Object sourceObj) { + if (!(sourceObj instanceof CommandSourceStack stack)) { + throw new IllegalArgumentException(); + } + 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 = MinecraftServerHolder.get().getPlayerList().getPlayer(uuid); + return player != null ? player : uuid; + } + + @Override + public Object getPlayerByUsername(@NonNull String username) { + ServerPlayer player = MinecraftServerHolder.get().getPlayerList().getPlayerByName(username); + return player != null ? player : username; + } + + @Override + protected Collection getOnlinePlayers() { + return MinecraftServerHolder.get().getPlayerList().getPlayers(); + } + + @Override + public boolean hasPermission(Object source, String permission) { + return commandManager.hasPermission(getUserAudience(source), permission); + } + + @Override + public void sendMessage(Object target, String message) { + CommandSourceStack commandSource = (CommandSourceStack) target; + if (commandSource.getEntity() instanceof ServerPlayer) { + MinecraftServerHolder.get().execute(() -> ((ServerPlayer) commandSource.getEntity()) + .displayClientMessage(Component.literal(message), false)); + } else { + // Console? + logger.info(message); + } + } + + @Override + public void kickPlayer(Object o, String message) { + if (o instanceof ServerPlayer player) { + player.connection.disconnect(Component.literal(message)); + } + } + + @Override + public boolean whitelistPlayer(UUID uuid, String username) { + GameProfile profile = new GameProfile(uuid, username); + MinecraftServerHolder.get().getPlayerList().getWhiteList().add(new UserWhiteListEntry(profile)); + return true; + } + + @Override + public boolean removePlayerFromWhitelist(UUID uuid, String username) { + GameProfile profile = new GameProfile(uuid, username); + MinecraftServerHolder.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 00000000..47945b7f --- /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 00000000..56135eab --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModPlatformUtils.java @@ -0,0 +1,22 @@ +package org.geysermc.floodgate.mod.util; + +import net.minecraft.SharedConstants; +import org.geysermc.floodgate.core.platform.util.PlatformUtils; +import org.geysermc.floodgate.mod.MinecraftServerHolder; + +public class ModPlatformUtils extends PlatformUtils { + @Override + public AuthType authType() { + return MinecraftServerHolder.get().usesAuthentication() ? AuthType.ONLINE : AuthType.OFFLINE; + } + + @Override + public String minecraftVersion() { + return SharedConstants.getCurrentVersion().name(); + } + + @Override + public String serverImplementationName() { + return MinecraftServerHolder.get().getServerModName(); + } +} diff --git a/mod/src/main/java/org/geysermc/floodgate/mod/util/ModTemplateReader.java b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModTemplateReader.java new file mode 100644 index 00000000..9f0d4df1 --- /dev/null +++ b/mod/src/main/java/org/geysermc/floodgate/mod/util/ModTemplateReader.java @@ -0,0 +1,25 @@ +package org.geysermc.floodgate.mod.util; + +import org.geysermc.configutils.file.template.TemplateReader; +import org.geysermc.floodgate.mod.FloodgateMod; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ModTemplateReader implements TemplateReader { + + @Override + public BufferedReader read(String configName) { + Path path = FloodgateMod.INSTANCE.resourcePath(configName); + if (path != null) { + try { + return Files.newBufferedReader(path); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + return null; + } +} diff --git a/mod/src/main/resources/floodgate.accesswidener b/mod/src/main/resources/floodgate.accesswidener new file mode 100644 index 00000000..f3860e9f --- /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 00000000..d3597fb1 --- /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 00000000..7d85275b --- /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/build.gradle.kts b/neoforge/build.gradle.kts new file mode 100644 index 00000000..6f18ab50 --- /dev/null +++ b/neoforge/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/gradle.properties b/neoforge/gradle.properties new file mode 100644 index 00000000..2914393d --- /dev/null +++ b/neoforge/gradle.properties @@ -0,0 +1 @@ +loom.platform=neoforge \ No newline at end of file diff --git a/neoforge/src/main/java/org/geysermc/floodgate/mod/util/neoforge/ModMixinConfigPluginImpl.java b/neoforge/src/main/java/org/geysermc/floodgate/mod/util/neoforge/ModMixinConfigPluginImpl.java new file mode 100644 index 00000000..27159f6f --- /dev/null +++ b/neoforge/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/src/main/java/org/geysermc/floodgate/platform/neoforge/NeoForgeFloodgateMod.java b/neoforge/src/main/java/org/geysermc/floodgate/platform/neoforge/NeoForgeFloodgateMod.java new file mode 100644 index 00000000..9268bbd5 --- /dev/null +++ b/neoforge/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/src/main/java/org/geysermc/floodgate/platform/neoforge/listener/NeoForgeEventRegistration.java b/neoforge/src/main/java/org/geysermc/floodgate/platform/neoforge/listener/NeoForgeEventRegistration.java new file mode 100644 index 00000000..77afc914 --- /dev/null +++ b/neoforge/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/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgeCommandModule.java b/neoforge/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgeCommandModule.java new file mode 100644 index 00000000..5b06fe88 --- /dev/null +++ b/neoforge/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/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgePlatformModule.java b/neoforge/src/main/java/org/geysermc/floodgate/platform/neoforge/module/NeoForgePlatformModule.java new file mode 100644 index 00000000..f224ad0f --- /dev/null +++ b/neoforge/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/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageRegistration.java b/neoforge/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageRegistration.java new file mode 100644 index 00000000..bd73bb3f --- /dev/null +++ b/neoforge/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/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageUtils.java b/neoforge/src/main/java/org/geysermc/floodgate/platform/neoforge/pluginmessage/NeoForgePluginMessageUtils.java new file mode 100644 index 00000000..5faf4d5b --- /dev/null +++ b/neoforge/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/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..7d7373f5 --- /dev/null +++ b/neoforge/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/settings.gradle.kts b/settings.gradle.kts index 802bb272..726f2e23 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,6 +44,16 @@ dependencyResolutionManagement { 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 +66,7 @@ rootProject.name = "floodgate-parent" include(":api") include(":universal") include(":isolation") +include(":mod") arrayOf("common", "netty4").forEach { val id = ":core-$it" @@ -63,7 +74,7 @@ arrayOf("common", "netty4").forEach { project(id).projectDir = file("core/$it") } -arrayOf("bungee", "spigot", "velocity").forEach { platform -> +arrayOf("bungee", "spigot", "velocity", "fabric", "neoforge").forEach { platform -> arrayOf("base", "isolated").forEach { var id = ":$platform-$it" // isolated is the new default