test: 1.12 integration tests
This commit is contained in:
1 parent
edbfef4e66
commit
3e4b6ce9e1
17 files changed
+1584
-139
No files matched your search
@@ -0,0 +1,81 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file captures repo-specific context discovered while working on this branch.
|
||||
|
||||
## Repo overview
|
||||
- Project: OldCombatMechanics (Bukkit/Paper plugin)
|
||||
- Branch context: working from `kotlin-tests` branch
|
||||
- Build tool: Gradle (wrapper currently at 9.2.1)
|
||||
- JDKs used locally: 8, 11, 17, 25
|
||||
|
||||
## Integration test harness (Kotlin)
|
||||
- Integration tests live in `src/integrationTest/kotlin` and are packaged into `OldCombatMechanics-<version>-tests.jar`.
|
||||
- Entrypoint plugin class: `kernitus.plugin.OldCombatMechanics.OCMTestMain`.
|
||||
- Tests run inside a real Paper server started by the Gradle `run-paper` plugin.
|
||||
- Matrix task: `integrationTest` depends on `integrationTestMatrix` which runs per-version tasks like:
|
||||
- `integrationTest1_19_2`, `integrationTest1_21_11`, `integrationTest1_12`, `integrationTest1_9`
|
||||
- Test result handoff:
|
||||
- `OCMTestMain` writes `plugins/OldCombatMechanicsTest/test-results.txt` containing `PASS` or `FAIL`.
|
||||
- Gradle `checkTestResults<version>` fails build if file missing, or content is not `PASS`.
|
||||
|
||||
## Version matrix + Java selection
|
||||
- Config is in `build.gradle.kts`:
|
||||
- `integrationTestVersions` from property or defaults.
|
||||
- `requiredJavaVersion` selects Java based on version.
|
||||
- Pre-1.13 versions use `integrationTestJavaVersionLegacyPre13` (default 8).
|
||||
- Modern versions use Java 25 if `>=1.20.5` else Java 17.
|
||||
- Legacy vanilla jar cache:
|
||||
- `downloadVanilla<version>` task downloads Mojang server jars for <=1.12 and writes `run/<version>/cache/mojang_<version>.jar`.
|
||||
|
||||
## Kotlin test runner split (Java 11+ vs Java 8)
|
||||
- Kotest 6 (Java 11+) is used for 1.19.2 and 1.21.11.
|
||||
- Kotest runner class: `kernitus.plugin.OldCombatMechanics.KotestRunner`
|
||||
- Project config: `KotestProjectConfig`
|
||||
- Java 8 uses a separate runner:
|
||||
- `kernitus.plugin.OldCombatMechanics.LegacyTestRunner`
|
||||
- Currently a **smoke test** only (verifies plugin enabled + `WeaponDamages` loaded).
|
||||
- This is a placeholder and **not a full integration test**.
|
||||
|
||||
## Fake player implementation notes
|
||||
- Primary fake player implementation is `src/integrationTest/kotlin/kernitus/plugin/OldCombatMechanics/FakePlayer.kt`.
|
||||
- It relies on `xyz.jpenilla:reflection-remapper` to map modern NMS names.
|
||||
- On legacy servers (1.12), reflection remapper mappings are unavailable; code falls back to `ReflectionRemapper.noop()`.
|
||||
- As-is, `FakePlayer` uses modern NMS class names (`net.minecraft.server.MinecraftServer`, etc.).
|
||||
- This fails on 1.12 which uses versioned NMS (`net.minecraft.server.v1_12_R1.*`).
|
||||
- 1.12 requires a dedicated fake player path or version-aware class mapping.
|
||||
|
||||
## Java 8 compatibility work
|
||||
- Java 8 compatibility backports already done in main code (records/pattern matching removed).
|
||||
- Java 8-incompatible APIs replaced:
|
||||
- `Stream.toList()` -> `collect(Collectors.toList())`
|
||||
- `Set.of`/`List.of` -> `Collections.unmodifiableSet(new HashSet<>(Arrays.asList(...)))` etc.
|
||||
- Added missing import in `PotionTypeCompat` for `PotionData`.
|
||||
- Build config sets `options.release.set(8)` for Java, Kotlin `jvmTarget = 1.8`.
|
||||
|
||||
## Dependency / build updates
|
||||
- Kotlin: 2.3.0
|
||||
- Kotest: 6.0.7
|
||||
- run-paper plugin: 3.0.2
|
||||
- Hangar publish plugin: 0.1.4
|
||||
- Other deps updated (bstats, netty, BSON, XSeries, authlib, reflection-remapper, adventure).
|
||||
- JSR-305 added for `javax.annotation.Nullable` (compileOnly).
|
||||
|
||||
## Current failing area
|
||||
- Paper 1.12 integration tests **do not run real fake player tests yet**.
|
||||
- Desired fix: add proper 1.12 fake player implementation (e.g., version-specific NMS path like `v1_12_R1`).
|
||||
- Example 1.12 fake player implementation provided by user (NMS, PlayerList manipulation, packet send, etc.).
|
||||
- We should integrate a conditional path in `FakePlayer` for 1.12 using versioned NMS classes or an alternate helper.
|
||||
|
||||
## Local test commands
|
||||
- Run full matrix:
|
||||
- `./gradlew integrationTest`
|
||||
- Change matrix:
|
||||
- `./gradlew integrationTest -PintegrationTestVersions=1.19.2,1.21.11,1.12`
|
||||
- Set Java toolchain paths (example):
|
||||
- `ORG_GRADLE_JAVA_INSTALLATIONS_PATHS=/path/to/jdk8:/path/to/jdk17:/path/to/jdk25 ./gradlew integrationTest`
|
||||
|
||||
## Notes
|
||||
- Paper 1.12 sometimes fails to download legacy vanilla jar from old Mojang endpoint. The custom `downloadVanilla` task fixes that by using the v2 manifest.
|
||||
- 1.21.11 servers log hostname warnings and Unsafe warnings; tests still pass.
|
||||
- 1.9 integration tests are currently on hold per user request.
|
||||
- Reflection should be used only as a fallback (performance cost); prefer direct API/code paths when available.
|
||||
+87
-2
@@ -5,14 +5,19 @@
|
||||
*/
|
||||
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import groovy.json.JsonSlurper
|
||||
import io.papermc.hangarpublishplugin.model.Platforms
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.attributes.java.TargetJvmVersion
|
||||
import org.gradle.api.file.FileCopyDetails
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import xyz.jpenilla.runpaper.task.RunServer
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Serializable
|
||||
import java.net.URI
|
||||
import java.security.MessageDigest
|
||||
import java.nio.file.Files
|
||||
|
||||
val paperVersion: List<String> = (property("gameVersions") as String)
|
||||
.split(",")
|
||||
@@ -85,6 +90,13 @@ configurations {
|
||||
}
|
||||
}
|
||||
|
||||
configurations.named("compileClasspath") {
|
||||
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
|
||||
}
|
||||
configurations.named("integrationTestCompileClasspath") {
|
||||
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.bstats:bstats-bukkit:3.1.0")
|
||||
// Shaded in by Bukkit
|
||||
@@ -141,6 +153,7 @@ tasks.named<Copy>("processResources") {
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
options.release.set(8)
|
||||
}
|
||||
|
||||
val shadowJarTask = tasks.named<ShadowJar>("shadowJar") {
|
||||
@@ -171,7 +184,7 @@ kotlin {
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
|
||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_1_8)
|
||||
}
|
||||
|
||||
val integrationTestJarTask = tasks.register<ShadowJar>("integrationTestJar") {
|
||||
@@ -198,7 +211,7 @@ val integrationTestJarTask = tasks.register<ShadowJar>("integrationTestJar") {
|
||||
val integrationTestMinecraftVersion =
|
||||
(findProperty("integrationTestMinecraftVersion") as String?) ?: "1.19.2"
|
||||
|
||||
val defaultIntegrationTestVersions = listOf(integrationTestMinecraftVersion, "1.21.11")
|
||||
val defaultIntegrationTestVersions = listOf(integrationTestMinecraftVersion, "1.21.11", "1.12")
|
||||
.distinct()
|
||||
|
||||
val integrationTestVersions: List<String> = (findProperty("integrationTestVersions") as String?)
|
||||
@@ -210,6 +223,8 @@ val integrationTestVersions: List<String> = (findProperty("integrationTestVersio
|
||||
|
||||
val integrationTestJavaVersionLegacy =
|
||||
(findProperty("integrationTestJavaVersionLegacy") as String?)?.toInt() ?: 17
|
||||
val integrationTestJavaVersionLegacyPre13 =
|
||||
(findProperty("integrationTestJavaVersionLegacyPre13") as String?)?.toInt() ?: 8
|
||||
val integrationTestJavaVersionModern =
|
||||
(findProperty("integrationTestJavaVersionModern") as String?)?.toInt() ?: 25
|
||||
|
||||
@@ -221,6 +236,11 @@ fun parseMinecraftVersion(version: String): Triple<Int, Int, Int> {
|
||||
return Triple(major, minor, patch)
|
||||
}
|
||||
|
||||
fun needsLegacyVanillaJar(version: String): Boolean {
|
||||
val (major, minor, _) = parseMinecraftVersion(version)
|
||||
return major == 1 && minor <= 12
|
||||
}
|
||||
|
||||
fun requiresModernJava(version: String): Boolean {
|
||||
val (major, minor, patch) = parseMinecraftVersion(version)
|
||||
if (major > 1) return true
|
||||
@@ -229,9 +249,23 @@ fun requiresModernJava(version: String): Boolean {
|
||||
}
|
||||
|
||||
fun requiredJavaVersion(version: String): Int {
|
||||
if (needsLegacyVanillaJar(version)) return integrationTestJavaVersionLegacyPre13
|
||||
return if (requiresModernJava(version)) integrationTestJavaVersionModern else integrationTestJavaVersionLegacy
|
||||
}
|
||||
|
||||
fun sha1(file: File): String {
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
file.inputStream().use { input ->
|
||||
val buffer = ByteArray(8192)
|
||||
while (true) {
|
||||
val read = input.read(buffer)
|
||||
if (read <= 0) break
|
||||
digest.update(buffer, 0, read)
|
||||
}
|
||||
}
|
||||
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
tasks.register("integrationTest") {
|
||||
group = "verification"
|
||||
description = "Runs integration tests against all configured Paper versions."
|
||||
@@ -258,6 +292,7 @@ for (version in integrationTestVersions) {
|
||||
val suffix = versionTaskSuffix(version)
|
||||
val runDir = file("run/$version")
|
||||
val resultFile = runDir.resolve("plugins/OldCombatMechanicsTest/test-results.txt")
|
||||
val vanillaCacheFile = runDir.resolve("cache/mojang_${version}.jar")
|
||||
|
||||
val writePropsTask = tasks.register<WriteProperties>("writeProperties${suffix}") {
|
||||
encoding = "UTF-8"
|
||||
@@ -265,8 +300,58 @@ for (version in integrationTestVersions) {
|
||||
destinationFile.set(runDir.resolve("server.properties"))
|
||||
}
|
||||
|
||||
val downloadVanillaTask = if (needsLegacyVanillaJar(version)) {
|
||||
tasks.register("downloadVanilla${suffix}") {
|
||||
outputs.file(vanillaCacheFile)
|
||||
notCompatibleWithConfigurationCache("Downloads vanilla server jar for legacy Paper versions.")
|
||||
doLast {
|
||||
val slurper = JsonSlurper()
|
||||
val manifestText = URI("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")
|
||||
.toURL()
|
||||
.readText()
|
||||
val manifest = slurper.parseText(manifestText) as Map<*, *>
|
||||
val versionsList = manifest["versions"] as List<Map<*, *>>
|
||||
val versionEntry = versionsList.firstOrNull { it["id"] == version }
|
||||
?: throw GradleException("Minecraft version '$version' not found in Mojang manifest.")
|
||||
val versionUrl = versionEntry["url"] as String
|
||||
val versionMetaText = URI(versionUrl).toURL().readText()
|
||||
val versionMeta = slurper.parseText(versionMetaText) as Map<*, *>
|
||||
val downloads = versionMeta["downloads"] as Map<*, *>
|
||||
val serverInfo = downloads["server"] as Map<*, *>
|
||||
val serverUrl = serverInfo["url"] as String
|
||||
val serverSha1 = serverInfo["sha1"] as String
|
||||
|
||||
if (vanillaCacheFile.exists()) {
|
||||
val existingSha1 = sha1(vanillaCacheFile)
|
||||
if (existingSha1.equals(serverSha1, ignoreCase = true)) {
|
||||
return@doLast
|
||||
}
|
||||
} else {
|
||||
vanillaCacheFile.parentFile.mkdirs()
|
||||
}
|
||||
|
||||
val tmpFile = Files.createTempFile("mc-server-${version}-", ".jar").toFile()
|
||||
URI(serverUrl).toURL().openStream().use { input ->
|
||||
tmpFile.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
val downloadedSha1 = sha1(tmpFile)
|
||||
if (!downloadedSha1.equals(serverSha1, ignoreCase = true)) {
|
||||
tmpFile.delete()
|
||||
throw GradleException(
|
||||
"Downloaded Minecraft server jar hash mismatch for $version. Expected $serverSha1, got $downloadedSha1."
|
||||
)
|
||||
}
|
||||
tmpFile.copyTo(vanillaCacheFile, overwrite = true)
|
||||
tmpFile.delete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val runServerTask = tasks.register<RunServer>("runServer${suffix}") {
|
||||
dependsOn(writePropsTask)
|
||||
downloadVanillaTask?.let { dependsOn(it) }
|
||||
runDirectory.set(runDir)
|
||||
minecraftVersion(version)
|
||||
jvmArgs("-Dcom.mojang.eula.agree=true")
|
||||
|
||||
+2
-2
@@ -4,5 +4,5 @@
|
||||
|
||||
gameVersions=1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2, 1.18.1, 1.18, 1.17, 1.16, 1.15, 1.14, 1.13, 1.12, 1.11, 1.10, 1.9
|
||||
|
||||
# Enable configuration cache.
|
||||
org.gradle.configuration-cache=true
|
||||
# Disable configuration cache (run-paper tasks are not compatible).
|
||||
org.gradle.configuration-cache=false
|
||||
@@ -3,6 +3,4 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
|
||||
rootProject.name = "OldCombatMechanics"
|
||||
@@ -11,12 +11,14 @@ import io.netty.channel.ChannelInboundHandlerAdapter
|
||||
import io.netty.channel.embedded.EmbeddedChannel
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.GameMode
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Entity
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.player.AsyncPlayerPreLoginEvent
|
||||
import org.bukkit.event.player.PlayerJoinEvent
|
||||
import org.bukkit.event.player.PlayerPreLoginEvent
|
||||
import org.bukkit.event.player.PlayerQuitEvent
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import xyz.jpenilla.reflectionremapper.ReflectionRemapper
|
||||
@@ -33,7 +35,14 @@ class FakePlayer(private val plugin: JavaPlugin) {
|
||||
private var bukkitPlayer: Player? = null
|
||||
private var networkConnection: Any? = null
|
||||
private var usedPlaceNewPlayer: Boolean = false
|
||||
private val reflectionRemapper = ReflectionRemapper.forReobfMappingsInPaperJar()
|
||||
private val isLegacy = !Reflector.versionIsNewerOrEqualTo(1, 13, 0)
|
||||
private val legacyImpl: LegacyFakePlayer12? = if (isLegacy) LegacyFakePlayer12(plugin, uuid, name) else null
|
||||
private val reflectionRemapper: ReflectionRemapper = try {
|
||||
ReflectionRemapper.forReobfMappingsInPaperJar()
|
||||
} catch (e: Throwable) {
|
||||
plugin.logger.warning("Reflection mappings not found; using no-op remapper for legacy server.")
|
||||
ReflectionRemapper.noop()
|
||||
}
|
||||
|
||||
// Helper function to load NMS classes using the appropriate class loader and remap names
|
||||
fun getNMSClass(name: String): Class<*> {
|
||||
@@ -50,6 +59,12 @@ class FakePlayer(private val plugin: JavaPlugin) {
|
||||
}
|
||||
|
||||
fun spawn(location: Location) {
|
||||
if (isLegacy) {
|
||||
legacyImpl!!.spawn(location)
|
||||
serverPlayer = legacyImpl.entityPlayer ?: throw IllegalStateException("Legacy entity player not created.")
|
||||
bukkitPlayer = legacyImpl.bukkitPlayer
|
||||
return
|
||||
}
|
||||
plugin.logger.info("Spawn: Starting")
|
||||
|
||||
// Get the NMS WorldServer (ServerLevel) from the Bukkit world
|
||||
@@ -563,6 +578,9 @@ class FakePlayer(private val plugin: JavaPlugin) {
|
||||
}
|
||||
|
||||
fun getConnection(serverPlayer: Any): Any {
|
||||
if (isLegacy) {
|
||||
return legacyImpl!!.getConnection(serverPlayer)
|
||||
}
|
||||
val entityPlayerClass = serverPlayer.javaClass
|
||||
val connectionFieldName = reflectionRemapper.remapFieldName(entityPlayerClass, "connection")
|
||||
val connectionField = Reflector.getField(entityPlayerClass, connectionFieldName)
|
||||
@@ -570,6 +588,10 @@ class FakePlayer(private val plugin: JavaPlugin) {
|
||||
}
|
||||
|
||||
fun removePlayer() {
|
||||
if (isLegacy) {
|
||||
legacyImpl!!.removePlayer()
|
||||
return
|
||||
}
|
||||
// Fire PlayerQuitEvent
|
||||
val quitMessage = "§e$name left the game"
|
||||
val playerQuitEvent = PlayerQuitEvent(bukkitPlayer!!, quitMessage)
|
||||
@@ -684,6 +706,16 @@ class FakePlayer(private val plugin: JavaPlugin) {
|
||||
}
|
||||
|
||||
fun doBlocking() {
|
||||
if (isLegacy) {
|
||||
// Legacy blocking: ensure sword in hand + shield in offhand, then raise offhand.
|
||||
bukkitPlayer!!.inventory.setItemInMainHand(org.bukkit.inventory.ItemStack(Material.DIAMOND_SWORD))
|
||||
if (bukkitPlayer!!.inventory.itemInOffHand.type != Material.SHIELD) {
|
||||
bukkitPlayer!!.inventory.setItemInOffHand(org.bukkit.inventory.ItemStack(Material.SHIELD))
|
||||
}
|
||||
bukkitPlayer!!.updateInventory()
|
||||
legacyImpl?.startUsingOffhand()
|
||||
return
|
||||
}
|
||||
bukkitPlayer!!.inventory.setItemInMainHand(org.bukkit.inventory.ItemStack(Material.SHIELD))
|
||||
|
||||
val livingEntityClass = getNMSClass("net.minecraft.world.entity.LivingEntity")
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import io.kotest.common.KotestInternal
|
||||
import io.kotest.core.config.AbstractProjectConfig
|
||||
import io.kotest.core.extensions.TestCaseExtension
|
||||
import io.kotest.core.spec.IsolationMode
|
||||
import io.kotest.core.test.TestCase
|
||||
import io.kotest.engine.TestEngineLauncher
|
||||
import io.kotest.engine.concurrency.SpecExecutionMode
|
||||
import io.kotest.engine.concurrency.TestExecutionMode
|
||||
import io.kotest.engine.listener.AbstractTestEngineListener
|
||||
import io.kotest.engine.test.TestResult
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
object KotestProjectConfig : AbstractProjectConfig() {
|
||||
override val isolationMode = IsolationMode.SingleInstance
|
||||
override val specExecutionMode = SpecExecutionMode.Sequential
|
||||
override val testExecutionMode = TestExecutionMode.Sequential
|
||||
}
|
||||
|
||||
class BukkitMainThreadDispatcher(private val plugin: JavaPlugin) : CoroutineDispatcher() {
|
||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||
Bukkit.getScheduler().runTask(plugin, block)
|
||||
}
|
||||
}
|
||||
|
||||
class MainThreadDispatcherExtension(private val plugin: JavaPlugin) : TestCaseExtension {
|
||||
override suspend fun intercept(testCase: TestCase, execute: suspend (TestCase) -> TestResult): TestResult {
|
||||
val dispatcher = BukkitMainThreadDispatcher(plugin)
|
||||
val newContext = coroutineContext + dispatcher
|
||||
return withContext(newContext) {
|
||||
execute(testCase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object KotestRunner {
|
||||
@OptIn(KotestInternal::class)
|
||||
@JvmStatic
|
||||
fun run(plugin: JavaPlugin) {
|
||||
// Schedule test asynchronously to avoid deadlock.
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, Runnable {
|
||||
try {
|
||||
var hasFailures = false
|
||||
|
||||
val listener = object : AbstractTestEngineListener() {
|
||||
override suspend fun testFinished(testCase: TestCase, result: TestResult) {
|
||||
val testName = testCase.name.name
|
||||
when {
|
||||
result.isSuccess -> plugin.logger.info("Test '$testName' passed.")
|
||||
result.isIgnored -> plugin.logger.info("Test '$testName' was ignored.")
|
||||
result.isFailure || result.isError -> {
|
||||
val message = result.errorOrNull?.message
|
||||
?: result.reasonOrNull
|
||||
?: "Unknown failure"
|
||||
plugin.logger.warning("Test '$testName' failed with exception: $message")
|
||||
hasFailures = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun testStarted(testCase: TestCase) {
|
||||
plugin.logger.info("Starting test '${testCase.name.name}'")
|
||||
}
|
||||
|
||||
override suspend fun engineFinished(t: List<Throwable>) {
|
||||
val success = t.isEmpty() && !hasFailures
|
||||
if (success) {
|
||||
plugin.logger.info("All tests passed successfully.")
|
||||
} else {
|
||||
plugin.logger.warning("Test run completed with failures.")
|
||||
t.forEach { throwable ->
|
||||
plugin.logger.warning("Engine error: ${throwable.message}")
|
||||
}
|
||||
}
|
||||
|
||||
TestResultWriter.writeAndShutdown(plugin, success)
|
||||
}
|
||||
}
|
||||
|
||||
TestEngineLauncher()
|
||||
.withListener(listener)
|
||||
.withProjectConfig(KotestProjectConfig)
|
||||
.withClasses(InGameTesterIntegrationTest::class)
|
||||
.launch()
|
||||
} catch (e: Throwable) {
|
||||
plugin.logger.severe("Failed to execute Kotest runner: ${e.message}")
|
||||
TestResultWriter.writeAndShutdown(plugin, false, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import com.mojang.authlib.GameProfile
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter
|
||||
import io.netty.channel.embedded.EmbeddedChannel
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.GameMode
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.player.AsyncPlayerPreLoginEvent
|
||||
import org.bukkit.event.player.PlayerJoinEvent
|
||||
import org.bukkit.event.player.PlayerPreLoginEvent
|
||||
import org.bukkit.event.player.PlayerQuitEvent
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.net.InetAddress
|
||||
import java.util.UUID
|
||||
|
||||
internal class LegacyFakePlayer12(
|
||||
private val plugin: JavaPlugin,
|
||||
val uuid: UUID,
|
||||
val name: String
|
||||
) {
|
||||
private val cbVersion: String = Bukkit.getServer().javaClass.`package`.name.substringAfterLast('.')
|
||||
var entityPlayer: Any? = null
|
||||
private set
|
||||
var bukkitPlayer: Player? = null
|
||||
private set
|
||||
|
||||
private fun nmsClass(simpleName: String): Class<*> =
|
||||
Class.forName("net.minecraft.server.$cbVersion.$simpleName")
|
||||
|
||||
private fun craftClass(simpleName: String): Class<*> =
|
||||
Class.forName("org.bukkit.craftbukkit.$cbVersion.$simpleName")
|
||||
|
||||
fun spawn(location: Location) {
|
||||
plugin.logger.info("Spawn: Starting (legacy $cbVersion)")
|
||||
val world = location.world ?: throw IllegalArgumentException("Location has no world!")
|
||||
|
||||
val craftWorld = craftClass("CraftWorld").cast(world)
|
||||
val worldServer = craftWorld.javaClass.getMethod("getHandle").invoke(craftWorld)
|
||||
|
||||
val craftServer = craftClass("CraftServer").cast(Bukkit.getServer())
|
||||
val minecraftServer = craftServer.javaClass.getMethod("getServer").invoke(craftServer)
|
||||
|
||||
val entityPlayer = createEntityPlayer(minecraftServer, worldServer)
|
||||
this.entityPlayer = entityPlayer
|
||||
val bukkitPlayer = entityPlayer.javaClass.getMethod("getBukkitEntity").invoke(entityPlayer) as Player
|
||||
this.bukkitPlayer = bukkitPlayer
|
||||
|
||||
firePreLoginEvents()
|
||||
|
||||
val playerList = minecraftServer.javaClass.getMethod("getPlayerList").invoke(minecraftServer)
|
||||
invokeMethodIfExists(playerList, "a", entityPlayer)
|
||||
|
||||
setPositionRotation(entityPlayer, location)
|
||||
setDataWatcherFlags(entityPlayer)
|
||||
|
||||
spawnInWorld(entityPlayer, worldServer)
|
||||
setGameMode(entityPlayer, worldServer)
|
||||
setupConnection(entityPlayer, minecraftServer)
|
||||
|
||||
addToPlayerChunkMap(worldServer, entityPlayer)
|
||||
addToPlayerList(playerList, entityPlayer)
|
||||
updatePlayerListMaps(playerList, entityPlayer)
|
||||
|
||||
val joinMessage = "§e$name joined the game"
|
||||
val joinEvent = PlayerJoinEvent(bukkitPlayer, joinMessage)
|
||||
Bukkit.getPluginManager().callEvent(joinEvent)
|
||||
broadcastMessage(joinEvent.joinMessage)
|
||||
|
||||
sendSpawnPackets(entityPlayer)
|
||||
addEntityToWorld(worldServer, entityPlayer)
|
||||
|
||||
Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, Runnable {
|
||||
runCatching { invokeMethod(entityPlayer, "playerTick") }
|
||||
}, 1L, 1L)
|
||||
|
||||
plugin.logger.info("Spawn: completed successfully (legacy)")
|
||||
}
|
||||
|
||||
fun removePlayer() {
|
||||
val entityPlayer = entityPlayer ?: return
|
||||
val bukkitPlayer = bukkitPlayer ?: return
|
||||
|
||||
val craftServer = craftClass("CraftServer").cast(Bukkit.getServer())
|
||||
val minecraftServer = craftServer.javaClass.getMethod("getServer").invoke(craftServer)
|
||||
val playerList = minecraftServer.javaClass.getMethod("getPlayerList").invoke(minecraftServer)
|
||||
|
||||
val quitMessage = "§e$name left the game"
|
||||
val quitEvent = PlayerQuitEvent(bukkitPlayer, quitMessage)
|
||||
Bukkit.getPluginManager().callEvent(quitEvent)
|
||||
|
||||
val worldServer = getWorldServer(entityPlayer)
|
||||
val playerChunkMap = getPlayerChunkMap(worldServer)
|
||||
invokeMethodIfExists(playerChunkMap, "removePlayer", entityPlayer)
|
||||
invokeMethodIfExists(worldServer, "removeEntity", entityPlayer)
|
||||
|
||||
bukkitPlayer.kickPlayer(quitMessage)
|
||||
|
||||
removeFromPlayerList(playerList, entityPlayer)
|
||||
removeFromPlayerMaps(playerList, entityPlayer)
|
||||
|
||||
sendRemovePackets(entityPlayer)
|
||||
|
||||
invokeMethodIfExists(playerList, "savePlayerFile", entityPlayer)
|
||||
}
|
||||
|
||||
fun startUsingOffhand() {
|
||||
val entityPlayer = entityPlayer ?: return
|
||||
val enumHandClass = nmsClass("EnumHand")
|
||||
val offHand = enumValue(enumHandClass, "OFF_HAND")
|
||||
val candidateNames = arrayOf("c", "a", "startUsingItem")
|
||||
for (name in candidateNames) {
|
||||
val method = findMethod(entityPlayer.javaClass, name, arrayOf(offHand), ignoreMissing = true)
|
||||
if (method != null) {
|
||||
method.invoke(entityPlayer, offHand)
|
||||
return
|
||||
}
|
||||
}
|
||||
val fallback = entityPlayer.javaClass.methods.firstOrNull { method ->
|
||||
method.parameterTypes.size == 1 && method.parameterTypes[0] == enumHandClass
|
||||
}
|
||||
fallback?.invoke(entityPlayer, offHand)
|
||||
}
|
||||
|
||||
fun getConnection(serverPlayer: Any): Any {
|
||||
val connectionField = findField(serverPlayer.javaClass, "playerConnection")
|
||||
?: throw NoSuchFieldException("playerConnection not found on ${serverPlayer.javaClass.name}")
|
||||
return connectionField.get(serverPlayer)
|
||||
}
|
||||
|
||||
private fun createEntityPlayer(minecraftServer: Any, worldServer: Any): Any {
|
||||
val entityPlayerClass = nmsClass("EntityPlayer")
|
||||
val playerInteractManagerClass = nmsClass("PlayerInteractManager")
|
||||
val pimCtor = playerInteractManagerClass.constructors.firstOrNull { ctor ->
|
||||
ctor.parameterTypes.size == 1 && ctor.parameterTypes[0].isAssignableFrom(worldServer.javaClass)
|
||||
} ?: playerInteractManagerClass.constructors.first()
|
||||
val playerInteractManager = pimCtor.newInstance(worldServer)
|
||||
val gameProfile = GameProfile(uuid, name)
|
||||
val ctor = entityPlayerClass.constructors.firstOrNull { ctor ->
|
||||
val params = ctor.parameterTypes
|
||||
params.size == 4 &&
|
||||
params[0].isAssignableFrom(minecraftServer.javaClass) &&
|
||||
params[1].isAssignableFrom(worldServer.javaClass) &&
|
||||
params[2] == GameProfile::class.java &&
|
||||
params[3].isAssignableFrom(playerInteractManagerClass)
|
||||
} ?: entityPlayerClass.constructors.first()
|
||||
return ctor.newInstance(minecraftServer, worldServer, gameProfile, playerInteractManager)
|
||||
}
|
||||
|
||||
private fun firePreLoginEvents() {
|
||||
try {
|
||||
val address = InetAddress.getByName("127.0.0.1")
|
||||
val asyncPreLogin = AsyncPlayerPreLoginEvent(name, address, uuid)
|
||||
val preLogin = PlayerPreLoginEvent(name, address, uuid)
|
||||
Thread { Bukkit.getPluginManager().callEvent(asyncPreLogin) }.start()
|
||||
Bukkit.getPluginManager().callEvent(preLogin)
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.warning("Failed to fire pre-login events: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPositionRotation(entityPlayer: Any, location: Location) {
|
||||
val method = entityPlayer.javaClass.getMethod(
|
||||
"setPositionRotation",
|
||||
Double::class.javaPrimitiveType,
|
||||
Double::class.javaPrimitiveType,
|
||||
Double::class.javaPrimitiveType,
|
||||
Float::class.javaPrimitiveType,
|
||||
Float::class.javaPrimitiveType
|
||||
)
|
||||
method.invoke(
|
||||
entityPlayer,
|
||||
location.x,
|
||||
location.y,
|
||||
location.z,
|
||||
location.yaw,
|
||||
location.pitch
|
||||
)
|
||||
}
|
||||
|
||||
private fun setDataWatcherFlags(entityPlayer: Any) {
|
||||
runCatching {
|
||||
val dataWatcher = invokeMethod(entityPlayer, "getDataWatcher")
|
||||
val dataWatcherRegistryClass = nmsClass("DataWatcherRegistry")
|
||||
val serializer = dataWatcherRegistryClass.getField("a").get(null)
|
||||
val dataWatcherObject = invokeMethod(serializer, "a", 13)
|
||||
val setMethod = dataWatcher.javaClass.methods.firstOrNull { it.name == "set" && it.parameterCount == 2 }
|
||||
setMethod?.invoke(dataWatcher, dataWatcherObject, 127.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
private fun spawnInWorld(entityPlayer: Any, worldServer: Any) {
|
||||
invokeMethodIfExists(entityPlayer, "spawnIn", worldServer)
|
||||
val playerInteractManager = findField(entityPlayer.javaClass, "playerInteractManager")?.get(entityPlayer)
|
||||
if (playerInteractManager != null) {
|
||||
invokeMethodIfExists(playerInteractManager, "a", worldServer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setGameMode(entityPlayer: Any, worldServer: Any) {
|
||||
val playerInteractManager = findField(entityPlayer.javaClass, "playerInteractManager")?.get(entityPlayer) ?: return
|
||||
val enumGamemodeClass = nmsClass("EnumGamemode")
|
||||
val gameMode = Bukkit.getServer().defaultGameMode
|
||||
val enumValue = enumValue(enumGamemodeClass, gameMode)
|
||||
invokeMethodIfExists(playerInteractManager, "b", enumValue)
|
||||
}
|
||||
|
||||
private fun setupConnection(entityPlayer: Any, minecraftServer: Any) {
|
||||
val networkManagerClass = nmsClass("NetworkManager")
|
||||
val enumProtocolDirectionClass = nmsClass("EnumProtocolDirection")
|
||||
val serverbound = enumValue(enumProtocolDirectionClass, "SERVERBOUND")
|
||||
val networkManager = networkManagerClass
|
||||
.getConstructor(enumProtocolDirectionClass)
|
||||
.newInstance(serverbound)
|
||||
|
||||
val playerConnectionClass = nmsClass("PlayerConnection")
|
||||
val pcCtor = playerConnectionClass.constructors.firstOrNull { ctor ->
|
||||
val params = ctor.parameterTypes
|
||||
params.size == 3 &&
|
||||
params[0].isAssignableFrom(minecraftServer.javaClass) &&
|
||||
params[1].isAssignableFrom(networkManagerClass) &&
|
||||
params[2].isAssignableFrom(entityPlayer.javaClass)
|
||||
} ?: playerConnectionClass.constructors.first()
|
||||
val playerConnection = pcCtor.newInstance(minecraftServer, networkManager, entityPlayer)
|
||||
|
||||
setFieldValue(entityPlayer, "playerConnection", playerConnection)
|
||||
val channel = EmbeddedChannel(ChannelInboundHandlerAdapter())
|
||||
setFieldValue(networkManager, "channel", channel)
|
||||
runCatching { channel.close() }
|
||||
}
|
||||
|
||||
private fun addToPlayerChunkMap(worldServer: Any, entityPlayer: Any) {
|
||||
val playerChunkMap = getPlayerChunkMap(worldServer)
|
||||
invokeMethodIfExists(playerChunkMap, "addPlayer", entityPlayer)
|
||||
}
|
||||
|
||||
private fun addToPlayerList(playerList: Any, entityPlayer: Any) {
|
||||
runCatching {
|
||||
val playersField = findField(playerList.javaClass, "players")
|
||||
@Suppress("UNCHECKED_CAST") val players = playersField?.get(playerList) as? MutableCollection<Any>
|
||||
players?.add(entityPlayer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlayerListMaps(playerList: Any, entityPlayer: Any) {
|
||||
runCatching {
|
||||
val byUuidField = findField(playerList.javaClass, "j")
|
||||
@Suppress("UNCHECKED_CAST") val byUuid = byUuidField?.get(playerList) as? MutableMap<Any, Any>
|
||||
byUuid?.put(uuid, entityPlayer)
|
||||
}
|
||||
runCatching {
|
||||
val byNameField = findField(playerList.javaClass, "playersByName")
|
||||
@Suppress("UNCHECKED_CAST") val byName = byNameField?.get(playerList) as? MutableMap<Any, Any>
|
||||
byName?.put(name, entityPlayer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendSpawnPackets(entityPlayer: Any) {
|
||||
val packetPlayOutPlayerInfo = nmsClass("PacketPlayOutPlayerInfo")
|
||||
val actionClass = nmsClass("PacketPlayOutPlayerInfo\$EnumPlayerInfoAction")
|
||||
val addAction = enumValue(actionClass, "ADD_PLAYER")
|
||||
val entityArray = java.lang.reflect.Array.newInstance(entityPlayer.javaClass, 1)
|
||||
java.lang.reflect.Array.set(entityArray, 0, entityPlayer)
|
||||
val infoPacket = packetPlayOutPlayerInfo
|
||||
.getConstructor(actionClass, entityArray.javaClass)
|
||||
.newInstance(addAction, entityArray)
|
||||
|
||||
val namedSpawnPacket = createSingleArgPacket("PacketPlayOutNamedEntitySpawn", entityPlayer)
|
||||
|
||||
val craftPlayerClass = craftClass("entity.CraftPlayer")
|
||||
for (player in Bukkit.getOnlinePlayers()) {
|
||||
val craftPlayer = craftPlayerClass.cast(player)
|
||||
val handle = craftPlayerClass.getMethod("getHandle").invoke(craftPlayer)
|
||||
val connection = findField(handle.javaClass, "playerConnection")?.get(handle) ?: continue
|
||||
sendPacket(connection, infoPacket)
|
||||
if (namedSpawnPacket != null) {
|
||||
sendPacket(connection, namedSpawnPacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendRemovePackets(entityPlayer: Any) {
|
||||
val packetDestroy = createEntityDestroyPacket(entityPlayer)
|
||||
val packetInfo = createPlayerInfoPacket("REMOVE_PLAYER", entityPlayer)
|
||||
val craftPlayerClass = craftClass("entity.CraftPlayer")
|
||||
for (player in Bukkit.getOnlinePlayers()) {
|
||||
val craftPlayer = craftPlayerClass.cast(player)
|
||||
val handle = craftPlayerClass.getMethod("getHandle").invoke(craftPlayer)
|
||||
val connection = findField(handle.javaClass, "playerConnection")?.get(handle) ?: continue
|
||||
if (packetDestroy != null) sendPacket(connection, packetDestroy)
|
||||
if (packetInfo != null) sendPacket(connection, packetInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPlayerInfoPacket(actionName: String, entityPlayer: Any): Any? {
|
||||
return runCatching {
|
||||
val packetPlayOutPlayerInfo = nmsClass("PacketPlayOutPlayerInfo")
|
||||
val actionClass = nmsClass("PacketPlayOutPlayerInfo\$EnumPlayerInfoAction")
|
||||
val action = enumValue(actionClass, actionName)
|
||||
val entityArray = java.lang.reflect.Array.newInstance(entityPlayer.javaClass, 1)
|
||||
java.lang.reflect.Array.set(entityArray, 0, entityPlayer)
|
||||
packetPlayOutPlayerInfo
|
||||
.getConstructor(actionClass, entityArray.javaClass)
|
||||
.newInstance(action, entityArray)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun createEntityDestroyPacket(entityPlayer: Any): Any? {
|
||||
return runCatching {
|
||||
val packetClass = nmsClass("PacketPlayOutEntityDestroy")
|
||||
val getIdMethod = entityPlayer.javaClass.getMethod("getId")
|
||||
val entityId = getIdMethod.invoke(entityPlayer) as Int
|
||||
val ids = intArrayOf(entityId)
|
||||
packetClass.getConstructor(IntArray::class.java).newInstance(ids)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun createSingleArgPacket(className: String, entityPlayer: Any): Any? {
|
||||
return runCatching {
|
||||
val packetClass = nmsClass(className)
|
||||
val ctor = packetClass.constructors.firstOrNull { ctor ->
|
||||
ctor.parameterTypes.size == 1 && ctor.parameterTypes[0].isAssignableFrom(entityPlayer.javaClass)
|
||||
} ?: packetClass.constructors.firstOrNull { ctor ->
|
||||
ctor.parameterTypes.size == 1 && ctor.parameterTypes[0].isAssignableFrom(entityPlayer.javaClass.superclass)
|
||||
}
|
||||
ctor?.newInstance(entityPlayer)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun addEntityToWorld(worldServer: Any, entityPlayer: Any) {
|
||||
invokeMethodIfExists(worldServer, "addEntity", entityPlayer)
|
||||
}
|
||||
|
||||
private fun getWorldServer(entityPlayer: Any): Any {
|
||||
val worldField = findField(entityPlayer.javaClass, "world")
|
||||
return worldField?.get(entityPlayer) ?: invokeMethod(entityPlayer, "getWorld")
|
||||
}
|
||||
|
||||
private fun getPlayerChunkMap(worldServer: Any): Any {
|
||||
val getPlayerChunkMap = worldServer.javaClass.methods.firstOrNull { it.name == "getPlayerChunkMap" }
|
||||
return if (getPlayerChunkMap != null) {
|
||||
getPlayerChunkMap.invoke(worldServer)
|
||||
} else {
|
||||
findField(worldServer.javaClass, "playerChunkMap")?.get(worldServer)
|
||||
?: throw NoSuchFieldException("playerChunkMap not found on ${worldServer.javaClass.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFromPlayerList(playerList: Any, entityPlayer: Any) {
|
||||
runCatching {
|
||||
val playersField = findField(playerList.javaClass, "players")
|
||||
@Suppress("UNCHECKED_CAST") val players = playersField?.get(playerList) as? MutableCollection<Any>
|
||||
players?.remove(entityPlayer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFromPlayerMaps(playerList: Any, entityPlayer: Any) {
|
||||
runCatching {
|
||||
val byUuidField = findField(playerList.javaClass, "j")
|
||||
@Suppress("UNCHECKED_CAST") val byUuid = byUuidField?.get(playerList) as? MutableMap<Any, Any>
|
||||
byUuid?.remove(uuid)
|
||||
}
|
||||
runCatching {
|
||||
val byNameField = findField(playerList.javaClass, "playersByName")
|
||||
@Suppress("UNCHECKED_CAST") val byName = byNameField?.get(playerList) as? MutableMap<Any, Any>
|
||||
byName?.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendPacket(connection: Any, packet: Any) {
|
||||
val packetClass = nmsClass("Packet")
|
||||
val sendMethod = connection.javaClass.getMethod("sendPacket", packetClass)
|
||||
sendMethod.invoke(connection, packet)
|
||||
}
|
||||
|
||||
private fun broadcastMessage(message: String?) {
|
||||
if (message.isNullOrEmpty()) return
|
||||
for (player in Bukkit.getOnlinePlayers()) {
|
||||
player.sendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun invokeMethod(target: Any, name: String, vararg args: Any?): Any {
|
||||
val method = findMethod(target.javaClass, name, args)
|
||||
?: throw NoSuchMethodException("Method '$name' not found on ${target.javaClass.name}")
|
||||
return method.invoke(target, *args)
|
||||
}
|
||||
|
||||
private fun invokeMethodIfExists(target: Any, name: String, vararg args: Any?) {
|
||||
val method = findMethod(target.javaClass, name, args, ignoreMissing = true) ?: return
|
||||
method.invoke(target, *args)
|
||||
}
|
||||
|
||||
private fun findMethod(
|
||||
clazz: Class<*>,
|
||||
name: String,
|
||||
args: Array<out Any?>,
|
||||
ignoreMissing: Boolean = false
|
||||
): Method? {
|
||||
val candidates = (clazz.methods + clazz.declaredMethods).filter { it.name == name }
|
||||
val method = candidates.firstOrNull { method ->
|
||||
if (method.parameterTypes.size != args.size) return@firstOrNull false
|
||||
method.parameterTypes.indices.all { idx ->
|
||||
val arg = args[idx]
|
||||
arg == null || method.parameterTypes[idx].isAssignableFrom(arg.javaClass)
|
||||
}
|
||||
} ?: candidates.firstOrNull { it.parameterTypes.size == args.size }
|
||||
if (method == null && !ignoreMissing) {
|
||||
throw NoSuchMethodException("Method '$name' not found on ${clazz.name}")
|
||||
}
|
||||
method?.isAccessible = true
|
||||
return method
|
||||
}
|
||||
|
||||
private fun findField(clazz: Class<*>, name: String): Field? {
|
||||
var current: Class<*>? = clazz
|
||||
while (current != null) {
|
||||
runCatching {
|
||||
val field = current.getDeclaredField(name)
|
||||
field.isAccessible = true
|
||||
return field
|
||||
}
|
||||
current = current.superclass
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun setFieldValue(target: Any, name: String, value: Any?) {
|
||||
val field = findField(target.javaClass, name) ?: return
|
||||
field.set(target, value)
|
||||
}
|
||||
|
||||
private fun enumValue(enumClass: Class<*>, name: String): Any {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val enumType = enumClass as Class<out Enum<*>>
|
||||
return java.lang.Enum.valueOf(enumType, name)
|
||||
}
|
||||
|
||||
private fun enumValue(enumClass: Class<*>, gameMode: GameMode): Any {
|
||||
return enumValue(enumClass, gameMode.name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModuleGoldenApple
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.damage.WeaponDamages
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.potions.PotionEffectTypeCompat
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.getPlayerData
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.setPlayerData
|
||||
import kernitus.plugin.OldCombatMechanics.versions.materials.MaterialRegistry.ENCHANTED_GOLDEN_APPLE
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.GameMode
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.attribute.Attribute
|
||||
import org.bukkit.block.BlockFace
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.block.Action
|
||||
import org.bukkit.event.player.PlayerInteractEvent
|
||||
import org.bukkit.event.player.PlayerItemConsumeEvent
|
||||
import org.bukkit.inventory.CraftingInventory
|
||||
import org.bukkit.inventory.EquipmentSlot
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import org.bukkit.potion.PotionEffect
|
||||
import org.bukkit.potion.PotionEffectType
|
||||
import java.lang.reflect.Constructor
|
||||
import java.util.logging.Level
|
||||
|
||||
object LegacyTestRunner {
|
||||
@JvmStatic
|
||||
fun run(plugin: JavaPlugin) {
|
||||
plugin.logger.info("Running legacy integration tests (Java 8 runtime).")
|
||||
Bukkit.getScheduler().runTask(plugin, Runnable {
|
||||
LegacyTestSuite(plugin).start()
|
||||
})
|
||||
}
|
||||
|
||||
private class LegacyTestSuite(private val plugin: JavaPlugin) {
|
||||
private var failures = 0
|
||||
private var total = 0
|
||||
private var delayTicks = 0L
|
||||
|
||||
private var meleeAttacker: FakePlayer? = null
|
||||
private var meleeDefender: FakePlayer? = null
|
||||
private var meleeAttackerPlayer: Player? = null
|
||||
private var meleeDefenderPlayer: Player? = null
|
||||
|
||||
private var singleFake: FakePlayer? = null
|
||||
private var singlePlayer: Player? = null
|
||||
private var craftingInventory: CraftingInventory? = null
|
||||
|
||||
private val ocm = Bukkit.getPluginManager().getPlugin("OldCombatMechanics") as? OCMMain
|
||||
private val module = ModuleGoldenApple.getInstance()
|
||||
|
||||
fun start() {
|
||||
if (ocm == null || !ocm.isEnabled) {
|
||||
fail("OldCombatMechanics plugin is not enabled.")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
schedule("sanity") {
|
||||
assertTrue(WeaponDamages.getDamage(Material.STONE_SWORD) > 0, "Weapon damages loaded")
|
||||
}
|
||||
|
||||
spawnMeleePlayers()
|
||||
schedule("melee attack") { runMeleeAttack() }
|
||||
schedule(1, "melee assert") { assertMeleeAttack() }
|
||||
schedule("remove melee players") { removeMeleePlayers() }
|
||||
|
||||
spawnSinglePlayer()
|
||||
schedule("sword blocking") { runSwordBlockingTest() }
|
||||
|
||||
addGoldenAppleTests()
|
||||
|
||||
schedule("cleanup single player") { removeSinglePlayer() }
|
||||
schedule("finish") { finish() }
|
||||
}
|
||||
|
||||
private fun schedule(name: String, action: () -> Unit) = schedule(0, name, action)
|
||||
|
||||
private fun schedule(delay: Long, name: String, action: () -> Unit) {
|
||||
delayTicks += delay
|
||||
Bukkit.getScheduler().runTaskLater(plugin, Runnable {
|
||||
try {
|
||||
action()
|
||||
} catch (e: Throwable) {
|
||||
fail("$name threw ${e.javaClass.simpleName}: ${e.message}")
|
||||
}
|
||||
}, delayTicks)
|
||||
}
|
||||
|
||||
private fun finish() {
|
||||
val success = failures == 0
|
||||
plugin.logger.info("Legacy test summary: passed ${total - failures}/$total")
|
||||
TestResultWriter.writeAndShutdown(plugin, success)
|
||||
}
|
||||
|
||||
private fun assertTrue(condition: Boolean, message: String) {
|
||||
total++
|
||||
if (condition) {
|
||||
plugin.logger.info("PASS: $message")
|
||||
} else {
|
||||
fail(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fail(message: String) {
|
||||
failures++
|
||||
plugin.logger.severe("FAIL: $message")
|
||||
}
|
||||
|
||||
private fun spawnMeleePlayers() {
|
||||
val world = Bukkit.getServer().getWorld("world")
|
||||
if (world == null) {
|
||||
fail("World 'world' is not loaded.")
|
||||
return
|
||||
}
|
||||
val baseLocation = Location(world, 0.0, 100.0, 0.0)
|
||||
meleeAttacker = FakePlayer(plugin).also { it.spawn(baseLocation.clone().add(2.0, 0.0, 0.0)) }
|
||||
meleeDefender = FakePlayer(plugin).also { it.spawn(baseLocation.clone().add(0.0, 0.0, 2.0)) }
|
||||
meleeAttackerPlayer = Bukkit.getPlayer(meleeAttacker!!.uuid)
|
||||
meleeDefenderPlayer = Bukkit.getPlayer(meleeDefender!!.uuid)
|
||||
if (meleeAttackerPlayer == null || meleeDefenderPlayer == null) {
|
||||
fail("Failed to resolve melee players.")
|
||||
return
|
||||
}
|
||||
preparePlayers(meleeAttackerPlayer!!, meleeDefenderPlayer!!)
|
||||
applyModesets(meleeAttackerPlayer!!, meleeDefenderPlayer!!)
|
||||
}
|
||||
|
||||
private fun runMeleeAttack() {
|
||||
val attacker = meleeAttackerPlayer ?: return
|
||||
val defender = meleeDefenderPlayer ?: return
|
||||
attacker.inventory.setItemInMainHand(ItemStack(Material.STONE_SWORD))
|
||||
defender.maximumNoDamageTicks = 0
|
||||
val attackMethod = attacker.javaClass.methods.firstOrNull { method ->
|
||||
method.name == "attack" &&
|
||||
method.parameterTypes.size == 1 &&
|
||||
method.parameterTypes[0].isAssignableFrom(defender.javaClass)
|
||||
}
|
||||
if (attackMethod != null) {
|
||||
attackMethod.invoke(attacker, defender)
|
||||
} else {
|
||||
defender.damage(1.0, attacker)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertMeleeAttack() {
|
||||
val attacker = meleeAttackerPlayer ?: return
|
||||
@Suppress("DEPRECATION") // Deprecated API kept for older server compatibility in tests.
|
||||
assertTrue(attacker.health == attacker.maxHealth, "Melee attack does not hurt attacker")
|
||||
}
|
||||
|
||||
private fun removeMeleePlayers() {
|
||||
meleeAttacker?.removePlayer()
|
||||
meleeDefender?.removePlayer()
|
||||
meleeAttacker = null
|
||||
meleeDefender = null
|
||||
meleeAttackerPlayer = null
|
||||
meleeDefenderPlayer = null
|
||||
}
|
||||
|
||||
private fun spawnSinglePlayer() {
|
||||
val world = Bukkit.getServer().getWorld("world")
|
||||
if (world == null) {
|
||||
fail("World 'world' is not loaded.")
|
||||
return
|
||||
}
|
||||
val location = Location(world, 0.0, 100.0, 0.0)
|
||||
singleFake = FakePlayer(plugin).also { it.spawn(location) }
|
||||
singlePlayer = Bukkit.getPlayer(singleFake!!.uuid)
|
||||
if (singlePlayer == null) {
|
||||
fail("Failed to resolve single player.")
|
||||
return
|
||||
}
|
||||
preparePlayers(singlePlayer!!)
|
||||
singlePlayer!!.isOp = true
|
||||
}
|
||||
|
||||
private fun removeSinglePlayer() {
|
||||
singleFake?.removePlayer()
|
||||
singleFake = null
|
||||
singlePlayer = null
|
||||
}
|
||||
|
||||
private fun runSwordBlockingTest() {
|
||||
val player = singlePlayer ?: return
|
||||
resetPlayerState(player)
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.DIAMOND_SWORD))
|
||||
callInteractEvent(player, Action.RIGHT_CLICK_AIR, player.inventory.itemInMainHand)
|
||||
assertTrue(player.inventory.itemInOffHand.type == Material.SHIELD, "Sword blocking: shield in offhand")
|
||||
singleFake?.doBlocking()
|
||||
assertTrue(player.isBlocking || player.isHandRaised, "Sword blocking: player blocking or hand raised")
|
||||
}
|
||||
|
||||
private fun addGoldenAppleTests() {
|
||||
val player = singlePlayer ?: return
|
||||
val snapshot = snapshotConfig()
|
||||
val modesetSnapshot = snapshotModeset("new")
|
||||
removeModuleFromModeset("new", "old-golden-apples")
|
||||
|
||||
schedule("gapple effects setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
}
|
||||
schedule(2, "gapple effects assert") {
|
||||
assertPotion(player.getPotionEffect(PotionEffectType.REGENERATION), 5 * 20, 1, "Gapple regen")
|
||||
assertPotion(player.getPotionEffect(PotionEffectType.ABSORPTION), 120 * 20, 0, "Gapple absorption")
|
||||
}
|
||||
|
||||
schedule("napple effects setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
player.inventory.setItemInMainHand(ENCHANTED_GOLDEN_APPLE.newInstance())
|
||||
callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
}
|
||||
schedule(2, "napple effects assert") {
|
||||
assertPotion(player.getPotionEffect(PotionEffectType.REGENERATION), 30 * 20, 4, "Napple regen")
|
||||
assertPotion(player.getPotionEffect(PotionEffectType.ABSORPTION), 120 * 20, 0, "Napple absorption")
|
||||
assertPotion(player.getPotionEffect(PotionEffectTypeCompat.RESISTANCE.get()), 300 * 20, 0, "Napple resistance")
|
||||
assertPotion(player.getPotionEffect(PotionEffectType.FIRE_RESISTANCE), 300 * 20, 0, "Napple fire resistance")
|
||||
}
|
||||
|
||||
schedule("higher amplifier setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
player.addPotionEffect(PotionEffect(PotionEffectType.REGENERATION, 100, 0))
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
}
|
||||
schedule(4, "higher amplifier assert") {
|
||||
assertPotion(player.getPotionEffect(PotionEffectType.REGENERATION), 5 * 20, 1, "Higher amplifier regen")
|
||||
}
|
||||
|
||||
schedule("same amplifier setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
player.addPotionEffect(PotionEffect(PotionEffectType.REGENERATION, 50, 1))
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
}
|
||||
schedule(4, "same amplifier assert") {
|
||||
assertPotion(player.getPotionEffect(PotionEffectType.REGENERATION), 5 * 20, 1, "Same amplifier regen")
|
||||
}
|
||||
|
||||
schedule("cooldown normal setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.cooldown.normal", 2)
|
||||
module.reload()
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
val event1 = callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
assertTrue(!event1.isCancelled, "Gapple cooldown: first consume allowed")
|
||||
val event2 = callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
assertTrue(event2.isCancelled, "Gapple cooldown: second consume blocked")
|
||||
}
|
||||
schedule(secondsToTicks(2) + 2, "cooldown normal assert") {
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
val event3 = callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
assertTrue(!event3.isCancelled, "Gapple cooldown: expires")
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule("shared cooldown setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.cooldown.normal", 5)
|
||||
ocm.config.set("old-golden-apples.cooldown.enchanted", 10)
|
||||
ocm.config.set("old-golden-apples.cooldown.is-shared", true)
|
||||
module.reload()
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
val gappleEvent = callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
assertTrue(!gappleEvent.isCancelled, "Shared cooldown: gapple allowed")
|
||||
player.inventory.setItemInMainHand(ENCHANTED_GOLDEN_APPLE.newInstance())
|
||||
val nappleEvent = callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
assertTrue(nappleEvent.isCancelled, "Shared cooldown: napple blocked")
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule("non-shared cooldown setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.cooldown.normal", 5)
|
||||
ocm.config.set("old-golden-apples.cooldown.enchanted", 10)
|
||||
ocm.config.set("old-golden-apples.cooldown.is-shared", false)
|
||||
module.reload()
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
val gappleEvent = callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
assertTrue(!gappleEvent.isCancelled, "Non-shared cooldown: gapple allowed")
|
||||
player.inventory.setItemInMainHand(ENCHANTED_GOLDEN_APPLE.newInstance())
|
||||
val nappleEvent = callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
assertTrue(!nappleEvent.isCancelled, "Non-shared cooldown: napple allowed")
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule(2, "module disabled via modeset setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.golden-apple-effects.regeneration.duration", 99)
|
||||
module.reload()
|
||||
setModeset(player, "new")
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
callConsumeEvent(player, player.inventory.itemInMainHand, applyVanillaEffects = true)
|
||||
}
|
||||
schedule(2, "module disabled via modeset assert") {
|
||||
assertPotion(player.getPotionEffect(PotionEffectType.REGENERATION), 5 * 20, 1, "Modeset disables module")
|
||||
setModeset(player, "old")
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule("old-potion-effects disabled setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.old-potion-effects", false)
|
||||
ocm.config.set("old-golden-apples.golden-apple-effects.regeneration.duration", 99)
|
||||
module.reload()
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
callConsumeEvent(player, player.inventory.itemInMainHand, applyVanillaEffects = true)
|
||||
}
|
||||
schedule(2, "old-potion-effects disabled assert") {
|
||||
assertPotion(player.getPotionEffect(PotionEffectType.REGENERATION), 5 * 20, 1, "Old potion effects disabled")
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule("crafting enabled setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.enchanted-golden-apple-crafting", true)
|
||||
module.reload()
|
||||
setModeset(player, "old")
|
||||
craftingInventory = openCraftingInventory(player)
|
||||
if (craftingInventory == null) {
|
||||
fail("Crafting inventory not available")
|
||||
return@schedule
|
||||
}
|
||||
prepareCraftingGrid(craftingInventory!!)
|
||||
}
|
||||
schedule(2, "crafting enabled assert") {
|
||||
val inv = craftingInventory
|
||||
if (inv == null) {
|
||||
fail("Crafting inventory missing for enabled assert")
|
||||
} else {
|
||||
assertTrue(inv.result != null && ENCHANTED_GOLDEN_APPLE.isSame(inv.result!!), "Crafting enabled")
|
||||
}
|
||||
player.closeInventory()
|
||||
craftingInventory = null
|
||||
}
|
||||
|
||||
schedule("crafting disabled config setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.enchanted-golden-apple-crafting", false)
|
||||
module.reload()
|
||||
setModeset(player, "old")
|
||||
craftingInventory = openCraftingInventory(player)
|
||||
if (craftingInventory == null) {
|
||||
fail("Crafting inventory not available")
|
||||
return@schedule
|
||||
}
|
||||
prepareCraftingGrid(craftingInventory!!)
|
||||
}
|
||||
schedule(2, "crafting disabled config assert") {
|
||||
val inv = craftingInventory
|
||||
if (inv == null) {
|
||||
fail("Crafting inventory missing for disabled config assert")
|
||||
} else {
|
||||
assertTrue(inv.result == null, "Crafting disabled in config")
|
||||
}
|
||||
player.closeInventory()
|
||||
craftingInventory = null
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule("crafting disabled modeset setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.enchanted-golden-apple-crafting", true)
|
||||
module.reload()
|
||||
setModeset(player, "new")
|
||||
craftingInventory = openCraftingInventory(player)
|
||||
if (craftingInventory == null) {
|
||||
fail("Crafting inventory not available")
|
||||
return@schedule
|
||||
}
|
||||
prepareCraftingGrid(craftingInventory!!)
|
||||
}
|
||||
schedule(2, "crafting disabled modeset assert") {
|
||||
val inv = craftingInventory
|
||||
if (inv == null) {
|
||||
fail("Crafting inventory missing for disabled modeset assert")
|
||||
} else {
|
||||
assertTrue(inv.result == null, "Crafting disabled by modeset")
|
||||
}
|
||||
player.closeInventory()
|
||||
craftingInventory = null
|
||||
setModeset(player, "old")
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule("no-conflict-mode setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.enchanted-golden-apple-crafting", true)
|
||||
ocm.config.set("old-golden-apples.no-conflict-mode", true)
|
||||
module.reload()
|
||||
setModeset(player, "new")
|
||||
craftingInventory = openCraftingInventory(player)
|
||||
if (craftingInventory == null) {
|
||||
fail("Crafting inventory not available")
|
||||
return@schedule
|
||||
}
|
||||
prepareCraftingGrid(craftingInventory!!)
|
||||
}
|
||||
schedule(2, "no-conflict-mode assert") {
|
||||
val inv = craftingInventory
|
||||
if (inv == null) {
|
||||
fail("Crafting inventory missing for no-conflict assert")
|
||||
} else {
|
||||
assertTrue(inv.result != null && ENCHANTED_GOLDEN_APPLE.isSame(inv.result!!), "No-conflict mode allows crafting")
|
||||
}
|
||||
player.closeInventory()
|
||||
craftingInventory = null
|
||||
setModeset(player, "old")
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule("getGappleCooldown setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.cooldown.normal", 10)
|
||||
module.reload()
|
||||
player.inventory.setItemInMainHand(ItemStack(Material.GOLDEN_APPLE))
|
||||
callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
}
|
||||
schedule(2, "getGappleCooldown assert 1") {
|
||||
val cd = module.getGappleCooldown(player.uniqueId)
|
||||
assertTrue(cd in 9..10, "getGappleCooldown initial")
|
||||
}
|
||||
schedule(secondsToTicks(5), "getGappleCooldown assert 2") {
|
||||
val cd = module.getGappleCooldown(player.uniqueId)
|
||||
assertTrue(cd in 4..5, "getGappleCooldown mid")
|
||||
}
|
||||
schedule(secondsToTicks(5), "getGappleCooldown assert 3") {
|
||||
val cd = module.getGappleCooldown(player.uniqueId)
|
||||
assertTrue(cd == 0L, "getGappleCooldown end")
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule("getNappleCooldown setup") {
|
||||
resetPlayerState(player)
|
||||
applyConfig(snapshot)
|
||||
ocm!!.config.set("old-golden-apples.cooldown.enchanted", 20)
|
||||
module.reload()
|
||||
player.inventory.setItemInMainHand(ENCHANTED_GOLDEN_APPLE.newInstance())
|
||||
callConsumeEvent(player, player.inventory.itemInMainHand)
|
||||
}
|
||||
schedule(2, "getNappleCooldown assert 1") {
|
||||
val cd = module.getNappleCooldown(player.uniqueId)
|
||||
assertTrue(cd in 19..20, "getNappleCooldown initial")
|
||||
}
|
||||
schedule(secondsToTicks(10), "getNappleCooldown assert 2") {
|
||||
val cd = module.getNappleCooldown(player.uniqueId)
|
||||
assertTrue(cd in 9..10, "getNappleCooldown mid")
|
||||
}
|
||||
schedule(secondsToTicks(10), "getNappleCooldown assert 3") {
|
||||
val cd = module.getNappleCooldown(player.uniqueId)
|
||||
assertTrue(cd == 0L, "getNappleCooldown end")
|
||||
applyConfig(snapshot)
|
||||
}
|
||||
|
||||
schedule("restore modesets") {
|
||||
restoreModeset("new", modesetSnapshot)
|
||||
}
|
||||
}
|
||||
|
||||
private fun snapshotConfig(): GappleConfigSnapshot {
|
||||
val config = ocm!!.config
|
||||
return GappleConfigSnapshot(
|
||||
config.getBoolean("old-golden-apples.old-potion-effects"),
|
||||
config.getLong("old-golden-apples.cooldown.normal"),
|
||||
config.getLong("old-golden-apples.cooldown.enchanted"),
|
||||
config.getBoolean("old-golden-apples.cooldown.is-shared"),
|
||||
config.getBoolean("old-golden-apples.enchanted-golden-apple-crafting"),
|
||||
config.getBoolean("old-golden-apples.no-conflict-mode"),
|
||||
config.getInt("old-golden-apples.golden-apple-effects.regeneration.duration")
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyConfig(snapshot: GappleConfigSnapshot) {
|
||||
val config = ocm!!.config
|
||||
config.set("old-golden-apples.old-potion-effects", snapshot.oldPotionEffects)
|
||||
config.set("old-golden-apples.cooldown.normal", snapshot.normalCooldown)
|
||||
config.set("old-golden-apples.cooldown.enchanted", snapshot.enchantedCooldown)
|
||||
config.set("old-golden-apples.cooldown.is-shared", snapshot.sharedCooldown)
|
||||
config.set("old-golden-apples.enchanted-golden-apple-crafting", snapshot.craftingEnabled)
|
||||
config.set("old-golden-apples.no-conflict-mode", snapshot.noConflictMode)
|
||||
config.set("old-golden-apples.golden-apple-effects.regeneration.duration", snapshot.regenDuration)
|
||||
module.reload()
|
||||
}
|
||||
|
||||
private fun resetPlayerState(player: Player) {
|
||||
player.inventory.clear()
|
||||
player.activePotionEffects.forEach { player.removePotionEffect(it.type) }
|
||||
val maxAttribute = resolveMaxHealthAttribute()
|
||||
val maxHealth = if (maxAttribute != null) {
|
||||
player.getAttribute(maxAttribute)?.value ?: 20.0
|
||||
} else {
|
||||
20.0
|
||||
}
|
||||
player.health = maxHealth
|
||||
player.foodLevel = 20
|
||||
setModeset(player, "old")
|
||||
player.isInvulnerable = false
|
||||
}
|
||||
|
||||
private fun resolveMaxHealthAttribute(): Attribute? {
|
||||
return runCatching { Attribute.valueOf("MAX_HEALTH") }
|
||||
.getOrElse { runCatching { Attribute.valueOf("GENERIC_MAX_HEALTH") }.getOrNull() }
|
||||
}
|
||||
|
||||
private fun preparePlayers(vararg players: Player) {
|
||||
for (player in players) {
|
||||
player.gameMode = GameMode.SURVIVAL
|
||||
player.maximumNoDamageTicks = 20
|
||||
player.noDamageTicks = 0
|
||||
player.isInvulnerable = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyModesets(attacker: Player, defender: Player) {
|
||||
var playerData = getPlayerData(attacker.uniqueId)
|
||||
playerData.setModesetForWorld(attacker.world.uid, "old")
|
||||
setPlayerData(attacker.uniqueId, playerData)
|
||||
|
||||
playerData = getPlayerData(defender.uniqueId)
|
||||
playerData.setModesetForWorld(defender.world.uid, "new")
|
||||
setPlayerData(defender.uniqueId, playerData)
|
||||
}
|
||||
|
||||
private fun setModeset(player: Player, modeset: String) {
|
||||
val playerData = getPlayerData(player.uniqueId)
|
||||
playerData.setModesetForWorld(player.world.uid, modeset)
|
||||
setPlayerData(player.uniqueId, playerData)
|
||||
}
|
||||
|
||||
private fun snapshotModeset(name: String): Set<String> {
|
||||
val modesets = Config.getModesets()
|
||||
val existing = modesets[name] ?: emptySet()
|
||||
return HashSet(existing)
|
||||
}
|
||||
|
||||
private fun removeModuleFromModeset(name: String, moduleName: String) {
|
||||
val modesets = Config.getModesets()
|
||||
val set = modesets[name] ?: return
|
||||
set.remove(moduleName)
|
||||
}
|
||||
|
||||
private fun restoreModeset(name: String, snapshot: Set<String>) {
|
||||
val modesets = Config.getModesets()
|
||||
val set = modesets[name] ?: return
|
||||
set.clear()
|
||||
set.addAll(snapshot)
|
||||
}
|
||||
|
||||
private fun assertPotion(effect: PotionEffect?, duration: Int, amplifier: Int, label: String) {
|
||||
assertTrue(effect != null, "$label present")
|
||||
if (effect != null) {
|
||||
val minDuration = duration - 3
|
||||
assertTrue(effect.duration in minDuration..duration, "$label duration (got ${effect.duration})")
|
||||
assertTrue(effect.amplifier == amplifier, "$label amplifier (got ${effect.amplifier})")
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareCraftingGrid(inventory: CraftingInventory) {
|
||||
inventory.matrix = arrayOf(
|
||||
ItemStack(Material.GOLD_BLOCK), ItemStack(Material.GOLD_BLOCK), ItemStack(Material.GOLD_BLOCK),
|
||||
ItemStack(Material.GOLD_BLOCK), ItemStack(Material.APPLE), ItemStack(Material.GOLD_BLOCK),
|
||||
ItemStack(Material.GOLD_BLOCK), ItemStack(Material.GOLD_BLOCK), ItemStack(Material.GOLD_BLOCK)
|
||||
)
|
||||
}
|
||||
|
||||
private fun openCraftingInventory(player: Player): CraftingInventory? {
|
||||
return runCatching {
|
||||
val method = player.javaClass.methods.firstOrNull { candidate ->
|
||||
candidate.name == "openWorkbench" && candidate.parameterTypes.size == 2
|
||||
} ?: return null
|
||||
val view = method.invoke(player, null, true) ?: return null
|
||||
val topInventory = view.javaClass.getMethod("getTopInventory").invoke(view)
|
||||
topInventory as? CraftingInventory
|
||||
}.onFailure { throwable ->
|
||||
plugin.logger.log(Level.WARNING, "Failed to open crafting inventory: ${throwable.message}")
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun applyVanillaEffects(player: Player, item: ItemStack) {
|
||||
val isEnchanted = ENCHANTED_GOLDEN_APPLE.isSame(item)
|
||||
if (item.type != Material.GOLDEN_APPLE && !isEnchanted) return
|
||||
|
||||
if (isEnchanted) {
|
||||
player.addPotionEffect(PotionEffect(PotionEffectType.REGENERATION, 30 * 20, 1))
|
||||
player.addPotionEffect(PotionEffect(PotionEffectType.ABSORPTION, 120 * 20, 3))
|
||||
player.addPotionEffect(PotionEffect(PotionEffectTypeCompat.RESISTANCE.get(), 300 * 20, 0))
|
||||
player.addPotionEffect(PotionEffect(PotionEffectType.FIRE_RESISTANCE, 300 * 20, 0))
|
||||
} else {
|
||||
player.addPotionEffect(PotionEffect(PotionEffectType.REGENERATION, 5 * 20, 1))
|
||||
player.addPotionEffect(PotionEffect(PotionEffectType.ABSORPTION, 120 * 20, 0))
|
||||
}
|
||||
}
|
||||
|
||||
private fun callConsumeEvent(
|
||||
player: Player,
|
||||
item: ItemStack,
|
||||
applyVanillaEffects: Boolean = false
|
||||
): PlayerItemConsumeEvent {
|
||||
val ctor = findConsumeCtor()
|
||||
val event = if (ctor.parameterTypes.size == 3) {
|
||||
ctor.newInstance(player, item, EquipmentSlot.HAND) as PlayerItemConsumeEvent
|
||||
} else {
|
||||
ctor.newInstance(player, item) as PlayerItemConsumeEvent
|
||||
}
|
||||
Bukkit.getPluginManager().callEvent(event)
|
||||
if (!event.isCancelled && applyVanillaEffects) {
|
||||
applyVanillaEffects(player, item)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
private fun callInteractEvent(player: Player, action: Action, item: ItemStack): PlayerInteractEvent {
|
||||
val ctor = findInteractCtor()
|
||||
val event = if (ctor.parameterTypes.size == 6) {
|
||||
ctor.newInstance(player, action, item, null, BlockFace.SELF, EquipmentSlot.HAND) as PlayerInteractEvent
|
||||
} else {
|
||||
ctor.newInstance(player, action, item, null, BlockFace.SELF) as PlayerInteractEvent
|
||||
}
|
||||
Bukkit.getPluginManager().callEvent(event)
|
||||
return event
|
||||
}
|
||||
|
||||
private fun findConsumeCtor(): Constructor<*> {
|
||||
val ctors = PlayerItemConsumeEvent::class.java.constructors
|
||||
return ctors.firstOrNull { it.parameterTypes.size == 3 }
|
||||
?: ctors.firstOrNull { it.parameterTypes.size == 2 }
|
||||
?: ctors.first()
|
||||
}
|
||||
|
||||
private fun findInteractCtor(): Constructor<*> {
|
||||
val ctors = PlayerInteractEvent::class.java.constructors
|
||||
return ctors.firstOrNull { it.parameterTypes.size == 6 }
|
||||
?: ctors.firstOrNull { it.parameterTypes.size == 5 }
|
||||
?: ctors.first()
|
||||
}
|
||||
|
||||
private fun secondsToTicks(seconds: Long): Long = seconds * 20
|
||||
}
|
||||
|
||||
private data class GappleConfigSnapshot(
|
||||
val oldPotionEffects: Boolean,
|
||||
val normalCooldown: Long,
|
||||
val enchantedCooldown: Long,
|
||||
val sharedCooldown: Boolean,
|
||||
val craftingEnabled: Boolean,
|
||||
val noConflictMode: Boolean,
|
||||
val regenDuration: Int
|
||||
)
|
||||
}
|
||||
@@ -6,115 +6,50 @@
|
||||
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import io.kotest.common.KotestInternal
|
||||
import io.kotest.core.config.AbstractProjectConfig
|
||||
import io.kotest.core.extensions.TestCaseExtension
|
||||
import io.kotest.core.spec.IsolationMode
|
||||
import io.kotest.core.test.TestCase
|
||||
import io.kotest.engine.TestEngineLauncher
|
||||
import io.kotest.engine.concurrency.SpecExecutionMode
|
||||
import io.kotest.engine.concurrency.TestExecutionMode
|
||||
import io.kotest.engine.listener.AbstractTestEngineListener
|
||||
import io.kotest.engine.test.TestResult
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import java.io.File
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
object KotestProjectConfig : AbstractProjectConfig() {
|
||||
override val isolationMode = IsolationMode.SingleInstance
|
||||
override val specExecutionMode = SpecExecutionMode.Sequential
|
||||
override val testExecutionMode = TestExecutionMode.Sequential
|
||||
}
|
||||
|
||||
|
||||
class BukkitMainThreadDispatcher(private val plugin: JavaPlugin) : CoroutineDispatcher() {
|
||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||
Bukkit.getScheduler().runTask(plugin, block)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MainThreadDispatcherExtension(private val plugin: JavaPlugin) : TestCaseExtension {
|
||||
override suspend fun intercept(testCase: TestCase, execute: suspend (TestCase) -> TestResult): TestResult {
|
||||
val dispatcher = BukkitMainThreadDispatcher(plugin)
|
||||
val newContext = coroutineContext + dispatcher
|
||||
return withContext(newContext) {
|
||||
execute(testCase)
|
||||
}
|
||||
}
|
||||
}
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.logging.Level
|
||||
|
||||
class OCMTestMain : JavaPlugin() {
|
||||
@OptIn(KotestInternal::class)
|
||||
override fun onEnable() {
|
||||
println("Enabled OCMTest plugin")
|
||||
logger.info("Enabled OCMTest plugin")
|
||||
|
||||
// Initialise player data storage
|
||||
val ocm = Bukkit.getPluginManager().getPlugin("OldCombatMechanics") as OCMMain
|
||||
kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.initialise(ocm)
|
||||
PlayerStorage.initialise(ocm)
|
||||
|
||||
// Schedule test asynchronously to avoid deadlock
|
||||
// This is because each test is run as a coroutine
|
||||
Bukkit.getScheduler().runTaskAsynchronously(this, Runnable {
|
||||
// Implement a test listener to capture test results
|
||||
var hasFailures = false
|
||||
val javaVersion = detectJavaVersion()
|
||||
logger.info("Detected Java $javaVersion for integration tests")
|
||||
|
||||
val listener = object : AbstractTestEngineListener() {
|
||||
// Override testFinished to capture each test result
|
||||
override suspend fun testFinished(testCase: TestCase, result: TestResult) {
|
||||
val testName = testCase.name.name
|
||||
when {
|
||||
result.isSuccess -> println("Test '$testName' passed.")
|
||||
result.isIgnored -> println("Test '$testName' was ignored.")
|
||||
result.isFailure || result.isError -> {
|
||||
val message = result.errorOrNull?.message ?: result.reasonOrNull ?: "Unknown failure"
|
||||
println("Test '$testName' failed with exception: $message")
|
||||
hasFailures = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (javaVersion >= 11) {
|
||||
runKotest()
|
||||
} else {
|
||||
LegacyTestRunner.run(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally override testStarted if you want to log when a test starts
|
||||
override suspend fun testStarted(testCase: TestCase) {
|
||||
println("Starting test '${testCase.name.name}'")
|
||||
}
|
||||
private fun runKotest() {
|
||||
try {
|
||||
val runnerClass = Class.forName("kernitus.plugin.OldCombatMechanics.KotestRunner")
|
||||
val runMethod = runnerClass.getMethod("run", JavaPlugin::class.java)
|
||||
runMethod.invoke(null, this)
|
||||
} catch (e: InvocationTargetException) {
|
||||
logger.log(Level.SEVERE, "Failed to launch Kotest runner.", e.targetException ?: e)
|
||||
TestResultWriter.writeAndShutdown(this, false)
|
||||
} catch (e: Throwable) {
|
||||
logger.log(Level.SEVERE, "Failed to launch Kotest runner.", e)
|
||||
TestResultWriter.writeAndShutdown(this, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Override engineFinished to print a summary after all tests
|
||||
override suspend fun engineFinished(t: List<Throwable>) {
|
||||
val success = t.isEmpty() && !hasFailures
|
||||
if (success) {
|
||||
println("All tests passed successfully.")
|
||||
} else {
|
||||
println("Test run completed with failures.")
|
||||
t.forEach { throwable ->
|
||||
println("Engine error: ${throwable.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Write the result to a file for the Gradle build to check
|
||||
try {
|
||||
val resultFile = File(this@OCMTestMain.dataFolder, "test-results.txt")
|
||||
resultFile.parentFile.mkdirs() // Ensure the directory exists
|
||||
resultFile.writeText(if (success) "PASS" else "FAIL")
|
||||
println("Test result written to ${resultFile.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
println("Failed to write test results file: ${e.message}")
|
||||
}
|
||||
|
||||
Bukkit.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests using TestEngineLauncher with Spec instances
|
||||
TestEngineLauncher()
|
||||
.withListener(listener)
|
||||
.withProjectConfig(KotestProjectConfig)
|
||||
.withClasses(InGameTesterIntegrationTest::class)
|
||||
.launch()
|
||||
})
|
||||
private fun detectJavaVersion(): Int {
|
||||
val version = System.getProperty("java.specification.version") ?: return 0
|
||||
return if (version.startsWith("1.")) {
|
||||
version.substringAfter("1.").toIntOrNull() ?: 0
|
||||
} else {
|
||||
version.toIntOrNull() ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import java.io.File
|
||||
import java.util.logging.Level
|
||||
|
||||
object TestResultWriter {
|
||||
@JvmStatic
|
||||
fun writeAndShutdown(plugin: JavaPlugin, success: Boolean, error: Throwable? = null) {
|
||||
try {
|
||||
val resultFile = File(plugin.dataFolder, "test-results.txt")
|
||||
resultFile.parentFile.mkdirs()
|
||||
resultFile.writeText(if (success) "PASS" else "FAIL")
|
||||
plugin.logger.info("Test result written to ${resultFile.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
plugin.logger.log(Level.SEVERE, "Failed to write test results file.", e)
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
plugin.logger.log(Level.SEVERE, "Integration tests failed.", error)
|
||||
}
|
||||
|
||||
Bukkit.shutdown()
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,9 @@ public class ModuleFishingKnockback extends OCMModule {
|
||||
|
||||
final FishHook hook = (FishHook) hookEntity;
|
||||
|
||||
if (!(hook.getShooter() instanceof Player rodder))
|
||||
if (!(hook.getShooter() instanceof Player))
|
||||
return;
|
||||
final Player rodder = (Player) hook.getShooter();
|
||||
if (!isEnabled(rodder))
|
||||
return;
|
||||
|
||||
@@ -66,8 +67,9 @@ public class ModuleFishingKnockback extends OCMModule {
|
||||
|
||||
if (hitEntity == null)
|
||||
return; // If no entity was hit
|
||||
if (!(hitEntity instanceof LivingEntity livingEntity))
|
||||
if (!(hitEntity instanceof LivingEntity))
|
||||
return;
|
||||
final LivingEntity livingEntity = (LivingEntity) hitEntity;
|
||||
if (!knockbackNonPlayerEntities && !(hitEntity instanceof Player))
|
||||
return;
|
||||
|
||||
|
||||
@@ -110,7 +110,9 @@ public class ModuleGoldenApple extends OCMModule {
|
||||
// something up.
|
||||
|
||||
if (ENCHANTED_GOLDEN_APPLE.isSame(item)) {
|
||||
final HumanEntity player = e.getView().getPlayer();
|
||||
final List<HumanEntity> viewers = e.getInventory().getViewers();
|
||||
if (viewers.isEmpty()) return;
|
||||
final HumanEntity player = viewers.get(0);
|
||||
|
||||
if (isSettingEnabled("no-conflict-mode"))
|
||||
return;
|
||||
@@ -120,6 +122,8 @@ public class ModuleGoldenApple extends OCMModule {
|
||||
}
|
||||
}
|
||||
|
||||
// Intentionally avoid InventoryView#getPlayer() to prevent class/interface mismatch on 1.12.
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onItemConsume(PlayerItemConsumeEvent e) {
|
||||
final Player player = e.getPlayer();
|
||||
@@ -197,6 +201,8 @@ public class ModuleGoldenApple extends OCMModule {
|
||||
}
|
||||
|
||||
private void applyEffects(LivingEntity target, List<PotionEffect> newEffects) {
|
||||
final boolean forceOverride = !kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector
|
||||
.versionIsNewerOrEqualTo(1, 13, 0);
|
||||
for (PotionEffect newEffect : newEffects) {
|
||||
// Find the existing effect of the same type with the highest amplifier
|
||||
final PotionEffect highestExistingEffect = target.getActivePotionEffects().stream()
|
||||
@@ -207,13 +213,21 @@ public class ModuleGoldenApple extends OCMModule {
|
||||
if (highestExistingEffect != null) {
|
||||
// If the new effect has a higher amplifier, apply it
|
||||
if (newEffect.getAmplifier() > highestExistingEffect.getAmplifier()) {
|
||||
target.addPotionEffect(newEffect);
|
||||
if (forceOverride) {
|
||||
target.addPotionEffect(newEffect, true);
|
||||
} else {
|
||||
target.addPotionEffect(newEffect);
|
||||
}
|
||||
}
|
||||
// If the amplifiers are the same and the new effect has a longer duration,
|
||||
// refresh the duration
|
||||
else if (newEffect.getAmplifier() == highestExistingEffect.getAmplifier() &&
|
||||
newEffect.getDuration() > highestExistingEffect.getDuration()) {
|
||||
target.addPotionEffect(newEffect);
|
||||
if (forceOverride) {
|
||||
target.addPotionEffect(newEffect, true);
|
||||
} else {
|
||||
target.addPotionEffect(newEffect);
|
||||
}
|
||||
}
|
||||
// If the new effect has a lower amplifier or shorter/equal duration, do nothing
|
||||
} else {
|
||||
@@ -311,12 +325,22 @@ public class ModuleGoldenApple extends OCMModule {
|
||||
}
|
||||
}
|
||||
|
||||
private record Cooldown(long normal, long enchanted, boolean sharedCooldown) {
|
||||
private static class Cooldown {
|
||||
private final long normal;
|
||||
private final long enchanted;
|
||||
private final boolean sharedCooldown;
|
||||
|
||||
private Cooldown(long normal, long enchanted, boolean sharedCooldown) {
|
||||
this.normal = normal;
|
||||
this.enchanted = enchanted;
|
||||
this.sharedCooldown = sharedCooldown;
|
||||
}
|
||||
|
||||
private long getCooldownForItem(ItemStack item) {
|
||||
return ENCHANTED_GOLDEN_APPLE.isSame(item) ? enchanted : normal;
|
||||
}
|
||||
|
||||
boolean isOnCooldown(ItemStack item, LastEaten lastEaten) {
|
||||
private boolean isOnCooldown(ItemStack item, LastEaten lastEaten) {
|
||||
return (sharedCooldown ? lastEaten.getNewestEatTime() : lastEaten.getForItem(item))
|
||||
.map(it -> ChronoUnit.SECONDS.between(it, Instant.now()))
|
||||
.map(it -> it < getCooldownForItem(item))
|
||||
|
||||
+3
-1
@@ -48,7 +48,9 @@ public class ModuleOldArmourDurability extends OCMModule {
|
||||
final List<ItemStack> armour = explosionDamaged.get(uuid);
|
||||
// ItemStack.equals() checks material, durability and quantity to make sure nothing changed in the meantime
|
||||
// We're checking all the pieces this way just in case they're wearing two helmets or something strange
|
||||
final List<ItemStack> matchedPieces = armour.stream().filter(piece -> piece.equals(item)).toList();
|
||||
final List<ItemStack> matchedPieces = armour.stream()
|
||||
.filter(piece -> piece.equals(item))
|
||||
.collect(Collectors.toList());
|
||||
armour.removeAll(matchedPieces);
|
||||
debug("Item matched explosion, ignoring...", player);
|
||||
if (!matchedPieces.isEmpty()) return;
|
||||
|
||||
+23
-15
@@ -26,28 +26,34 @@ import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.bukkit.potion.PotionType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* Allows configurable potion effect durations.
|
||||
*/
|
||||
public class ModuleOldPotionEffects extends OCMModule {
|
||||
private static final Set<PotionTypeCompat> EXCLUDED_POTION_TYPES = Set.of(
|
||||
// Base potions without any effect
|
||||
new PotionTypeCompat("AWKWARD"),
|
||||
new PotionTypeCompat("MUNDANE"),
|
||||
new PotionTypeCompat("THICK"),
|
||||
new PotionTypeCompat("WATER"),
|
||||
// Instant potions with no further effects
|
||||
new PotionTypeCompat("HARMING"),
|
||||
new PotionTypeCompat("STRONG_HARMING"),
|
||||
new PotionTypeCompat("HEALING"),
|
||||
new PotionTypeCompat("STRONG_HEALING"),
|
||||
// This type doesn't exist anymore >1.20.5, is specially handled in compat class
|
||||
new PotionTypeCompat("UNCRAFTABLE")
|
||||
private static final Set<PotionTypeCompat> EXCLUDED_POTION_TYPES = Collections.unmodifiableSet(
|
||||
new HashSet<>(Arrays.asList(
|
||||
// Base potions without any effect
|
||||
new PotionTypeCompat("AWKWARD"),
|
||||
new PotionTypeCompat("MUNDANE"),
|
||||
new PotionTypeCompat("THICK"),
|
||||
new PotionTypeCompat("WATER"),
|
||||
// Instant potions with no further effects
|
||||
new PotionTypeCompat("HARMING"),
|
||||
new PotionTypeCompat("STRONG_HARMING"),
|
||||
new PotionTypeCompat("HEALING"),
|
||||
new PotionTypeCompat("STRONG_HEALING"),
|
||||
// This type doesn't exist anymore >1.20.5, is specially handled in compat class
|
||||
new PotionTypeCompat("UNCRAFTABLE")
|
||||
))
|
||||
);
|
||||
|
||||
private Map<PotionTypeCompat, PotionDurations> durations;
|
||||
@@ -136,9 +142,11 @@ public class ModuleOldPotionEffects extends OCMModule {
|
||||
List<PotionEffectType> potionEffects;
|
||||
final PotionType potionType = potionTypeCompat.getType();
|
||||
try {
|
||||
potionEffects = potionType.getPotionEffects().stream().map(PotionEffect::getType).toList();
|
||||
potionEffects = potionType.getPotionEffects().stream()
|
||||
.map(PotionEffect::getType)
|
||||
.collect(Collectors.toList());
|
||||
} catch (NoSuchMethodError e) {
|
||||
potionEffects = List.of(potionType.getEffectType());
|
||||
potionEffects = Collections.singletonList(potionType.getEffectType());
|
||||
}
|
||||
|
||||
for (PotionEffectType effectType : potionEffects) {
|
||||
|
||||
@@ -157,15 +157,17 @@ public class ModuleSwordBlocking extends OCMModule {
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onInventoryClick(InventoryClickEvent e) {
|
||||
if (e.getWhoClicked() instanceof Player player) {
|
||||
if (areItemsStored(player.getUniqueId())) {
|
||||
final ItemStack cursor = e.getCursor();
|
||||
final ItemStack current = e.getCurrentItem();
|
||||
if (cursor != null && cursor.getType() == Material.SHIELD ||
|
||||
current != null && current.getType() == Material.SHIELD) {
|
||||
e.setCancelled(true);
|
||||
restore(player, true);
|
||||
}
|
||||
if (!(e.getWhoClicked() instanceof Player)) {
|
||||
return;
|
||||
}
|
||||
final Player player = (Player) e.getWhoClicked();
|
||||
if (areItemsStored(player.getUniqueId())) {
|
||||
final ItemStack cursor = e.getCursor();
|
||||
final ItemStack current = e.getCurrentItem();
|
||||
if (cursor != null && cursor.getType() == Material.SHIELD ||
|
||||
current != null && current.getType() == Material.SHIELD) {
|
||||
e.setCancelled(true);
|
||||
restore(player, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -8,5 +8,20 @@ package kernitus.plugin.OldCombatMechanics.utilities.potions;
|
||||
/**
|
||||
* Hold information on duration of drinkable & splash version of a potion type
|
||||
*/
|
||||
public record PotionDurations(int drinkable, int splash) {
|
||||
public final class PotionDurations {
|
||||
private final int drinkable;
|
||||
private final int splash;
|
||||
|
||||
public PotionDurations(int drinkable, int splash) {
|
||||
this.drinkable = drinkable;
|
||||
this.splash = splash;
|
||||
}
|
||||
|
||||
public int drinkable() {
|
||||
return drinkable;
|
||||
}
|
||||
|
||||
public int splash() {
|
||||
return splash;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -7,6 +7,7 @@
|
||||
package kernitus.plugin.OldCombatMechanics.utilities.potions;
|
||||
|
||||
import org.bukkit.inventory.meta.PotionMeta;
|
||||
import org.bukkit.potion.PotionData;
|
||||
import org.bukkit.potion.PotionType;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -170,7 +171,7 @@ public class PotionTypeCompat {
|
||||
return new PotionTypeCompat("UNCRAFTABLE");
|
||||
return new PotionTypeCompat(potionType.name());
|
||||
} catch (NoSuchMethodError e) {
|
||||
final var potionData = potionMeta.getBasePotionData();
|
||||
final PotionData potionData = potionMeta.getBasePotionData();
|
||||
final PotionType potionType = potionData.getType();
|
||||
return new PotionTypeCompat(potionType.name(), potionData.isUpgraded(), potionData.isExtended());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user