diff --git a/.github/workflows/call-gradle-wrapper-validation.yml b/.github/workflows/call-gradle-wrapper-validation.yml index 29cb46a..b0e496d 100644 --- a/.github/workflows/call-gradle-wrapper-validation.yml +++ b/.github/workflows/call-gradle-wrapper-validation.yml @@ -9,4 +9,4 @@ jobs: steps: - uses: actions/checkout@v6 - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v3 \ No newline at end of file + uses: gradle/actions/wrapper-validation@v6 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 064d195..c5c8df1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,11 @@ plugins { alias(libs.plugins.gradle.neoforgegradle) apply false alias(libs.plugins.klibs.gradle.detekt) apply false - alias(libs.plugins.klibs.gradle.dokka.module) + alias(libs.plugins.klibs.gradle.dokka.root) alias(libs.plugins.klibs.gradle.java.version) apply false alias(libs.plugins.klibs.gradle.publication) apply false alias(libs.plugins.klibs.gradle.rootinfo) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.gradle.forgegradle) apply false } diff --git a/core-forge/build.gradle.kts b/core-forge/build.gradle.kts new file mode 100644 index 0000000..c31f40e --- /dev/null +++ b/core-forge/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("net.minecraftforge.gradle") + id("org.jetbrains.kotlin.jvm") + id("org.jetbrains.kotlin.plugin.serialization") + id("ru.astrainteractive.gradleplugin.detekt") + id("ru.astrainteractive.gradleplugin.java.version") + id("ru.astrainteractive.gradleplugin.publication") + id("ru.astrainteractive.gradleplugin.rootinfo") +} + +repositories { + minecraft.mavenizer(this) + mavenCentral() + mavenLocal() + maven(fg.forgeMaven) + maven(fg.minecraftLibsMaven) +} + +dependencies { + compileOnly(libs.klibs.mikro.core) + compileOnly(libs.kotlin.coroutines.core) + compileOnly(libs.minecraft.kyori.api) + compileOnly(libs.minecraft.kyori.gson) + compileOnly(libs.minecraft.kyori.legacy) + compileOnly(libs.minecraft.kyori.minimessage) + compileOnly(libs.minecraft.kyori.plain) + compileOnly(libs.minecraft.luckperms) + + implementation(projects.command) + implementation(projects.core) + + testImplementation(libs.kotlin.serialization.kaml) + testImplementation(libs.tests.kotlin.test) +} + +dependencies { + minecraft { + implementation( + dependency( + "net.minecraftforge:forge:" + + libs.versions.minecraft.minecraftforge.minecraft.get() + + "-" + + libs.versions.minecraft.minecraftforge.forge.get() + ) + ) + mappings("official", libs.versions.minecraft.minecraftforge.minecraft.get()) + } +} + +configurations.runtimeElements { + setExtendsFrom(emptySet()) +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/command/ForgeCommandExt.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/command/ForgeCommandExt.kt new file mode 100644 index 0000000..8008c9a --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/command/ForgeCommandExt.kt @@ -0,0 +1,160 @@ +@file:Suppress("TooManyFunctions") + +package ru.astrainteractive.astralibs.command.brigadier.command + +import com.mojang.brigadier.Command +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import net.minecraft.commands.CommandSourceStack +import net.minecraft.commands.Commands +import net.minecraft.server.MinecraftServer +import net.minecraft.server.dedicated.DedicatedServer +import net.minecraft.server.level.ServerPlayer +import net.minecraft.server.rcon.RconConsoleSource +import ru.astrainteractive.astralibs.command.api.argumenttype.ArgumentConverter +import ru.astrainteractive.astralibs.command.api.exception.ArgumentConverterException +import ru.astrainteractive.astralibs.command.api.exception.CommandException +import ru.astrainteractive.astralibs.command.api.exception.NoPermissionException +import ru.astrainteractive.astralibs.command.api.exception.NotPlayerExecutorException +import ru.astrainteractive.astralibs.server.permission.Permission +import ru.astrainteractive.astralibs.server.util.asPermissible + +fun command( + alias: String, + block: LiteralArgumentBuilder.() -> Unit +): LiteralArgumentBuilder { + val literal = Commands.literal(alias) + literal.block() + return literal +} + +fun LiteralArgumentBuilder.literal( + alias: String, + block: LiteralArgumentBuilder.() -> Unit +) { + val literal = Commands.literal(alias) + literal.block() + this.then(literal) +} + +data class BrigadierArgument( + val alias: String, + val type: ArgumentType, + val clazz: Class +) + +inline fun LiteralArgumentBuilder.argument( + alias: String, + type: ArgumentType, + noinline block: RequiredArgumentBuilder.(BrigadierArgument) -> Unit +) = argument( + alias = alias, + type = type, + clazz = T::class.java, + block = block +) + +fun LiteralArgumentBuilder.argument( + alias: String, + type: ArgumentType, + clazz: Class, + block: RequiredArgumentBuilder.(BrigadierArgument) -> Unit +) { + val argument = Commands.argument(alias, type) + val brigadierArgument = BrigadierArgument( + alias = alias, + type = type, + clazz = clazz + ) + argument.block(brigadierArgument) + this.then(argument) +} + +inline fun RequiredArgumentBuilder.argument( + alias: String, + type: ArgumentType, + noinline block: RequiredArgumentBuilder.(BrigadierArgument) -> Unit +) = argument( + alias = alias, + type = type, + clazz = T::class.java, + block = block +) + +fun RequiredArgumentBuilder.argument( + alias: String, + type: ArgumentType, + clazz: Class, + block: RequiredArgumentBuilder.(BrigadierArgument) -> Unit +) { + val argument = Commands.argument(alias, type) + val brigadierArgument = BrigadierArgument( + alias = alias, + type = type, + clazz = clazz + ) + argument.block(brigadierArgument) + this.then(argument) +} + +fun RequiredArgumentBuilder.runs( + onFailure: (CommandContext, Throwable) -> Unit = { _, _ -> }, + block: (RequiredArgumentBuilder.(CommandContext) -> Unit) +) { + executes { ctx -> + runCatching { block.invoke(this, ctx) } + .onFailure { onFailure.invoke(ctx, it) } + Command.SINGLE_SUCCESS + } +} + +fun LiteralArgumentBuilder.runs( + onFailure: (CommandContext, Throwable) -> Unit = { _, _ -> }, + block: LiteralArgumentBuilder.(CommandContext) -> Unit +) { + executes { ctx -> + runCatching { block.invoke(this, ctx) } + .onFailure { onFailure.invoke(ctx, it) } + Command.SINGLE_SUCCESS + } +} + +fun RequiredArgumentBuilder.hints(block: (CommandContext) -> List) { + suggests { context, builder -> + block.invoke(context).forEach(builder::suggest) + builder.buildFuture() + } +} + +@Throws(NoPermissionException::class) +fun CommandContext.requirePermission(permission: Permission) { + if (source.source is RconConsoleSource) return + if (source.source is DedicatedServer) return + if (source.source is MinecraftServer) return + val serverPlayer = source.entity as? ServerPlayer ?: run { + throw CommandException("$source.source; is not a player!") + } + val hasPermission = serverPlayer.asPermissible().hasPermission(permission) + if (!hasPermission) throw NoPermissionException(permission) +} + +@Throws(NotPlayerExecutorException::class) +fun CommandContext.requirePlayer(): ServerPlayer { + return this.source.player ?: throw NotPlayerExecutorException() +} + +@Throws(IllegalArgumentException::class) +fun CommandContext.requireArgument(bArgument: BrigadierArgument): T { + return getArgument(bArgument.alias, bArgument.clazz) +} + +@Throws(ArgumentConverterException::class) +fun CommandContext.requireArgument( + bArgument: BrigadierArgument, + converter: ArgumentConverter +): T { + val string = getArgument(bArgument.alias, bArgument.clazz) + return converter.transform(string) +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/command/ForgeMultiplatformCommands.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/command/ForgeMultiplatformCommands.kt new file mode 100644 index 0000000..b178722 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/command/ForgeMultiplatformCommands.kt @@ -0,0 +1,53 @@ +package ru.astrainteractive.astralibs.command.brigadier.command + +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import net.minecraft.commands.CommandSourceStack +import net.minecraft.commands.Commands +import net.minecraft.server.MinecraftServer +import net.minecraft.server.dedicated.DedicatedServer +import net.minecraft.server.level.ServerPlayer +import net.minecraft.server.rcon.RconConsoleSource +import ru.astrainteractive.astralibs.command.api.brigadier.command.MultiplatformCommands +import ru.astrainteractive.astralibs.command.api.brigadier.sender.KCommandSender +import ru.astrainteractive.astralibs.command.api.brigadier.sender.KPlayerKCommandSender +import ru.astrainteractive.astralibs.command.brigadier.sender.ForgeRconKCommandSender +import ru.astrainteractive.astralibs.command.brigadier.sender.ForgeServerKCommandSender +import ru.astrainteractive.astralibs.server.util.asOnlineMinecraftPlayer + +@Suppress("UNCHECKED_CAST") +class ForgeMultiplatformCommands : MultiplatformCommands { + override fun literal(literal: String): LiteralArgumentBuilder { + return Commands.literal(literal) as LiteralArgumentBuilder + } + + override fun argument( + name: String, + argumentType: ArgumentType + ): RequiredArgumentBuilder { + return Commands.argument(name, argumentType) as RequiredArgumentBuilder + } + + override fun getSender(context: CommandContext<*>): KCommandSender { + val commandSourceStack = context.source as CommandSourceStack + + return when (val source = commandSourceStack.source) { + is ServerPlayer -> { + KPlayerKCommandSender(source.asOnlineMinecraftPlayer()) + } + + is RconConsoleSource -> { + ForgeRconKCommandSender(source) + } + + is DedicatedServer, + is MinecraftServer -> { + ForgeServerKCommandSender(source) + } + + else -> error("Could not wrap sender ${commandSourceStack.source.javaClass}") + } + } +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/sender/ForgeRconKCommandSender.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/sender/ForgeRconKCommandSender.kt new file mode 100644 index 0000000..36cd229 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/sender/ForgeRconKCommandSender.kt @@ -0,0 +1,17 @@ +package ru.astrainteractive.astralibs.command.brigadier.sender + +import net.minecraft.server.rcon.RconConsoleSource +import ru.astrainteractive.astralibs.command.api.brigadier.sender.ConsoleKCommandSender +import ru.astrainteractive.astralibs.server.KAudience +import ru.astrainteractive.astralibs.server.KCommandDispatcher +import ru.astrainteractive.astralibs.server.permission.ConsoleKPermissible +import ru.astrainteractive.astralibs.server.permission.KPermissible +import ru.astrainteractive.astralibs.server.util.asKAudience +import ru.astrainteractive.astralibs.server.util.asKCommandDispatcher + +class ForgeRconKCommandSender( + sender: RconConsoleSource +) : ConsoleKCommandSender, + KPermissible by ConsoleKPermissible, + KAudience by sender.asKAudience(), + KCommandDispatcher by sender.asKCommandDispatcher() diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/sender/ForgeServerKCommandSender.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/sender/ForgeServerKCommandSender.kt new file mode 100644 index 0000000..1866493 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/brigadier/sender/ForgeServerKCommandSender.kt @@ -0,0 +1,17 @@ +package ru.astrainteractive.astralibs.command.brigadier.sender + +import net.minecraft.server.MinecraftServer +import ru.astrainteractive.astralibs.command.api.brigadier.sender.ConsoleKCommandSender +import ru.astrainteractive.astralibs.server.KAudience +import ru.astrainteractive.astralibs.server.KCommandDispatcher +import ru.astrainteractive.astralibs.server.permission.ConsoleKPermissible +import ru.astrainteractive.astralibs.server.permission.KPermissible +import ru.astrainteractive.astralibs.server.util.asKAudience +import ru.astrainteractive.astralibs.server.util.asKCommandDispatcher + +class ForgeServerKCommandSender( + sender: MinecraftServer +) : ConsoleKCommandSender, + KPermissible by ConsoleKPermissible, + KAudience by sender.asKAudience(), + KCommandDispatcher by sender.asKCommandDispatcher() diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/registrar/ForgeCommandRegistrarContext.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/registrar/ForgeCommandRegistrarContext.kt new file mode 100644 index 0000000..21f736b --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/command/registrar/ForgeCommandRegistrarContext.kt @@ -0,0 +1,31 @@ +package ru.astrainteractive.astralibs.command.registrar + +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import net.minecraft.commands.CommandSourceStack +import net.minecraftforge.event.RegisterCommandsEvent +import net.minecraftforge.eventbus.api.EventPriority +import ru.astrainteractive.astralibs.command.api.registrar.CommandRegistrarContext +import ru.astrainteractive.astralibs.event.flowEvent + +class ForgeCommandRegistrarContext( + private val mainScope: CoroutineScope +) : CommandRegistrarContext { + private val registerCommandsEvent = flowEvent(EventPriority.HIGHEST) + .filterNotNull() + .stateIn(mainScope, SharingStarted.Eagerly, null) + + override fun registerWhenReady(node: LiteralArgumentBuilder<*>) { + node as LiteralArgumentBuilder + registerCommandsEvent + .mapNotNull { registerCommandsEvent -> registerCommandsEvent?.dispatcher } + .onEach { commandDispatcher -> commandDispatcher.register(node) } + .launchIn(mainScope) + } +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/coroutines/ForgeDispatchers.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/coroutines/ForgeDispatchers.kt new file mode 100644 index 0000000..c7ca223 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/coroutines/ForgeDispatchers.kt @@ -0,0 +1,18 @@ +package ru.astrainteractive.astralibs.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainCoroutineDispatcher +import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers + +class ForgeDispatchers : KotlinDispatchers { + override val Main: MainCoroutineDispatcher by lazy { + ForgeMainDispatcher() + } + override val IO: CoroutineDispatcher + get() = Dispatchers.IO + override val Default: CoroutineDispatcher + get() = Dispatchers.Default + override val Unconfined: CoroutineDispatcher + get() = Dispatchers.Unconfined +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/coroutines/ForgeMainDispatcher.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/coroutines/ForgeMainDispatcher.kt new file mode 100644 index 0000000..94900c9 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/coroutines/ForgeMainDispatcher.kt @@ -0,0 +1,35 @@ +package ru.astrainteractive.astralibs.coroutines + +import kotlinx.coroutines.MainCoroutineDispatcher +import net.minecraft.client.Minecraft +import net.minecraftforge.api.distmarker.Dist +import net.minecraftforge.fml.loading.FMLEnvironment +import ru.astrainteractive.astralibs.server.util.ForgeUtil +import java.util.concurrent.Executor +import kotlin.coroutines.CoroutineContext + +class ForgeMainDispatcher : MainCoroutineDispatcher() { + override val immediate: MainCoroutineDispatcher + get() = this + private val executor = when (FMLEnvironment.dist) { + Dist.CLIENT -> Minecraft.getInstance() + Dist.DEDICATED_SERVER -> Executor { block -> + ForgeUtil.requireServer().executeBlocking(block) + } + } + + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + return true + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + val coroutineTimings = context[CoroutineTimings.Key] + + if (coroutineTimings == null) { + executor.execute(block) + } else { + coroutineTimings.queue.add(block) + executor.execute(coroutineTimings) + } + } +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/event/ForgeEvent.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/event/ForgeEvent.kt new file mode 100644 index 0000000..e9434b1 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/event/ForgeEvent.kt @@ -0,0 +1,36 @@ +package ru.astrainteractive.astralibs.event + +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.eventbus.api.Event +import net.minecraftforge.eventbus.api.EventPriority +import java.util.function.Consumer + +fun flowEvent( + type: Class, + priority: EventPriority = EventPriority.NORMAL +): Flow = callbackFlow { + val isCancelled = false + val consumer = Consumer { event -> + launch { send(event) } + } + MinecraftForge.EVENT_BUS.addListener( + priority, + isCancelled, + type, + consumer + ) + awaitClose { + MinecraftForge.EVENT_BUS.unregister(consumer) + } +} + +inline fun flowEvent( + priority: EventPriority = EventPriority.NORMAL +): Flow = flowEvent( + type = T::class.java, + priority = priority +) diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/event/PlayerMoveEvent.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/event/PlayerMoveEvent.kt new file mode 100644 index 0000000..a875eb8 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/event/PlayerMoveEvent.kt @@ -0,0 +1,54 @@ +package ru.astrainteractive.astralibs.event + +import com.google.common.cache.CacheBuilder +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach +import net.minecraft.world.entity.player.Player +import net.minecraft.world.level.storage.ServerLevelData +import net.minecraftforge.event.TickEvent +import ru.astrainteractive.astralibs.server.location.KLocation +import ru.astrainteractive.klibs.mikro.core.util.cast +import ru.astrainteractive.klibs.mikro.core.util.tryCast +import java.util.UUID +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +class PlayerMoveEvent( + val instance: TickEvent.PlayerTickEvent, + val oldKLocation: KLocation, + val newKLocation: KLocation, + val player: Player +) + +fun playerMoveFlowEvent() = flow { + val cache = CacheBuilder.newBuilder() + .expireAfterAccess(10.seconds.toJavaDuration()) + .build() + flowEvent() + .filter { it.phase == TickEvent.Phase.END } + .onEach { event -> + val player = event.player.tryCast() ?: return@onEach + val kLocation = KLocation( + x = event.player.x, + y = event.player.y, + z = event.player.z, + worldName = event.player + .level() + .levelData + .cast() + .levelName + ) + val cachedLocation = cache.get(event.player.uuid) { + kLocation + } + val moveEvent = PlayerMoveEvent( + instance = event, + oldKLocation = cachedLocation, + newKLocation = kLocation, + player = player + ) + emit(moveEvent) + }.collect() +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/lifecycle/ForgeLifecycleServer.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/lifecycle/ForgeLifecycleServer.kt new file mode 100644 index 0000000..d3fa77b --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/lifecycle/ForgeLifecycleServer.kt @@ -0,0 +1,38 @@ +package ru.astrainteractive.astralibs.lifecycle + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import net.minecraftforge.event.server.ServerStartedEvent +import net.minecraftforge.event.server.ServerStoppingEvent +import net.minecraftforge.eventbus.api.EventPriority +import ru.astrainteractive.astralibs.event.flowEvent +import ru.astrainteractive.astralibs.server.util.ForgeUtil +import ru.astrainteractive.klibs.mikro.core.logging.Logger + +abstract class ForgeLifecycleServer : Lifecycle, Logger { + private val unconfinedScope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined) + + val serverStartedEvent = flowEvent(EventPriority.HIGHEST) + .onEach { onEnable() } + .catch { throwable -> + error(throwable) { "#serverStartedEvent ${throwable.localizedMessage}" } + } + .launchIn(unconfinedScope) + + val serverStoppingEvent = flowEvent(EventPriority.HIGHEST) + .onEach { onDisable() } + .onEach { unconfinedScope.cancel() } + .catch { throwable -> + error(throwable) { "#serverStartedEvent ${throwable.localizedMessage}" } + } + .launchIn(unconfinedScope) + + init { + ForgeUtil.bootstrap() + } +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/bridge/ForgePlatformServer.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/bridge/ForgePlatformServer.kt new file mode 100644 index 0000000..1ac7a7c --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/bridge/ForgePlatformServer.kt @@ -0,0 +1,35 @@ +package ru.astrainteractive.astralibs.server.bridge + +import ru.astrainteractive.astralibs.server.player.KPlayer +import ru.astrainteractive.astralibs.server.player.OnlineKPlayer +import ru.astrainteractive.astralibs.server.util.ForgeUtil +import ru.astrainteractive.astralibs.server.util.asOfflineMinecraftPlayer +import ru.astrainteractive.astralibs.server.util.asOnlineMinecraftPlayer +import ru.astrainteractive.astralibs.server.util.getOnlinePlayer +import ru.astrainteractive.astralibs.server.util.getOnlinePlayers +import ru.astrainteractive.astralibs.server.util.getPlayerGameProfile +import java.util.UUID + +object ForgePlatformServer : PlatformServer { + override fun getOnlinePlayers(): List { + return ForgeUtil + .getOnlinePlayers() + .map { serverPlayer -> serverPlayer.asOnlineMinecraftPlayer() } + } + + override fun findOnlinePlayer(uuid: UUID): OnlineKPlayer? { + return ForgeUtil.getOnlinePlayer(uuid)?.asOnlineMinecraftPlayer() + } + + override fun findOfflinePlayer(uuid: UUID): KPlayer? { + return ForgeUtil.getPlayerGameProfile(uuid)?.asOfflineMinecraftPlayer() + } + + override fun findOnlinePlayer(name: String): OnlineKPlayer? { + return ForgeUtil.getOnlinePlayer(name)?.asOnlineMinecraftPlayer() + } + + override fun findOfflinePlayer(name: String): KPlayer? { + return ForgeUtil.getPlayerGameProfile(name)?.asOfflineMinecraftPlayer() + } +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/player/ForgeKPlayer.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/player/ForgeKPlayer.kt new file mode 100644 index 0000000..710457a --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/player/ForgeKPlayer.kt @@ -0,0 +1,23 @@ +package ru.astrainteractive.astralibs.server.player + +import com.mojang.authlib.GameProfile +import net.minecraft.world.level.storage.LevelResource +import net.minecraftforge.server.ServerLifecycleHooks +import ru.astrainteractive.astralibs.server.annotation.InternalPlatformApi +import java.util.UUID + +@OptIn(InternalPlatformApi::class) +class ForgeKPlayer(val instance: GameProfile) : KPlayer { + override val uuid: UUID + get() = instance.id + + override val name: String? + get() = instance.name + + override fun hasPlayedBefore(): Boolean { + val playerDataDir = ServerLifecycleHooks.getCurrentServer() + ?.getWorldPath(LevelResource.PLAYER_DATA_DIR) + ?.toFile() + return playerDataDir?.resolve("$uuid.dat")?.exists() == true + } +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/player/ForgeOnlineKPlayer.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/player/ForgeOnlineKPlayer.kt new file mode 100644 index 0000000..52079e6 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/player/ForgeOnlineKPlayer.kt @@ -0,0 +1,43 @@ +package ru.astrainteractive.astralibs.server.player + +import net.minecraft.server.level.ServerPlayer +import ru.astrainteractive.astralibs.server.KAudience +import ru.astrainteractive.astralibs.server.KCommandDispatcher +import ru.astrainteractive.astralibs.server.Locatable +import ru.astrainteractive.astralibs.server.Teleportable +import ru.astrainteractive.astralibs.server.annotation.InternalPlatformApi +import ru.astrainteractive.astralibs.server.permission.KPermissible +import ru.astrainteractive.astralibs.server.util.asKAudience +import ru.astrainteractive.astralibs.server.util.asKCommandDispatcher +import ru.astrainteractive.astralibs.server.util.asLocatable +import ru.astrainteractive.astralibs.server.util.asPermissible +import ru.astrainteractive.astralibs.server.util.asTeleportable +import ru.astrainteractive.astralibs.server.util.toPlain +import ru.astrainteractive.klibs.mikro.core.util.cast +import java.net.InetSocketAddress +import java.util.* + +@OptIn(InternalPlatformApi::class) +class ForgeOnlineKPlayer(val instance: ServerPlayer) : + OnlineKPlayer, + KAudience by instance.asKAudience(), + Locatable by instance.asLocatable(), + Teleportable by instance.asTeleportable(), + KPermissible by instance.asPermissible(), + KCommandDispatcher by instance.asKCommandDispatcher() { + override val uuid: UUID + get() = instance.uuid + override val name: String + get() = instance.name.toPlain() + + override fun hasPlayedBefore(): Boolean { + return instance.server.playerList + .load(instance) + ?.isEmpty != true + } + + override val address: InetSocketAddress + get() = instance.connection + .remoteAddress + .cast() +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/AudienceExt.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/AudienceExt.kt new file mode 100644 index 0000000..f1d523d --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/AudienceExt.kt @@ -0,0 +1,23 @@ +package ru.astrainteractive.astralibs.server.util + +import net.minecraft.commands.CommandSourceStack +import net.minecraft.server.MinecraftServer +import net.minecraft.server.rcon.RconConsoleSource +import net.minecraft.world.entity.player.Player +import ru.astrainteractive.astralibs.server.KAudience + +fun Player.asKAudience() = KAudience { component -> + this.sendSystemMessage(component.toNative()) +} + +fun CommandSourceStack.asKAudience() = KAudience { component -> + this.sendSystemMessage(component.toNative()) +} + +fun MinecraftServer.asKAudience() = KAudience { component -> + sendSystemMessage(component.toNative()) +} + +fun RconConsoleSource.asKAudience() = KAudience { component -> + sendSystemMessage(component.toNative()) +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/CommandDispatcherExt.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/CommandDispatcherExt.kt new file mode 100644 index 0000000..1cd750b --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/CommandDispatcherExt.kt @@ -0,0 +1,31 @@ +package ru.astrainteractive.astralibs.server.util + +import net.minecraft.server.MinecraftServer +import net.minecraft.server.level.ServerPlayer +import net.minecraft.server.rcon.RconConsoleSource +import net.minecraftforge.server.ServerLifecycleHooks +import ru.astrainteractive.astralibs.command.brigadier.command.command +import ru.astrainteractive.astralibs.server.KCommandDispatcher + +fun MinecraftServer.asKCommandDispatcher() = KCommandDispatcher { command -> + this.commands.performPrefixedCommand( + this.createCommandSourceStack(), + command + ) +} + +fun RconConsoleSource.asKCommandDispatcher() = KCommandDispatcher { command -> + ServerLifecycleHooks.getCurrentServer() + ?.commands + ?.performPrefixedCommand( + this.createCommandSourceStack(), + command + ) +} + +fun ServerPlayer.asKCommandDispatcher() = KCommandDispatcher { command -> + this.server.commands.performPrefixedCommand( + this.createCommandSourceStack(), + command + ) +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/ForgeUtil.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/ForgeUtil.kt new file mode 100644 index 0000000..65653b0 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/ForgeUtil.kt @@ -0,0 +1,88 @@ +package ru.astrainteractive.astralibs.server.util + +import com.mojang.authlib.GameProfile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import net.minecraft.server.MinecraftServer +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.level.Level +import net.minecraftforge.event.server.ServerStartedEvent +import net.minecraftforge.fml.loading.FMLLoader +import net.minecraftforge.server.ServerLifecycleHooks +import ru.astrainteractive.astralibs.event.flowEvent +import ru.astrainteractive.klibs.mikro.core.logging.JUtiltLogger +import ru.astrainteractive.klibs.mikro.core.logging.Logger +import java.util.UUID + +/** + * Don't forget to instantiate [ForgeUtil] on init at loader + */ +object ForgeUtil : Logger by JUtiltLogger("AstraLibs-ForgeUtil") { + private val serverFlow = flowEvent() + .map { event -> event.server } + .flowOn(Dispatchers.Unconfined) + .stateIn(GlobalScope, SharingStarted.Eagerly, null) + + val serverOrNull: MinecraftServer? + get() = ServerLifecycleHooks.getCurrentServer() + + suspend fun awaitServer(): MinecraftServer { + return serverFlow.filterNotNull().first() + } + + fun requireServer(): MinecraftServer { + return ServerLifecycleHooks + .getCurrentServer() + ?: error("Server is not available") + } + + fun bootstrap() = Unit +} + +fun ForgeUtil.isModLoaded(modId: String): Boolean { + return FMLLoader.getLoadingModList().getModFileById(modId) != null +} + +fun ForgeUtil.getDefaultWorldName(): String? { + return serverOrNull + ?.getLevel(Level.OVERWORLD) + ?.level + ?.dimension() + ?.location() + ?.path +} + +fun ForgeUtil.getPlayerGameProfile(uuid: UUID): GameProfile? { + return serverOrNull?.profileCache?.get(uuid)?.orElse(null) +} + +fun ForgeUtil.getPlayerGameProfile(name: String): GameProfile? { + return serverOrNull?.profileCache?.get(name)?.orElse(null) +} + +fun ForgeUtil.getOnlinePlayers(): List { + return serverOrNull + ?.playerList + ?.players + ?.toList() + .orEmpty() + .filterNotNull() +} + +fun ForgeUtil.getOnlinePlayer(uuid: UUID): ServerPlayer? { + return serverOrNull?.playerList?.getPlayer(uuid) +} + +fun ForgeUtil.getOnlinePlayer(name: String): ServerPlayer? { + return serverOrNull?.playerList?.getPlayerByName(name) +} + +fun ForgeUtil.getNextTickTime(): Double { + return serverOrNull?.nextTickTime?.toDouble() ?: 0.0 +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/LocatableExt.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/LocatableExt.kt new file mode 100644 index 0000000..242ce45 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/LocatableExt.kt @@ -0,0 +1,15 @@ +package ru.astrainteractive.astralibs.server.util + +import net.minecraft.world.entity.Entity +import net.minecraft.world.level.storage.ServerLevelData +import ru.astrainteractive.astralibs.server.Locatable +import ru.astrainteractive.astralibs.server.location.KLocation + +fun Entity.asLocatable() = Locatable { + KLocation( + x = this.x, + y = this.y, + z = this.z, + worldName = (level().levelData as ServerLevelData).levelName + ) +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/NativeComponentExt.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/NativeComponentExt.kt new file mode 100644 index 0000000..61b9723 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/NativeComponentExt.kt @@ -0,0 +1,25 @@ +package ru.astrainteractive.astralibs.server.util + +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.Component.Serializer +import ru.astrainteractive.astralibs.kyori.KyoriComponentSerializer + +fun net.kyori.adventure.text.Component.toNative(): Component { + val json = KyoriComponentSerializer.Json + val jsonComponent = json.serializer.serialize(this) + + return Serializer.fromJson(jsonComponent) ?: Component.empty() +} + +fun Component.toKyori(): net.kyori.adventure.text.Component { + val json = Serializer.toJson(this) + return KyoriComponentSerializer.Json.serializer.deserialize(json) +} + +fun net.kyori.adventure.text.Component.toPlain(): String { + return KyoriComponentSerializer.Plain.serializer.serialize(this) +} + +fun Component.toPlain(): String { + return toKyori().toPlain() +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/PermissibleExt.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/PermissibleExt.kt new file mode 100644 index 0000000..a0cc4de --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/PermissibleExt.kt @@ -0,0 +1,13 @@ +package ru.astrainteractive.astralibs.server.util + +import net.minecraft.world.entity.player.Player +import ru.astrainteractive.astralibs.server.permission.KPermissible +import ru.astrainteractive.astralibs.server.permission.LuckPermsKPermissible + +fun Player.asPermissible(): KPermissible { + return if (ForgeUtil.isModLoaded("luckperms")) { + LuckPermsKPermissible(this.uuid) + } else { + error("No permission provider loaded!") + } +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/PlayerExt.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/PlayerExt.kt new file mode 100644 index 0000000..114aee0 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/PlayerExt.kt @@ -0,0 +1,16 @@ +package ru.astrainteractive.astralibs.server.util + +import com.mojang.authlib.GameProfile +import net.minecraft.server.level.ServerPlayer +import ru.astrainteractive.astralibs.server.player.ForgeKPlayer +import ru.astrainteractive.astralibs.server.player.ForgeOnlineKPlayer +import ru.astrainteractive.astralibs.server.player.KPlayer +import ru.astrainteractive.astralibs.server.player.OnlineKPlayer + +fun ServerPlayer.asOnlineMinecraftPlayer(): OnlineKPlayer { + return ForgeOnlineKPlayer(this) +} + +fun GameProfile.asOfflineMinecraftPlayer(): KPlayer { + return ForgeKPlayer(this) +} diff --git a/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/TeleportableExt.kt b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/TeleportableExt.kt new file mode 100644 index 0000000..47dd3f9 --- /dev/null +++ b/core-forge/src/main/kotlin/ru/astrainteractive/astralibs/server/util/TeleportableExt.kt @@ -0,0 +1,22 @@ +package ru.astrainteractive.astralibs.server.util + +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.level.storage.ServerLevelData +import ru.astrainteractive.astralibs.server.Teleportable + +fun ServerPlayer.asTeleportable() = Teleportable { location -> + val player = this + + val level = player.server + ?.allLevels + ?.firstOrNull { (it.level.levelData as ServerLevelData).levelName == location.worldName } + ?: return@Teleportable + player.teleportTo( + level, + location.x, + location.y, + location.z, + 0f, + 0f + ) +} diff --git a/gradle.properties b/gradle.properties index f81d580..a9e490c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ klibs.java.ktarget=21 # Project klibs.project.name=AstraLibs klibs.project.group=ru.astrainteractive.astralibs -klibs.project.version.string=3.38.1 +klibs.project.version.string=3.39.0 klibs.project.description=Core utilities for spigot development klibs.project.developers=makeevrserg|Makeev Roman|makeevrserg@gmail.com klibs.project.url=https://empireprojekt.ru diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f83ae93..b6eff50 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,6 @@ driver-jdbc = "3.53.0.0" driver-mariadb = "3.5.8" driver-mysql = "8.0.33" exposed = "1.2.0" -forgegradle = "[6.0,6.2)" gradle-buildconfig = "6.0.9" gradle-ftp = "0.1.3" gradle-shadow = "9.4.1" @@ -46,6 +45,9 @@ minecraft-mockbukkit = "4.108.0" minecraft-mojang-version = "1.21.11" minecraft-neoforged-bus = "8.0.2" minecraft-neoforgegradle = "7.1.20" +minecraft-forgegradle = "7.0.25" +minecraft-minecraftforge-forge = "47.4.20" +minecraft-minecraftforge-minecraft = "1.20.1" minecraft-neoforgeversion = "21.1.129" minecraft-packetevents = "2.12.1" minecraft-papi = "2.12.2" @@ -151,13 +153,13 @@ tests-turbine = { module = "app.cash.turbine:turbine", version.ref = "tests-turb [plugins] gradle-buildconfig = { id = "com.github.gmazzo.buildconfig", version.ref = "gradle-buildconfig" } gradle-fabric-loom = { id = "fabric-loom", version.ref = "minecraft-fabric-loom" } -gradle-forgegradle = { id = "net.minecraftforge.gradle", version.ref = "forgegradle" } +gradle-forgegradle = { id = "net.minecraftforge.gradle", version.ref = "minecraft-forgegradle" } gradle-ftp = { id = "ru.astrainteractive.ftpplugin.gradleftp", version.ref = "gradle-ftp" } gradle-neoforgegradle = { id = "net.neoforged.gradle.userdev", version.ref = "minecraft-neoforgegradle" } gradle-shadow = { id = "com.gradleup.shadow", version.ref = "gradle-shadow" } klibs-gradle-detekt = { id = "ru.astrainteractive.gradleplugin.detekt", version.ref = "klibs-gradleplugin" } klibs-gradle-detekt-compose = { id = "ru.astrainteractive.gradleplugin.detekt.compose", version.ref = "klibs-gradleplugin" } -klibs-gradle-dokka-module = { id = "ru.astrainteractive.gradleplugin.dokka.module", version.ref = "klibs-gradleplugin" } +klibs-gradle-dokka-root = { id = "ru.astrainteractive.gradleplugin.dokka.root", version.ref = "klibs-gradleplugin" } klibs-gradle-java-core = { id = "ru.astrainteractive.gradleplugin.java.core", version.ref = "klibs-gradleplugin" } klibs-gradle-java-version = { id = "ru.astrainteractive.gradleplugin.java.version", version.ref = "klibs-gradleplugin" } klibs-gradle-minecraft-empty = { id = "ru.astrainteractive.gradleplugin.minecraft.empty", version.ref = "klibs-gradleplugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 2daa84a..2e26b71 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ pluginManagement { maven("https://repo.dmulloy2.net/repository/public/") maven("https://oss.sonatype.org/content/groups/public/") maven("https://repo.essentialsx.net/snapshots/") + maven("https://files.minecraftforge.net/maven") maven("https://repo.maven.apache.org/maven2/") maven("https://maven.neoforged.net/releases") maven("https://maven.enginehub.org/repo/") @@ -30,6 +31,8 @@ dependencyResolutionManagement { maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") maven("https://papermc.io/repo/repository/maven-public/") maven("https://repo.papermc.io/repository/maven-public/") + maven("https://files.minecraftforge.net/maven") + maven("https://maven.minecraftforge.net/") maven("https://nexus.scarsz.me/content/groups/public/") maven("https://repo.dmulloy2.net/repository/public/") maven("https://oss.sonatype.org/content/groups/public/") @@ -37,6 +40,7 @@ dependencyResolutionManagement { maven("https://repo.essentialsx.net/releases/") maven("https://repo.maven.apache.org/maven2/") maven("https://maven.enginehub.org/repo/") + maven("https://libraries.minecraft.net/") maven("https://repo1.maven.org/maven2/") maven("https://maven.fabricmc.net/") maven("https://maven.playpro.com") @@ -51,6 +55,7 @@ rootProject.name = "AstraLibs" include(":core") include(":core-bukkit") include(":core-neoforge") +include(":core-forge") include(":core-fabric") include(":menu-bukkit") include(":command")