diff --git a/.github/workflows/Check.yml b/.github/workflows/Check.yml deleted file mode 100644 index a0a411e6..00000000 --- a/.github/workflows/Check.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Check - -on: - push: - paths-ignore: - - '**.md' - -concurrency: - group: check-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-22.04 - steps: - - name: Checkout project - uses: actions/checkout@v3 - - - name: Initialization - uses: ./.github/actions/init - with: - jdk: 17 - - - name: Build - uses: gradle/gradle-build-action@v2.4.0 - with: - arguments: build -x test - - - name: Test - uses: gradle/gradle-build-action@v2.4.0 - with: - arguments: test \ No newline at end of file diff --git a/.github/workflows/check-branch.yml b/.github/workflows/check-branch.yml new file mode 100644 index 00000000..5f3aa59e --- /dev/null +++ b/.github/workflows/check-branch.yml @@ -0,0 +1,34 @@ +name: Check + +on: + push: + branches-ignore: + - main + paths-ignore: + - '**.md' + +concurrency: + group: check-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout project + uses: actions/checkout@v3 + + - name: Initialization + uses: ./.github/actions/init + with: + jdk: 17 + + - name: Build + uses: gradle/gradle-build-action@v2.7.0 + with: + arguments: build -x test + + - name: Test + uses: gradle/gradle-build-action@v2.4.0 + with: + arguments: test diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 00000000..8f18b0b8 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,38 @@ +name: SonarCloud + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: sonar-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and analyze + runs-on: ubuntu-latest + steps: + - name: Checkout project + uses: actions/checkout@v3 + + - name: Initialization + uses: ./.github/actions/init + with: + jdk: 17 + + - name: Build + uses: gradle/gradle-build-action@v2.7.0 + env: + DETEKT_IGNORE_FAILURES: true + with: + arguments: build detekt test jacocoTestReport + + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.gitignore b/.gitignore index 3dcc4b43..98ada130 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # Created by https://www.toptal.com/developers/gitignore/api/intellij,eclipse,kotlin,gradle # Edit at https://www.toptal.com/developers/gitignore?templates=intellij,eclipse,kotlin,gradle @@ -103,14 +102,14 @@ local.properties # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr +.idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr # CMake cmake-build-*/ @@ -227,8 +226,4 @@ gradle-app.setting # End of https://www.toptal.com/developers/gitignore/api/intellij,eclipse,kotlin,gradle -dokka/ -# In case of someone execute the server in IDE -/*.conf -/world/ -/extensions/ \ No newline at end of file +dokka \ No newline at end of file diff --git a/LICENSE b/LICENSE index 5b6b5f31..8c14a885 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Rushyverse +Copyright (c) 2023 Rushyverse Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 21ef12ff..1ba73a78 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # API -This project allows to create a Minestom server easily in Kotlin. It provides a lot of features to create a Minecraft +This project allows creating a PaperMC based server easily in Kotlin. It provides a lot of features to create a Minecraft server. ## Usage -Check the [wiki](https://github.com/Rushyverse/api/wiki) to know how to use the project. \ No newline at end of file +Check the [wiki](https://github.com/Rushyverse/api/wiki) to know how to use the project. diff --git a/build.gradle.kts b/build.gradle.kts index e1fc7b48..0a4ec9ec 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,54 +1,112 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.8.10" - kotlin("plugin.serialization") version "1.8.10" - id("org.jetbrains.dokka") version "1.8.10" - `java-library` + embeddedKotlin("jvm") + embeddedKotlin("plugin.serialization") + id("org.jetbrains.dokka") version "1.8.20" + id("com.github.johnrengelman.shadow") version "8.1.1" + id("io.gitlab.arturbosch.detekt") version "1.23.1" `maven-publish` + `java-library` + jacoco +} + +detekt { + // Allows having different behavior for CI. + // When building a branch, we want to fail the build if detekt fails. + // When building a PR, we want to ignore failures to report them in sonar. + val envIgnoreFailures = System.getenv("DETEKT_IGNORE_FAILURES")?.toBooleanStrictOrNull() ?: false + ignoreFailures = envIgnoreFailures + + config.from(file("config/detekt/detekt.yml")) +} + +jacoco { + reportsDirectory.set(file("${layout.buildDirectory.get()}/reports/jacoco")) } repositories { mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.codemc.org/repository/maven-public/") maven("https://jitpack.io") } dependencies { - val minestomVersion = "aebf72de90" - val loggingVersion = "3.0.5" - val mockkVersion = "1.13.4" - val coroutinesVersion = "1.6.4" - val kotlinSerializationVersion = "1.5.0" - val commonsNetVersion = "3.9.0" - val icu4jVersion = "72.1" - val minimessageVersion = "4.13.0" - - api(kotlin("stdlib")) - api(kotlin("reflect")) - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") - api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") - api("org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinSerializationVersion") - api("org.jetbrains.kotlinx:kotlinx-serialization-hocon:$kotlinSerializationVersion") - - api("com.github.Minestom.Minestom:Minestom:$minestomVersion") - api("commons-net:commons-net:$commonsNetVersion") - api("com.ibm.icu:icu4j:$icu4jVersion") - api("net.kyori:adventure-text-minimessage:$minimessageVersion") - - // Logging information - api("io.github.microutils:kotlin-logging:$loggingVersion") + val kotlinSerializableVersion = "1.5.1" + val kamlVersion = "0.53.0" + val coroutineVersion = "1.6.4" + val loggingVersion = "2.1.23" + val koinVersion = "3.2.0" + val mccoroutineVersion = "2.4.0" + val paperVersion = "1.19-R0.1-SNAPSHOT" + val mockBukkitVersion = "3.18.0" + val junitVersion = "5.9.0" + val mockkVersion = "1.12.5" + val slf4jVersion = "2.0.0-alpha6" + val fastboardVersion = "2.0.0" + val kotestVersion = "5.6.2" + val icu4jVersion = "73.2" + + implementation(kotlin("stdlib")) + implementation(kotlin("stdlib-jdk8")) + implementation(kotlin("reflect")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinSerializableVersion") + implementation("com.charleskorn.kaml:kaml:$kamlVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutineVersion") + implementation("io.github.microutils:kotlin-logging:$loggingVersion") + + // Plural translation + implementation("com.ibm.icu:icu4j:$icu4jVersion") + + // Injection framework + implementation("io.insert-koin:koin-core:$koinVersion") + implementation("io.insert-koin:koin-logger-slf4j:$koinVersion") + + // MC coroutine framework + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:$mccoroutineVersion") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:$mccoroutineVersion") + + // Minecraft server framework + "io.papermc.paper:paper-api:$paperVersion".let { + compileOnly(it) + testImplementation(it) + } + + // Scoreboard framework + implementation("fr.mrmicky:fastboard:$fastboardVersion") + api("com.github.Rushyverse:core:6ae31a9250") + + // Tests + testImplementation("com.github.seeseemelk:MockBukkit-v1.20:$mockBukkitVersion") testImplementation(kotlin("test-junit5")) - testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion") + testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") + testImplementation("io.kotest:kotest-assertions-json:$kotestVersion") + + testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") + testImplementation("io.insert-koin:koin-test:$koinVersion") { + exclude("org.jetbrains.kotlin", "kotlin-test-junit") + } testImplementation("io.mockk:mockk:$mockkVersion") - testImplementation("com.github.Minestom.Minestom:testing:$minestomVersion") } +val javaVersion get() = JavaVersion.VERSION_17 +val javaVersionString get() = javaVersion.toString() +val javaVersionInt get() = javaVersionString.toInt() + kotlin { explicitApi = org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Strict + jvmToolchain(javaVersionInt) sourceSets { + compilerOptions { + freeCompilerArgs = listOf("-Xcontext-receivers") + } + all { languageSettings { optIn("kotlin.RequiresOptIn") @@ -65,55 +123,70 @@ val dokkaOutputDir = "${rootProject.projectDir}/dokka" tasks { withType { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + kotlinOptions.jvmTarget = javaVersionString + } + + withType { + sourceCompatibility = javaVersionString + targetCompatibility = javaVersionString } test { useJUnitPlatform() } - clean { - delete(dokkaOutputDir) + build { + dependsOn(shadowJar) } - val deleteDokkaOutputDir by register("deleteDokkaOutputDirectory") { - group = "documentation" + clean { delete(dokkaOutputDir) } dokkaHtml.configure { - dependsOn(deleteDokkaOutputDir) outputDirectory.set(file(dokkaOutputDir)) } + + shadowJar { + archiveClassifier.set("") + } + + jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(true) + csv.required.set(false) + } + } +} + +val deleteDokkaOutputDir by tasks.register("deleteDokkaOutputDirectory") { + delete(dokkaOutputDir) } val sourcesJar by tasks.registering(Jar::class) { - group = "build" archiveClassifier.set("sources") from(sourceSets.main.get().allSource) } val javadocJar = tasks.register("javadocJar") { - group = "documentation" - dependsOn(tasks.dokkaHtml) + dependsOn(deleteDokkaOutputDir, tasks.dokkaHtml) archiveClassifier.set("javadoc") from(dokkaOutputDir) } publishing { - val projectName = project.name - publications { - val projectOrganizationPath = "Rushyverse/$projectName" + val projectOrganizationPath = "Rushyverse/${project.name}" val projectGitUrl = "https://github.com/$projectOrganizationPath" - create(projectName) { - from(components["kotlin"]) + create(project.name) { + shadow.component(this) artifact(sourcesJar.get()) artifact(javadocJar.get()) pom { - name.set(projectName) + name.set(project.name) description.set(project.description) url.set(projectGitUrl) @@ -129,7 +202,7 @@ publishing { licenses { license { name.set("MIT") - url.set("https://mit-license.org") + url.set("https://mit-license.org/") } } @@ -139,6 +212,7 @@ publishing { email.set("Quentixx@outlook.fr") url.set("https://github.com/Quentixx") } + developer { name.set("Distractic") email.set("Distractic@outlook.fr") diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 00000000..53f2cf19 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,771 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + # exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + UndocumentedPublicFunction: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + excludes: [ '**/serializer/**', '**/test/**' ] + LabeledExpression: + active: false + ignoredLabels: [ ] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: false + threshold: 60 + LongParameterList: + active: false + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [ ] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: true + StringLiteralDuplication: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + +coroutines: + active: true + GlobalCoroutineUsage: + active: true + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: true + SuspendFunWithCoroutineScopeReceiver: + active: true + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: true + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [ ] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: false + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: true + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + SpreadOperator: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnnecessaryPartOfBinaryExpression: + active: true + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: true + ignoredSubjectTypes: [ ] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [ ] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: [ '**/*.kts' ] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - 'FIXME:' + - 'STOPSHIP:' + - 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [ ] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [ ] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [ ] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: [ '**/_Number.kt', '**/serializer/**', '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: true + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: true + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: true + PreferToOverPairSyntax: + active: true + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: false + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: true + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [ ] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: true + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: true + UnnecessaryParentheses: + active: true + allowForUnclearPrecedence: true + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: true + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: true + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: false + excludeImports: + - 'java.util.*' diff --git a/gradle.properties b/gradle.properties index 650dbfe7..cf58b4f0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -kotlin.code.style=official -org.gradle.parallel=true -kotlin.incremental=true group=com.github.Rushyverse -version=1.4.1 -description=Library to create Minestom server for Rushyverse +version=1.0.0 +description= + +org.gradle.parallel = true +kotlin.incremental = true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832..41d9927a 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ef626410..c9eb15be 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-rc-1-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..55a18efd --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.projectKey=Rushyverse_api +sonar.organization=rushyverse +sonar.projectName=${project.name} +sonar.projectVersion=${project.version} +sonar.sources=src/main +sonar.tests=src/test +sonar.kotlin.detekt.reportPaths=build/reports/detekt/detekt.xml +sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml +sonar.sourceEncoding=UTF-8 +sonar.kotlin.threads=4 +sonar.verbose=true diff --git a/src/main/kotlin/com/github/rushyverse/api/APIPlugin.kt b/src/main/kotlin/com/github/rushyverse/api/APIPlugin.kt new file mode 100644 index 00000000..2bb5d419 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/APIPlugin.kt @@ -0,0 +1,50 @@ +package com.github.rushyverse.api + +import com.github.rushyverse.api.extension.registerListener +import com.github.rushyverse.api.game.SharedGameData +import com.github.rushyverse.api.koin.CraftContext +import com.github.rushyverse.api.koin.loadModule +import com.github.rushyverse.api.listener.api.LanguageListener +import com.github.rushyverse.api.listener.api.ScoreboardListener +import com.github.rushyverse.api.player.language.LanguageManager +import com.github.rushyverse.api.player.scoreboard.ScoreboardManager +import org.bukkit.Bukkit +import org.bukkit.plugin.java.JavaPlugin + +/** + * Plugin to enable the API in server. + */ +public class APIPlugin : JavaPlugin() { + + public companion object { + /** + * A unique identifier for this plugin. This ID is used for tasks like identifying + * the Koin application, loading Koin modules, etc. + */ + public const val ID_API: String = "api" + + /** + * This ID is used to identify the resource bundle of API translations in the project files. + */ + public const val BUNDLE_API: String = "api_translate" + } + + override fun onEnable() { + super.onEnable() + CraftContext.startKoin(ID_API) + loadModule(ID_API) { + single { Bukkit.getServer() } + single { ScoreboardManager() } + single { LanguageManager() } + single { SharedGameData() } + } + + registerListener { LanguageListener() } + registerListener { ScoreboardListener() } + } + + override fun onDisable() { + CraftContext.stopKoin(ID_API) + super.onDisable() + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/Plugin.kt b/src/main/kotlin/com/github/rushyverse/api/Plugin.kt new file mode 100644 index 00000000..31d770e5 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/Plugin.kt @@ -0,0 +1,198 @@ +package com.github.rushyverse.api + +import com.charleskorn.kaml.Yaml +import com.charleskorn.kaml.YamlConfiguration +import com.github.rushyverse.api.APIPlugin.Companion.BUNDLE_API +import com.github.rushyverse.api.configuration.reader.IFileReader +import com.github.rushyverse.api.configuration.reader.YamlFileReader +import com.github.rushyverse.api.extension.asComponent +import com.github.rushyverse.api.extension.registerListener +import com.github.rushyverse.api.koin.CraftContext +import com.github.rushyverse.api.koin.inject +import com.github.rushyverse.api.koin.loadModule +import com.github.rushyverse.api.listener.PlayerListener +import com.github.rushyverse.api.listener.VillagerListener +import com.github.rushyverse.api.player.Client +import com.github.rushyverse.api.player.ClientManager +import com.github.rushyverse.api.player.ClientManagerImpl +import com.github.rushyverse.api.player.language.LanguageManager +import com.github.rushyverse.api.serializer.* +import com.github.rushyverse.api.translation.ResourceBundleTranslator +import com.github.rushyverse.api.translation.Translator +import com.github.rushyverse.api.translation.registerResourceBundleForSupportedLocales +import com.github.shynixn.mccoroutine.bukkit.SuspendingJavaPlugin +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.SerializersModuleBuilder +import kotlinx.serialization.modules.contextual +import net.kyori.adventure.text.Component +import org.bukkit.entity.Player +import org.jetbrains.annotations.Blocking +import org.koin.core.module.Module +import org.koin.dsl.bind +import java.util.* + +/** + * Represents the base functionality required to create a plugin. + * This abstract class provides necessary tools and life-cycle methods to facilitate the creation + * and management of a plugin that utilizes asynchronous operations, dependency injection, and + * other utility functions. + * @property id A unique identifier for this plugin. + * @property bundle The name of the resource bundle to use for this plugin. + * This ID is used for tasks like identifying the Koin application and loading Koin modules. + */ +public abstract class Plugin( + public val id: String, + public val bundle: String +) : SuspendingJavaPlugin() { + + /** + * Client manager linked to this plugin. + */ + public val clientManager: ClientManager by inject(id) + + /** + * Translator linked to this plugin. + */ + public val translator: Translator by inject(id) + + /** + * Common language manager for all plugins. + */ + public val languageManager: LanguageManager by inject() + + override suspend fun onEnableAsync() { + super.onEnableAsync() + + CraftContext.startKoin(id) + moduleBukkit() + moduleClients() + moduleTranslation() + + registerListener { PlayerListener(this) } + registerListener { VillagerListener(this) } + } + + /** + * Creates and loads a Koin module that stores instances of this plugin and of his child. + * + * @return The Koin module containing the plugin instances. + */ + protected inline fun modulePlugin(): Module = loadModule(id) { + single { this@Plugin } + single { this@Plugin as T } + } + + /** + * Creates and loads a Koin module containing client management components. + * + * @return The Koin module for client management. + */ + protected fun moduleClients(): Module = loadModule(id) { + single { ClientManagerImpl() } bind ClientManager::class + } + + /** + * Creates and loads a Koin module containing translation components. + * + * @return The Koin module for translation. + */ + protected fun moduleTranslation(): Module = loadModule(id) { + single { createTranslator() } bind Translator::class + } + + /** + * Creates and loads a Koin module with Bukkit-specific components. + * Can be overridden by derived classes to provide additional or customized components. + * + * @return The Koin module for Bukkit components. + */ + protected open fun moduleBukkit(): Module = loadModule(id) { + single { getLogger() } + } + + override suspend fun onDisableAsync() { + CraftContext.stopKoin(id) + super.onDisableAsync() + } + + /** + * Creates a new YAML reader instance to handle YAML configurations. + * Allows for customization of serializers and configurations. + * + * @param configuration Configuration options for the YAML reader. + * @param serializerModuleBuilder Provides additional serializers. + * @return A configured YAML reader instance. + */ + protected open fun createYamlReader( + configuration: YamlConfiguration = YamlConfiguration(), + serializerModuleBuilder: SerializersModuleBuilder.() -> Unit = {}, + ): IFileReader { + val yaml = Yaml( + serializersModule = SerializersModule { + contextual(ComponentSerializer) + contextual(DyeColorSerializer) + contextual(EnchantmentSerializer) + contextual(ItemStackSerializer) + contextual(LocationSerializer) + contextual(MaterialSerializer) + contextual(NamespacedSerializer) + contextual(PatternSerializer) + contextual(PatternTypeSerializer) + contextual(RangeDoubleSerializer) + serializerModuleBuilder() + }, + configuration = configuration, + ) + return YamlFileReader(this, yaml) + } + + /** + * Abstract function to create a new client instance associated with a given player. + * + * @param player The player for whom the client instance should be created. + * @return The created client instance. + */ + public abstract fun createClient(player: Player): Client + + /** + * Creates a new translator to fetch translations for the supported languages. + * Can be overridden by derived classes to provide custom translation providers. + * + * @return A translator configured for the supported languages. + */ + @Blocking + protected open fun createTranslator(): ResourceBundleTranslator = + ResourceBundleTranslator(bundle).apply { + registerResourceBundleForSupportedLocales(BUNDLE_API, ResourceBundle::getBundle) + } + + /** + * Broadcasts a localized message to all players. + * + * This function groups players by their language preferences, translates the message once per language, + * and then sends the appropriate localized message to each player. + * + * @param players The players to whom the message should be sent. + * @param key The key used to look up the translation in the resource bundle. + * @param bundle The resource bundle to use for the translation. + * @param argumentBuilder A function that builds the arguments for the translation. + * @param messageModifier A function that modifies the translated message before it is sent. + * The modification must be chained. + */ + public suspend inline fun broadcast( + players: Collection, + key: String, + bundle: String = this.bundle, + messageModifier: (Component) -> Component = { it }, + argumentBuilder: Translator.(Locale) -> Array = { emptyArray() }, + ) { + players.groupBy { languageManager.get(it).locale } + .forEach { (lang, receiver) -> + val translatedComponent = translator + .get(key, lang, translator.argumentBuilder(lang), bundle) + .asComponent().let(messageModifier) + + receiver.forEach { it.sendMessage(translatedComponent) } + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/RushyServer.kt b/src/main/kotlin/com/github/rushyverse/api/RushyServer.kt deleted file mode 100644 index 2620611a..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/RushyServer.kt +++ /dev/null @@ -1,175 +0,0 @@ -package com.github.rushyverse.api - -import com.github.rushyverse.api.command.GamemodeCommand -import com.github.rushyverse.api.command.GiveCommand -import com.github.rushyverse.api.command.KickCommand -import com.github.rushyverse.api.command.StopCommand -import com.github.rushyverse.api.configuration.* -import com.github.rushyverse.api.translation.ResourceBundleTranslationsProvider -import com.github.rushyverse.api.translation.SupportedLanguage -import com.github.rushyverse.api.translation.TranslationsProvider -import com.github.rushyverse.api.translation.registerResourceBundleForSupportedLocales -import com.github.rushyverse.api.utils.workingDirectory -import mu.KLogger -import mu.KotlinLogging -import net.minestom.server.MinecraftServer -import net.minestom.server.command.CommandManager -import net.minestom.server.extras.MojangAuth -import net.minestom.server.extras.bungee.BungeeCordProxy -import net.minestom.server.extras.velocity.VelocityProxy -import net.minestom.server.instance.AnvilLoader -import net.minestom.server.instance.InstanceContainer -import java.io.File -import java.util.* -import kotlin.reflect.KClass - -public val logger: KLogger = KotlinLogging.logger { } - -/** - * Abstract implementation of Minecraft server. - */ -public abstract class RushyServer { - - public companion object API { - /** - * Name of the bundle for API. - */ - public const val BUNDLE_API: String = "api" - - /** - * Register the API commands in the [CommandManager]. - * @param manager CommandManager to register the commands in. - */ - public fun registerCommands(manager: CommandManager = MinecraftServer.getCommandManager()) { - manager.register(StopCommand()) - manager.register(KickCommand()) - manager.register(GiveCommand()) - manager.register(GamemodeCommand()) - } - } - - /** - * Start the minecraft server. - */ - public abstract suspend fun start() - - /** - * Initialize the server and start it using the given (or default) configuration. - * @param configurationPath Path of the configuration file in working directory. - * @param init Initialization function with the configuration loaded and the world container. - */ - protected suspend inline fun start( - configurationPath: String? = null, - init: T.(InstanceContainer) -> Unit - ) { - logger.info { "Loading configuration from $configurationPath" } - val config = loadConfiguration(configurationPath) - logger.info { "Configuration loaded" } - - val minecraftServer = MinecraftServer.init() - val instanceManager = MinecraftServer.getInstanceManager() - val instanceContainer = instanceManager.createInstanceContainer() - val serverConfig = config.server - loadWorld(serverConfig.world, instanceContainer) - - applyVelocityConfiguration(serverConfig.velocity) - applyBungeeCordConfiguration(serverConfig.bungeeCord) - - applyOnlineMode(serverConfig.onlineMode) - - init(config, instanceContainer) - - minecraftServer.start("0.0.0.0", serverConfig.port) - } - - /** - * Enable the online mode if [enabled] is true. - * @param enabled If the online mode should be enabled. - */ - protected open suspend fun applyOnlineMode(enabled: Boolean) { - if (enabled) { - logger.info { "Enabling Online mode" } - MojangAuth.init() - logger.info { "Online mode enabled" } - } - } - - /** - * Enable the [Velocity system][VelocityProxy] if the configuration [IVelocityConfiguration] is enabled. - * @param velocity Velocity configuration. - */ - protected open suspend fun applyVelocityConfiguration(velocity: IVelocityConfiguration) { - if (velocity.enabled) { - logger.info { "Enabling Velocity support" } - VelocityProxy.enable(velocity.secret) - logger.info { "Velocity support enabled" } - } - } - - /** - * Enable the [BungeeCord system][BungeeCordProxy] if the configuration [IBungeeCordConfiguration] is enabled. - * @param bungeeCord BungeeCord configuration. - */ - protected open suspend fun applyBungeeCordConfiguration(bungeeCord: IBungeeCordConfiguration) { - if (bungeeCord.enabled) { - logger.info { "Enabling BungeeCord support" } - BungeeCordProxy.enable() - BungeeCordProxy.setBungeeGuardTokens(bungeeCord.secrets) - logger.info { "BungeeCord support enabled" } - } - } - - /** - * With the [worldFolder], retrieve the file of the world and load it in the [instanceContainer]. - * @param worldFolder World folder. - * @param instanceContainer Instance container of the server. - */ - protected open suspend fun loadWorld(worldFolder: String, instanceContainer: InstanceContainer) { - val anvilWorld = File(workingDirectory, worldFolder) - if (!anvilWorld.isDirectory) { - throw FileSystemException( - anvilWorld, - null, - "World ${anvilWorld.absolutePath} does not exist or is not a directory" - ) - } - instanceContainer.chunkLoader = AnvilLoader(anvilWorld.toPath()) - } - - /** - * Load the configuration using the file or the default config file. - * Will use the [HoconConfigurationReader] to load the configuration. - * @param configFile Path of the configuration file. - * @return The configuration of the server. - */ - protected suspend inline fun loadConfiguration( - configFile: String? - ): T { - return loadConfiguration(T::class, configFile) - } - - /** - * Load the configuration using the file or the default config file. - * @param clazz Type of configuration class to load. - * @param configFile Path of the configuration file. - * @return The configuration of the server. - */ - protected open suspend fun loadConfiguration( - clazz: KClass, - configFile: String? - ): T { - val configurationFile = IConfigurationReader.getOrCreateConfigurationFile(configFile) - return HoconConfigurationReader().readConfigurationFile(clazz, configurationFile) - } - - /** - * Create a translation provider to provide translations for the [supported languages][SupportedLanguage]. - * @param bundles Bundles to load. - * @return New translation provider. - */ - protected open suspend fun createTranslationsProvider(bundles: Iterable): TranslationsProvider { - return ResourceBundleTranslationsProvider().apply { - bundles.forEach { registerResourceBundleForSupportedLocales(it, ResourceBundle::getBundle) } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/command/CommandMessages.kt b/src/main/kotlin/com/github/rushyverse/api/command/CommandMessages.kt deleted file mode 100644 index 6e4cf307..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/command/CommandMessages.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.github.rushyverse.api.command - -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.minestom.server.command.CommandSender -import net.minestom.server.entity.Player - -/** - * Utils class to send specific messages. - */ -public object CommandMessages { - - /** - * Send a message to the sender because no player was found in a previous process. - * @param sender Sender who will receive the message. - */ - public fun sendPlayerNotFoundMessage(sender: CommandSender) { - if (sender is Player) { - sender.sendMessage(Component.translatable("argument.entity.notfound.player", NamedTextColor.RED)) - } else { - sender.sendMessage(Component.text("No player was found", NamedTextColor.RED)) - } - } - - /** - * Send a message to the sender because he doesn't have the permission to execute the command. - * @param sender Sender who will receive the message. - */ - public fun sendMissingPermissionMessage(sender: CommandSender) { - sender.sendMessage(Component.translatable("commands.help.failed", NamedTextColor.RED)) - } - -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/command/GamemodeCommand.kt b/src/main/kotlin/com/github/rushyverse/api/command/GamemodeCommand.kt deleted file mode 100644 index bd3ea49b..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/command/GamemodeCommand.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.github.rushyverse.api.command - -import com.github.rushyverse.api.command.CommandMessages.sendMissingPermissionMessage -import com.github.rushyverse.api.command.CommandMessages.sendPlayerNotFoundMessage -import com.github.rushyverse.api.extension.sync -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.minestom.server.command.CommandSender -import net.minestom.server.command.builder.Command -import net.minestom.server.command.builder.arguments.ArgumentEnum -import net.minestom.server.command.builder.arguments.ArgumentType -import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity -import net.minestom.server.entity.GameMode -import net.minestom.server.entity.Player -import net.minestom.server.permission.Permission - -/** - * Command to define the game mode of a player. - */ -public class GamemodeCommand : Command("gamemode") { - - /** - * Enum of permission to perform [command][GamemodeCommand]. - * @property permission Permission. - */ - public enum class Permissions(public val permission: Permission) { - /** - * Permission to change game mode of oneself. - */ - SELF(Permission("gamemode.self")), - - /** - * Permission to change game mode of another player. - */ - OTHER(Permission("gamemode.other")) - } - - init { - val gamemodeArg = createGamemodeArgument() - val playerArg = argumentPlayer() - - setCondition { sender, _ -> - sender !is Player || sender.hasPermission(Permissions.SELF.permission) || sender.hasPermission(Permissions.OTHER.permission) - } - - setDefaultExecutor { sender, context -> - val commandName = context.commandName - sendSyntaxMessage(sender, commandName) - } - - addSelfSyntax(gamemodeArg) - addOtherSyntax(playerArg, gamemodeArg) - } - - /** - * Send an error message to define the usage syntax of the command. - * @param sender Command's sender. - * @param commandName Name of the command used to execute it. - */ - private fun sendSyntaxMessage(sender: CommandSender, commandName: String) { - sender.sendMessage(Component.text("Usage: /$commandName [target]", NamedTextColor.RED)) - } - - /** - * Create a new argument targeting players name. - * @return New argument. - */ - private fun argumentPlayer(): ArgumentEntity = ArgumentType.Entity("target").onlyPlayers(true).singleEntity(true) - - /** - * Create a new argument targeting the gamemode choice. - * @return New argument. - */ - private fun createGamemodeArgument(): ArgumentEnum { - return ArgumentType.Enum("gamemode", GameMode::class.java).setFormat(ArgumentEnum.Format.LOWER_CASED) - .apply { - setCallback { sender, exception -> - sender.sendMessage( - Component.text("Invalid gamemode ", NamedTextColor.RED) - .append(Component.text(exception.input, NamedTextColor.WHITE)) - .append(Component.text("!")) - ) - } - } - } - - /** - * Define the syntax to process the command on another player. - * @param playerArg Argument to retrieve player(s) selected. - * @param gamemodeArg Argument to retrieve game mode selected. - */ - private fun addOtherSyntax(playerArg: ArgumentEntity, gamemodeArg: ArgumentEnum) { - addConditionalSyntax( - { sender, _ -> - sender !is Player || sender.hasPermission(Permissions.OTHER.permission) - }, - { sender, context -> - val finder = context.get(playerArg) - val player = finder.findFirstPlayer(sender) - if (player == null) { - sendPlayerNotFoundMessage(sender) - return@addConditionalSyntax - } - - if (player == sender) { - if (!sender.hasPermission(Permissions.SELF.permission)) { - sendMissingPermissionMessage(sender) - return@addConditionalSyntax - } - - processSelf(player, context.get(gamemodeArg)) - return@addConditionalSyntax - } - - processOther(sender, player, context.get(gamemodeArg)) - }, gamemodeArg, playerArg - ) - } - - /** - * Define the syntax to process the command on the sender. - * @param gamemodeArg Argument to retrieve game mode selected. - */ - private fun addSelfSyntax(gamemodeArg: ArgumentEnum) { - addSyntax({ sender, context -> - if (sender !is Player || !sender.hasPermission(Permissions.SELF.permission)) { - sendMissingPermissionMessage(sender) - return@addSyntax - } - - processSelf(sender, context.get(gamemodeArg)) - }, gamemodeArg) - } - - /** - * Change the game mode of the player and notify it. - * @param player Player who has his game mode modified. - * @param gamemode Game mode applied to the player. - */ - private fun processSelf(player: Player, gamemode: GameMode) { - val gamemodeComponent = createTranslatableGameModeComponent(gamemode) - changeGameModeSafe(player, gamemode) - player.sendMessage(Component.translatable("commands.gamemode.success.self", gamemodeComponent)) - } - - /** - * Change the game m ode of the targeted players and notify them. - * @param sender Command's sender. - * @param player List of entities targeted by the sender. - * @param gamemode Game mode applied to the players. - */ - private fun processOther(sender: CommandSender, player: Player, gamemode: GameMode) { - val gamemodeComponent = createTranslatableGameModeComponent(gamemode) - val playerName: Component = player.displayName ?: player.name - changeGameModeSafe(player, gamemode) - player.sendMessage(Component.translatable("gameMode.changed", gamemodeComponent)) - sender.sendMessage(Component.translatable("commands.gamemode.success.other", playerName, gamemodeComponent)) - } - - /** - * Get a safe instance of the player to assign the new game mode. - * @param player Player who has his game mode modified. - * @param gamemode GameMode applied to the player. - */ - private fun changeGameModeSafe(player: Player, gamemode: GameMode) { - player.sync { gameMode = gamemode } - } - - /** - * Create a component representing the game mode. - * @param gamemode Game mode. - * @return Component representing the game mode. - */ - private fun createTranslatableGameModeComponent(gamemode: GameMode): Component { - return Component.translatable("gameMode." + gamemode.name.lowercase()) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/command/GiveCommand.kt b/src/main/kotlin/com/github/rushyverse/api/command/GiveCommand.kt deleted file mode 100644 index 87b55fd3..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/command/GiveCommand.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.github.rushyverse.api.command - -import com.github.rushyverse.api.extension.sync -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.minestom.server.command.CommandSender -import net.minestom.server.command.builder.Command -import net.minestom.server.command.builder.arguments.Argument -import net.minestom.server.command.builder.arguments.ArgumentType -import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity -import net.minestom.server.command.builder.arguments.minecraft.ArgumentItemStack -import net.minestom.server.entity.Player -import net.minestom.server.item.ItemStack -import net.minestom.server.permission.Permission - -/** - * Command to give item to a player. - */ -public class GiveCommand : Command("give") { - - /** - * Enum of permission to perform [command][GiveCommand]. - * @property permission Permission. - */ - public enum class Permissions(public val permission: Permission) { - /** - * Permission to give item to oneself. - */ - EXECUTE(Permission("give")), - } - - init { - setCondition { sender, _ -> - sender !is Player || sender.hasPermission(Permissions.EXECUTE.permission) - } - - setDefaultExecutor { sender, context -> - val commandName = context.commandName - sendSyntaxMessage(sender, commandName) - } - - val playersArg = argumentPlayers() - val itemArg = argumentItem() - val amountArg = argumentAmount() - - setSyntax(playersArg, itemArg, amountArg) - } - - /** - * Send an error message to define the usage syntax of the command. - * @param sender Command's sender. - * @param commandName Name of the command used to execute it. - */ - private fun sendSyntaxMessage(sender: CommandSender, commandName: String) { - sender.sendMessage(Component.text("Usage: /$commandName [amount]", NamedTextColor.RED)) - } - - /** - * Create a new argument targeting players name. - * @return New argument. - */ - private fun argumentPlayers(): ArgumentEntity = - ArgumentType.Entity("targets").singleEntity(false) - - /** - * Create a new argument targeting the amount of item. - * @return New argument. - */ - private fun argumentAmount() = ArgumentType.Integer("amount").setDefaultValue(1) - - /** - * Create a new argument targeting item to give. - * @return New argument. - */ - private fun argumentItem(): ArgumentItemStack = ArgumentType.ItemStack("item") - - /** - * Define the syntax to process the command. - */ - private fun setSyntax(playersArg: ArgumentEntity, itemArg: ArgumentItemStack, amountArg: Argument) { - addSyntax({ sender, context -> - val targets = context.get(playersArg).find(sender).asSequence().filterIsInstance() - - val amount = context.get(amountArg) - val item = context.get(itemArg).withAmount(amount) - - process(sender, targets, item) - }, playersArg, itemArg, amountArg) - } - - /** - * Give the item and notify sender. - * @param sender Command's sender. - * @param targets Player who will receive the item. - * @param item Item given. - */ - private fun process( - sender: CommandSender, - targets: Sequence, - item: ItemStack - ) { - val receivers = targets.map { - it.sync { - inventory.addItemStack(item) - name - } - }.toList() - - if (receivers.size == 1) { - sender.sendMessage( - Component.translatable( - "commands.give.success.single", - Component.text(item.amount()), - Component.text(item.material().name()), - receivers.first() - ) - ) - } else { - sender.sendMessage( - Component.translatable( - "commands.give.success.multiple", - Component.text(item.amount()), - Component.text(item.material().name()), - Component.text(receivers.size) - ) - ) - } - - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/command/KickCommand.kt b/src/main/kotlin/com/github/rushyverse/api/command/KickCommand.kt deleted file mode 100644 index 974f745e..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/command/KickCommand.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.github.rushyverse.api.command - -import com.github.rushyverse.api.command.CommandMessages.sendPlayerNotFoundMessage -import com.github.rushyverse.api.extension.sync -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.minestom.server.command.CommandSender -import net.minestom.server.command.builder.Command -import net.minestom.server.command.builder.arguments.Argument -import net.minestom.server.command.builder.arguments.ArgumentType -import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity -import net.minestom.server.entity.Player -import net.minestom.server.permission.Permission - -/** - * Command to kick a player. - * @param isProtected Lambda to check if a player is protected from being kicked. - */ -public class KickCommand( - private val isProtected: (CommandSender, Player) -> Boolean = { _, target -> - target.sync { hasPermission(Permissions.EXECUTE.permission) } - } -) : Command("kick") { - - /** - * Enum of permission to perform [command][KickCommand]. - * @property permission Permission. - */ - public enum class Permissions(public val permission: Permission) { - /** - * Permission to kick another player. - */ - EXECUTE(Permission("kick")) - } - - init { - setDefaultExecutor { sender, context -> - val commandName = context.commandName - sendSyntaxMessage(sender, commandName) - } - - setCondition { sender, _ -> - sender !is Player || sender.hasPermission(Permissions.EXECUTE.permission) - } - - val playerArg = argumentPlayer() - val reasonArg = argumentReason() - addSyntaxPlayer(playerArg, reasonArg) - } - - /** - * Create a new argument to retrieve the reason of the kick. - * @return New string argument - */ - private fun argumentReason(): Argument = - ArgumentType.String("reason").setDefaultValue("No reason specified") - - /** - * Create a new argument targeting players name. - * @return New argument. - */ - private fun argumentPlayer(): ArgumentEntity = - ArgumentType.Entity("target").onlyPlayers(true).singleEntity(true) - - /** - * Send an error message to define the usage syntax of the command. - * @param sender Command's sender. - * @param commandName Name of the command used to execute it. - */ - private fun sendSyntaxMessage(sender: CommandSender, commandName: String) { - sender.sendMessage(Component.text("Usage: /$commandName ", NamedTextColor.RED)) - } - - /** - * Define the syntax to process the command on a player. - * @param playerArg Argument to retrieve player selected. - */ - private fun addSyntaxPlayer(playerArg: ArgumentEntity, reasonArg: Argument) { - addSyntax({ sender, context -> - val player = context.get(playerArg).findFirstPlayer(sender) - if (player == null) { - sendPlayerNotFoundMessage(sender) - return@addSyntax - } - - if (!isProtected(sender, player)) { - sender.sendMessage(Component.text("You can't kick this player", NamedTextColor.RED)) - return@addSyntax - } - - val reasonComponent = Component.text(context.get(reasonArg)) - - kickPlayer(player, reasonComponent) - - sender.sendMessage( - Component.translatable("commands.kick.success", player.name, reasonComponent) - .color(NamedTextColor.YELLOW) - ) - }, playerArg, reasonArg) - } - - /** - * Kick the player from the server.ad - * - * @param player Player to kick. - * @param reason Component to display as the reason of the kick. - */ - private fun kickPlayer( - player: Player, - reason: Component - ) { - val kickComponent = Component.translatable("multiplayer.disconnect.kicked") - .appendNewline() - .append(reason) - - player.sync { kick(kickComponent) } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/command/StopCommand.kt b/src/main/kotlin/com/github/rushyverse/api/command/StopCommand.kt deleted file mode 100644 index 0c6d3313..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/command/StopCommand.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.github.rushyverse.api.command - -import com.github.rushyverse.api.extension.async -import com.github.rushyverse.api.extension.setDefaultExecutorSuspend -import kotlinx.coroutines.awaitAll -import net.kyori.adventure.text.Component -import net.minestom.server.MinecraftServer -import net.minestom.server.ServerProcess -import net.minestom.server.command.builder.Command -import net.minestom.server.entity.Player -import net.minestom.server.instance.InstanceManager -import net.minestom.server.permission.Permission - -/** - * Command to stop the server. - */ -public class StopCommand( - private val serverProcess: ServerProcess = MinecraftServer.process(), - private val instanceManager: InstanceManager? = MinecraftServer.getInstanceManager() -) : Command("stop") { - - /** - * Enum of permission to perform [command][StopCommand]. - * @property permission Permission. - */ - public enum class Permissions(public val permission: Permission) { - /** - * Permission to give item to another player. - */ - EXECUTE(Permission("stop")) - } - - init { - setCondition { sender, _ -> - sender !is Player || sender.hasPermission(Permissions.EXECUTE.permission) - } - - setDefaultExecutorSuspend { _, _ -> - kickPlayers() - serverProcess.stop() - } - } - - /** - * Kick all players from the server. - */ - private suspend fun kickPlayers() { - val instanceManager = instanceManager ?: return - val stopComponent = Component.translatable("commands.stop.stopping") - instanceManager.instances - .asSequence() - .flatMap { it.players } - .map { - it.async { kick(stopComponent) } - }.toList().awaitAll() - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/configuration/HoconConfigurationReader.kt b/src/main/kotlin/com/github/rushyverse/api/configuration/HoconConfigurationReader.kt deleted file mode 100644 index e725478a..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/configuration/HoconConfigurationReader.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.rushyverse.api.configuration - -import com.github.rushyverse.api.serializer.PosSerializer -import com.typesafe.config.ConfigFactory -import kotlinx.serialization.KSerializer -import kotlinx.serialization.hocon.Hocon -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.serializer -import net.minestom.server.coordinate.Pos -import java.io.File -import kotlin.reflect.KClass -import kotlin.reflect.full.createType - -/** - * Read configuration from HOCON file. - * @receiver Configuration reader. - * @param configFile Configuration file to load. - * @return The configuration loaded from the given file. - */ -public inline fun HoconConfigurationReader.readConfigurationFile(configFile: File): T = - readConfigurationFile(hocon.serializersModule.serializer(), configFile) - -/** - * Read configuration from HOCON file. - * @property hocon [Hocon] configuration to use. - */ -public class HoconConfigurationReader(public val hocon: Hocon = hoconDefault) : IConfigurationReader { - - public companion object { - /** - * Default [Hocon] configuration using custom serializer. - * @see PosSerializer - */ - public val hoconDefault: Hocon = Hocon { - serializersModule = SerializersModule { - contextual(Pos::class, PosSerializer) - } - } - } - - override fun readConfigurationFile(clazz: KClass, configFile: File): T { - val serializer = hocon.serializersModule.serializer(clazz.createType()) - @Suppress("UNCHECKED_CAST") - return readConfigurationFile(serializer as KSerializer, configFile) - } - - override fun readConfigurationFile(serializer: KSerializer, configFile: File): T { - val config = ConfigFactory.parseFile(configFile) - return hocon.decodeFromConfig(serializer, config) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/configuration/IConfiguration.kt b/src/main/kotlin/com/github/rushyverse/api/configuration/IConfiguration.kt deleted file mode 100644 index bd1d7bf3..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/configuration/IConfiguration.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.github.rushyverse.api.configuration - -import kotlinx.serialization.Serializable - -/** - * Configuration of the application. - * @property server Configuration of server. - */ -public interface IConfiguration { - - public val server: IServerConfiguration - -} - -/** - * Configuration about the minestom server. - * @property port Port of the server. - * @property world Path of the world to load. - * @property onlineMode `true` to enable the Mojang authentication. - * @property velocity Velocity configuration. - * @property bungeeCord BungeeCord configuration. - */ -public interface IServerConfiguration { - - public val port: Int - - public val world: String - - public val onlineMode: Boolean - - public val velocity: IVelocityConfiguration - - public val bungeeCord: IBungeeCordConfiguration -} - -/** - * Configuration to connect the server to the velocity proxy. - * @property enabled Whether the velocity support is enabled. - * @property secret Secret to verify if the client comes from the proxy. - */ -public interface IVelocityConfiguration { - - public val enabled: Boolean - - public val secret: String - -} - -/** - * Configuration to connect the server to the velocity proxy. - */ -@Serializable -public data class VelocityConfiguration( - override val enabled: Boolean, - override val secret: String -) : IVelocityConfiguration - - -/** - * Configuration to connect the server to the bungeeCord proxy. - * @property enabled Whether the server should connect to the bungeeCord proxy. - * @property secrets Secrets to verify if the client comes from the proxy. - */ -public interface IBungeeCordConfiguration { - - public val enabled: Boolean - - public val secrets: Set - -} - -/** - * Configuration to connect the server to the bungeeCord proxy. - */ -@Serializable -public data class BungeeCordConfiguration( - override val enabled: Boolean, - override val secrets: Set -) : IBungeeCordConfiguration \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/configuration/IConfigurationReader.kt b/src/main/kotlin/com/github/rushyverse/api/configuration/IConfigurationReader.kt deleted file mode 100644 index 68d4aedc..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/configuration/IConfigurationReader.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.rushyverse.api.configuration - -import com.github.rushyverse.api.utils.workingDirectory -import kotlinx.serialization.KSerializer -import java.io.File -import java.io.FileNotFoundException -import kotlin.reflect.KClass - -/** - * Configuration reader. - */ -public interface IConfigurationReader { - - public companion object { - - /** - * Default name of the config file. - * This name is used to create the default config file when the user does not provide one. - */ - public const val DEFAULT_CONFIG_FILE_NAME: String = "server.conf" - - /** - * Get the configuration file from the given path. - * If the path is null, the default config file will be used. - * If the default config file does not exist, it will be created with the default configuration from resources folder. - * @param filePath Path of the configuration file. - * @return The configuration file that must be used to load application configuration. - */ - public fun getOrCreateConfigurationFile(filePath: String? = null): File { - if (filePath != null) { - val configFile = File(filePath) - if (!configFile.isFile) { - throw FileNotFoundException("Config file $filePath does not exist or is not a regular file") - } - return configFile - } - - return getOrCreateDefaultConfigurationFile(workingDirectory) - } - - /** - * Search for the default config file in the current directory. - * If the file does not exist, it will be created with the default configuration from resources folder. - * @param parent Parent directory of the default config file. - * @return The default config file. - */ - private fun getOrCreateDefaultConfigurationFile(parent: File): File = - File(parent, DEFAULT_CONFIG_FILE_NAME).apply { - if (exists()) { - return this - } - - val defaultConfiguration = - IConfigurationReader::class.java.classLoader.getResourceAsStream(DEFAULT_CONFIG_FILE_NAME) - ?: error("Unable to find default configuration file in server resources") - - defaultConfiguration.use { inputStream -> - if (!createNewFile()) { - throw FileSystemException(this, null, "Unable to create configuration file $absolutePath") - } - - outputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - } - } - } - - /** - * Load the configuration from the given file. - * @param clazz Type of configuration class to load. - * @param configFile Configuration file to load. - * @return The configuration loaded from the given file. - */ - public fun readConfigurationFile(clazz: KClass, configFile: File): T - - /** - * Load the configuration from the given file. - * @param serializer Serializer to deserialize the configuration to the given type. - * @param configFile Configuration file to load. - * @return The configuration loaded from the given file. - */ - public fun readConfigurationFile(serializer: KSerializer, configFile: File): T -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/configuration/reader/ConfigurationReader.kt b/src/main/kotlin/com/github/rushyverse/api/configuration/reader/ConfigurationReader.kt new file mode 100644 index 00000000..b80dd726 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/configuration/reader/ConfigurationReader.kt @@ -0,0 +1,19 @@ +package com.github.rushyverse.api.configuration.reader + +import org.jetbrains.annotations.Blocking + +/** + * Configuration reader. + * Useful to read configuration and transform it to a specific type. + * @param T Final type to obtain after transformation. + */ +public fun interface ConfigurationReader { + + /** + * Read the configuration from the given file. + * @param file File to read. + * @return Configuration read from the given file. + */ + @Blocking + public fun readConfiguration(file: String): T +} diff --git a/src/main/kotlin/com/github/rushyverse/api/configuration/reader/FileReader.kt b/src/main/kotlin/com/github/rushyverse/api/configuration/reader/FileReader.kt new file mode 100644 index 00000000..1c0ac6f1 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/configuration/reader/FileReader.kt @@ -0,0 +1,48 @@ +package com.github.rushyverse.api.configuration.reader + +import com.github.rushyverse.api.serializer.LocationSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.StringFormat +import kotlinx.serialization.serializer +import kotlin.reflect.KClass + +/** + * Read configuration from YAML file. + * @receiver Configuration reader. + * @param configFile Configuration file to load. + * @return The configuration loaded from the given file. + */ +public inline fun IFileReader.readConfigurationFile(configFile: String): T = + readConfigurationFile(format.serializersModule.serializer(), configFile) + +/** + * Configuration reader. + */ +public interface IFileReader { + + /** + * Format to read string from configuration file. + * The field should contain custom serializer for Bukkit classes like [LocationSerializer]. + */ + public val format: StringFormat + + /** + * Load the configuration from the given file. + * @param clazz Type of configuration class to load. + * @param filename Configuration file to load based on the "plugins/${plugin.name}" directory. + * So if the plugin name is "MyPlugin" and the config file is "config.yml", + * the file will be loaded from "plugins/MyPlugin/config.yml". + * @return The configuration loaded from the given file. + */ + public fun readConfigurationFile(clazz: KClass, filename: String): T + + /** + * Load the configuration from the given file. + * @param serializer Serializer to deserialize the configuration to the given type. + * @param filename Configuration file to load based on the "plugins/${plugin.name}" directory. + * So if the plugin name is "MyPlugin" and the config file is "config.yml", + * the file will be loaded from "plugins/MyPlugin/config.yml". + * @return The configuration loaded from the given file. + */ + public fun readConfigurationFile(serializer: KSerializer, filename: String): T +} diff --git a/src/main/kotlin/com/github/rushyverse/api/configuration/reader/YamlFileReader.kt b/src/main/kotlin/com/github/rushyverse/api/configuration/reader/YamlFileReader.kt new file mode 100644 index 00000000..57a7805b --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/configuration/reader/YamlFileReader.kt @@ -0,0 +1,77 @@ +package com.github.rushyverse.api.configuration.reader + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.StringFormat +import kotlinx.serialization.serializer +import org.bukkit.plugin.java.JavaPlugin +import org.jetbrains.annotations.Blocking +import java.io.File +import kotlin.io.path.createParentDirectories +import kotlin.reflect.KClass +import kotlin.reflect.full.createType + +/** + * Read configuration from YAML file. + * If the file does not exist, it will be created from the resource with the same name. + * @property plugin Plugin to get the data folder from. + */ +public class YamlFileReader( + public val plugin: JavaPlugin, + override val format: StringFormat +) : IFileReader { + + @Blocking + override fun readConfigurationFile(clazz: KClass, filename: String): T { + val serializer = format.serializersModule.serializer(clazz.createType()) + @Suppress("UNCHECKED_CAST") + return readConfigurationFile(serializer as KSerializer, filename) + } + + @Blocking + override fun readConfigurationFile(serializer: KSerializer, filename: String): T { + val dataFolder = plugin.dataFolder + require(dataFolder.exists() || dataFolder.mkdirs()) { + "Unable to get or create the plugin data folder ${dataFolder.absolutePath}." + } + + val config = File(dataFolder, filename) + createConfigurationFileIfNecessary(config, filename) + + return format.decodeFromString(serializer, config.readText()) + } + + /** + * Creates the [configuration][config] file if it does not exist. + * Will create the parent directories if necessary. + * + * @param config The configuration file to create. + * @param resourceFile The resource file to copy if the configuration file does not exist. + */ + private fun createConfigurationFileIfNecessary(config: File, resourceFile: String) { + if (config.exists()) return + + config.toPath().createParentDirectories() + require(config.createNewFile()) { + "Unable to create the configuration file ${config.absoluteFile}." + } + + createFileFromResource(config, resourceFile) + } + + /** + * Read the file [resourceFile] from the plugin jar and copy content to [target]. + * @param target File to write the resource to. + * @param resourceFile Name of the resource to copy. + */ + @Blocking + private fun createFileFromResource(target: File, resourceFile: String) { + val resource = plugin::class.java.getResourceAsStream("/$resourceFile") + ?: error("Cannot find resource $resourceFile in the plugin.") + + resource.bufferedReader().use { reader -> + target.bufferedWriter().use { writer -> + reader.copyTo(writer) + } + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/coroutine/MinestomAsyncCoroutineDispatcher.kt b/src/main/kotlin/com/github/rushyverse/api/coroutine/MinestomAsyncCoroutineDispatcher.kt deleted file mode 100644 index 3a4ac576..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/coroutine/MinestomAsyncCoroutineDispatcher.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.rushyverse.api.coroutine - -import kotlinx.coroutines.Dispatchers -import net.minestom.server.MinecraftServer -import net.minestom.server.ServerProcess -import net.minestom.server.timer.ExecutionType - -/** - * Static instance of [MinestomAsyncCoroutineDispatcher]. - */ -internal val minestomAsyncDispatcher = MinestomAsyncCoroutineDispatcher(MinecraftServer.process()) - -/** - * @see [MinestomAsyncCoroutineDispatcher] - */ -public val Dispatchers.MinestomAsync: MinestomCoroutineDispatcher get() = minestomAsyncDispatcher - -/** - * Dispatcher to execute task in a [async][ExecutionType.ASYNC] context of the server. - * @property serverProcess Server's process - */ -public class MinestomAsyncCoroutineDispatcher(serverProcess: ServerProcess) : - MinestomCoroutineDispatcher(serverProcess, ExecutionType.ASYNC) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/coroutine/MinestomCoroutineDispatcher.kt b/src/main/kotlin/com/github/rushyverse/api/coroutine/MinestomCoroutineDispatcher.kt deleted file mode 100644 index 1d031827..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/coroutine/MinestomCoroutineDispatcher.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.rushyverse.api.coroutine - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import net.minestom.server.ServerProcess -import net.minestom.server.timer.ExecutionType -import kotlin.coroutines.CoroutineContext - -/** - * A coroutine dispatcher that uses the Minestom server thread. - * @property serverProcess The server process to use. - * @property type Determine if the execution should be [synchronous][ExecutionType.SYNC] or [asynchronous][ExecutionType.ASYNC]. - * @property scope Coroutine scope using the dispatcher and a [SupervisorJob]. - */ -public open class MinestomCoroutineDispatcher( - private val serverProcess: ServerProcess, - public val type: ExecutionType -) : CoroutineDispatcher() { - - public val scope: CoroutineScope by lazy { CoroutineScope(this + SupervisorJob()) } - - override fun dispatch(context: CoroutineContext, block: Runnable) { - if (!serverProcess.isAlive) { - return - } - - serverProcess.scheduler().scheduleNextProcess(block, type) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/coroutine/MinestomSyncCoroutineDispatcher.kt b/src/main/kotlin/com/github/rushyverse/api/coroutine/MinestomSyncCoroutineDispatcher.kt deleted file mode 100644 index 898fc369..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/coroutine/MinestomSyncCoroutineDispatcher.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.rushyverse.api.coroutine - -import kotlinx.coroutines.Dispatchers -import net.minestom.server.MinecraftServer -import net.minestom.server.ServerProcess -import net.minestom.server.timer.ExecutionType - -/** - * Static instance of [MinestomSyncCoroutineDispatcher]. - */ -internal val minestomSyncDispatcher = MinestomSyncCoroutineDispatcher(MinecraftServer.process()) - -/** - * @see [MinestomSyncCoroutineDispatcher] - */ -public val Dispatchers.MinestomSync: MinestomCoroutineDispatcher get() = minestomSyncDispatcher - -/** - * Dispatcher to execute task in a [sync][ExecutionType.SYNC] context of the server. - * @property serverProcess Server's process - */ -public class MinestomSyncCoroutineDispatcher(serverProcess: ServerProcess) : - MinestomCoroutineDispatcher(serverProcess, ExecutionType.SYNC) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/coroutine/exception/SilentCancellationException.kt b/src/main/kotlin/com/github/rushyverse/api/coroutine/exception/SilentCancellationException.kt new file mode 100644 index 00000000..c67dfb71 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/coroutine/exception/SilentCancellationException.kt @@ -0,0 +1,20 @@ +package com.github.rushyverse.api.coroutine.exception + +import kotlin.coroutines.cancellation.CancellationException + +/** + * Exception who doesn't have a stack trace. + */ +public class SilentCancellationException(message: String, cause: Throwable? = null) : CancellationException(message) { + + init { + if (cause != null) { + initCause(cause) + } + } + + override fun fillInStackTrace(): Throwable { + stackTrace = emptyArray() + return this + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/delegate/DelegatePlayer.kt b/src/main/kotlin/com/github/rushyverse/api/delegate/DelegatePlayer.kt new file mode 100644 index 00000000..6b149293 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/delegate/DelegatePlayer.kt @@ -0,0 +1,18 @@ +package com.github.rushyverse.api.delegate + +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import java.util.* +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * Class to delegate the retrieve of a player through Bukkit. + * @property uuid Player's UUID. + */ +public class DelegatePlayer(public val uuid: UUID) : ReadOnlyProperty { + + override operator fun getValue(thisRef: Any?, property: KProperty<*>): Player? { + return Bukkit.getPlayer(uuid) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/delegate/DelegateWorld.kt b/src/main/kotlin/com/github/rushyverse/api/delegate/DelegateWorld.kt new file mode 100644 index 00000000..f1210a25 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/delegate/DelegateWorld.kt @@ -0,0 +1,18 @@ +package com.github.rushyverse.api.delegate + +import org.bukkit.Bukkit +import org.bukkit.World +import java.util.* +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * Class to delegate the retrieve of a world through Bukkit. + * @property uuid World's UUID. + */ +public class DelegateWorld(public val uuid: UUID) : ReadOnlyProperty { + + override operator fun getValue(thisRef: Any?, property: KProperty<*>): World? { + return Bukkit.getWorld(uuid) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/entity/NPCEntity.kt b/src/main/kotlin/com/github/rushyverse/api/entity/NPCEntity.kt deleted file mode 100644 index 607d9c85..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/entity/NPCEntity.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.github.rushyverse.api.entity - -import com.extollit.gaming.ai.path.HydrazinePathFinder -import com.github.rushyverse.api.extension.sync -import com.github.rushyverse.api.position.IAreaLocatable -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.EntityType -import net.minestom.server.entity.LivingEntity -import net.minestom.server.entity.Player -import net.minestom.server.entity.pathfinding.NavigableEntity -import net.minestom.server.entity.pathfinding.Navigator -import net.minestom.server.event.player.PlayerEntityInteractEvent -import net.minestom.server.instance.Instance -import java.util.* -import java.util.concurrent.CompletableFuture - -/** - * A non-playable character. - * @property areaTrigger Area to know which players are near of the entity. - * @property navigator Navigator of the entity. - */ -public open class NPCEntity( - type: EntityType, - public var areaTrigger: IAreaLocatable? = null, - uuid: UUID = UUID.randomUUID() -) : LivingEntity(type, uuid), NavigableEntity { - - private val navigator: Navigator = Navigator(this) - - override fun getNavigator(): Navigator = navigator - - override fun update(time: Long) { - super.update(time) - navigator.tick() - updateAreaEntities(position, instance) - } - - /** - * If the [areaTrigger] is not null, update the entities in the area. - * @param position Position of the entity. - * @param instance Instance where is located the entity. - */ - private fun updateAreaEntities(position: Pos, instance: Instance) { - areaTrigger?.let { - it.position = position - it.instance = instance - val (enter, quit) = it.updateEntitiesInArea() - enter.forEach(this::onEnterArea) - quit.forEach(this::onLeaveArea) - } - } - - override fun setInstance(instance: Instance): CompletableFuture { - navigator.setPathFinder(HydrazinePathFinder(navigator.pathingEntity, instance.instanceSpace)) - return super.setInstance(instance) - } - - /** - * Look at the nearest player. - * Will retrieve the entities in [areaTrigger] and look at the nearest one by comparing the distance between the entity and the player. - * @throws IllegalStateException If the [areaTrigger] is null. - */ - public open fun lookNearestPlayer() { - val area = areaTrigger ?: throw IllegalStateException("An area detector must be set to use this method.") - val entities = area.entitiesInArea - if (entities.isEmpty()) return - - val npcPosition = position - val nearestEntity = entities.minByOrNull { - it.sync { position.distance(npcPosition) } - } ?: return - - lookAt(nearestEntity) - } - - /** - * Called when a player interact with the entity. - * @param event Event of the interaction. - */ - public open fun onInteract(event: PlayerEntityInteractEvent) { - } - - /** - * Called when a player enter the area of the entity. - * @param player Player who enter the area. - */ - public open fun onEnterArea(player: Player) { - } - - /** - * Called when a player leave the area of the entity. - * @param player Player who leave the area. - */ - public open fun onLeaveArea(player: Player) { - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/entity/PlayerNPCEntity.kt b/src/main/kotlin/com/github/rushyverse/api/entity/PlayerNPCEntity.kt deleted file mode 100644 index 83b2c0c7..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/entity/PlayerNPCEntity.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.github.rushyverse.api.entity - -import com.github.rushyverse.api.position.IAreaLocatable -import net.kyori.adventure.text.Component -import net.minestom.server.entity.EntityType -import net.minestom.server.entity.GameMode -import net.minestom.server.entity.Player -import net.minestom.server.network.packet.server.play.PlayerInfoPacket -import net.minestom.server.network.packet.server.play.PlayerInfoPacket.AddPlayer -import net.minestom.server.network.packet.server.play.PlayerInfoPacket.RemovePlayer -import java.util.* - -/** - * A non-player character that looks like a player. - * @property name Name of the NPC. - * @property properties Properties of the NPC. - * @property playerRemovePacket Remove player packet. - */ -public open class PlayerNPCEntity( - public val name: String, - public val properties: List = emptyList(), - areaTrigger: IAreaLocatable? = null, - uuid: UUID = UUID.randomUUID(), - public val inTabList: Boolean = false, -) : NPCEntity(EntityType.PLAYER, areaTrigger, uuid) { - - private val playerRemovePacket = PlayerInfoPacket( - PlayerInfoPacket.Action.REMOVE_PLAYER, - listOf(RemovePlayer(uuid)) - ) - - override fun updateNewViewer(player: Player) { - val connection = player.playerConnection - connection.sendPacket(createPlayerAddPacket()) - - if (!inTabList) { - scheduleNextTick { connection.sendPacket(playerRemovePacket) } - } - - super.updateNewViewer(player) - } - - /** - * Create a packet to add the NPC in the player view. - * @return A new packet. - */ - private fun createPlayerAddPacket() = PlayerInfoPacket( - PlayerInfoPacket.Action.ADD_PLAYER, - listOf( - AddPlayer( - uuid, - name, - properties, - GameMode.CREATIVE, - 0, - customName ?: Component.text(name), - null - ) - ) - ) -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/Acquirable.kt b/src/main/kotlin/com/github/rushyverse/api/extension/Acquirable.kt deleted file mode 100644 index b295b87f..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/extension/Acquirable.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.rushyverse.api.extension - -import net.minestom.server.entity.Entity -import net.minestom.server.thread.Acquirable -import net.minestom.server.thread.AcquirableCollection - -/** - * Get the [Acquirable] of the entity. - */ -public val E.acquirable: Acquirable get() = getAcquirable() - -/** - * Transform an iterable of entities into an [AcquirableCollection]. - * @receiver Iterable of entities. - * @return An [AcquirableCollection] of entities. - */ -public fun Iterable.toAcquirables(): AcquirableCollection { - return AcquirableCollection(map { it.getAcquirable() }) -} - -/** - * Transform an array of entities into an [AcquirableCollection]. - * @receiver Array of entities. - * @return An [AcquirableCollection] of entities. - */ -public fun Array.toAcquirables(): AcquirableCollection { - return AcquirableCollection(map { it.getAcquirable() }) -} - -/** - * Transform a sequence of entities into an [AcquirableCollection]. - * @receiver Sequence of entities. - * @return An [AcquirableCollection] of entities. - */ -public fun Sequence.toAcquirables(): AcquirableCollection { - return AcquirableCollection(map { it.acquirable }.toList()) -} - diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/CommandExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/CommandExt.kt deleted file mode 100644 index 89d8ba95..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/extension/CommandExt.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.github.rushyverse.api.extension - -import com.github.rushyverse.api.coroutine.MinestomSync -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import net.minestom.server.command.CommandSender -import net.minestom.server.command.builder.Command -import net.minestom.server.command.builder.CommandContext -import net.minestom.server.command.builder.CommandSyntax -import net.minestom.server.command.builder.arguments.Argument - -/** - * Allows to set the default executor command in a coroutine scope. - * @see [Command.setDefaultExecutor] - * @receiver Command where the default executor will be set. - * @param executor Executor to process the command in a suspendable context. - * @param coroutineScope Coroutine scope where the default executor will be executed. - */ -public inline fun Command.setDefaultExecutorSuspend( - coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope, - crossinline executor: suspend (sender: CommandSender, context: CommandContext) -> Unit -): Unit = setDefaultExecutor { sender, context -> - coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { - executor(sender, context) - } -} - -/** - * Allows to add a syntax to a command in a coroutine scope. - * @see [Command.addSyntax] - * @receiver Command where the syntax will be added. - * @param executor Executor to process the command in a suspendable context. - * @param arguments Arguments of the syntax. - * @param coroutineScope Coroutine scope where the syntax will be executed. - */ -public inline fun Command.addSyntaxSuspend( - crossinline executor: suspend (sender: CommandSender, context: CommandContext) -> Unit, - vararg arguments: Argument<*>, - coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope -): MutableCollection = addSyntax({ sender, context -> - coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { - executor(sender, context) - } -}, *arguments) - -/** - * Allows to add a conditional syntax to a command in a coroutine scope. - * @see [Command.addConditionalSyntax] - * @receiver Command where the syntax will be added. - * @param condition Condition to check before executing the syntax. - * @param executor Executor to process the command in a suspendable context. - * @param arguments Arguments of the syntax. - * @param coroutineScope Coroutine scope where the syntax will be executed. - */ -public inline fun Command.addConditionalSyntaxSuspend( - noinline condition: (sender: CommandSender, commandString: String?) -> Boolean, - crossinline executor: suspend (sender: CommandSender, context: CommandContext) -> Unit, - vararg arguments: Argument<*>, - coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope -): MutableCollection = addConditionalSyntax(condition, { sender, context -> - coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { - executor(sender, context) - } -}, *arguments) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/EntityExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/EntityExt.kt deleted file mode 100644 index 8ea09108..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/extension/EntityExt.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.github.rushyverse.api.extension - -import com.github.rushyverse.api.coroutine.MinestomAsync -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import net.minestom.server.entity.Entity -import net.minestom.server.event.EventListener -import net.minestom.server.event.trait.EntityEvent -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -/** - * Async version of [sync]. - * @receiver Entity to lock. - * @param block The callback to execute once the element has been safely acquired. - * @return The result of the callback in a [Deferred] object. - */ -public inline fun E.async( - coroutineScope: CoroutineScope = Dispatchers.MinestomAsync.scope, - crossinline block: suspend E.() -> T -): Deferred = coroutineScope.async { - val acquirable = acquirable.lock() - try { - block(acquirable.get()) - } finally { - acquirable.unlock() - } -} - -/** - * Locks the acquirable element, execute {@code consumer} synchronously and unlock the thread. - * Free if the element is already present in the current thread, blocking otherwise. - * @receiver Entity to lock. - * @param block The callback to execute once the element has been safely acquired. - * @return The result of the callback. - */ -public inline fun E.sync(block: E.() -> T): T { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - - val acquirable = acquirable.lock() - try { - return block(acquirable.get()) - } finally { - acquirable.unlock() - } -} - -/** - * Registers an event listener for this entity. - * @param block Handler of event. - * @return The registered event listener. - */ -public inline fun Entity.onEvent( - crossinline block: Entity.(T) -> EventListener.Result -): EventListener { - val listener = object : EventListener { - - override fun eventType(): Class { - return T::class.java - } - - override fun run(event: T): EventListener.Result { - return block(event) - } - } - eventNode().addListener(listener) - return listener -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/InventoryExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/InventoryExt.kt deleted file mode 100644 index 37068e0f..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/extension/InventoryExt.kt +++ /dev/null @@ -1,283 +0,0 @@ -package com.github.rushyverse.api.extension - -import com.github.rushyverse.api.coroutine.MinestomSync -import com.github.rushyverse.api.item.InventoryConditionSuspend -import com.github.rushyverse.api.item.ItemComparator -import com.github.rushyverse.api.item.asNative -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.format.TextDecoration -import net.minestom.server.inventory.AbstractInventory -import net.minestom.server.inventory.Inventory -import net.minestom.server.inventory.condition.InventoryCondition -import net.minestom.server.item.ItemStack -import net.minestom.server.item.Material - -/** - * Range of slots. - */ -public val AbstractInventory.slots: IntRange - get() = this.itemStacks.indices - -/** - * Add a new suspend inventory condition to the inventory. - * @receiver Inventory where the condition will be added. - * @param coroutineScope Scope to launch action when a new event is received. - * @param inventoryConditionSuspend Inventory condition with a suspendable execution. - * @return A native [InventoryCondition] added to the inventory. - */ -public fun AbstractInventory.addInventoryConditionSuspend( - coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope, - inventoryConditionSuspend: InventoryConditionSuspend -): InventoryCondition { - return inventoryConditionSuspend.asNative(coroutineScope).apply { addInventoryCondition(this) } -} - -/** - * Remove the condition of interaction with the inventory. - * @receiver Inventory. - * @param condition Condition to remove. - * @return `true` if the condition was removed, `false` otherwise. - */ -public fun AbstractInventory.removeCondition(condition: InventoryCondition): Boolean = - inventoryConditions.remove(condition) - -/** - * Lock the position of all items in the inventory. - * If a player tries to move an item, the item will not move. - * @receiver Inventory. - * @return Condition of interaction with the inventory. - */ -public fun AbstractInventory.lockItemPositions(): InventoryCondition { - val condition = InventoryCondition { _, _, _, result -> - result.isCancel = true - } - addInventoryCondition(condition) - return condition -} - -/** - * Handle the click event on a specific slot in coroutine context. - * @receiver Inventory where the handler will be used. - * @param slot Slot that should be clicked. - * @param coroutineScope Coroutine scope where the handler will be called. - * @param handler Handler that will be called when the slot is clicked. - * @return Condition of interaction with the inventory. - */ -public fun AbstractInventory.registerClickEventOnSlotSuspend( - slot: Int, - coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope, - handler: InventoryConditionSuspend -): InventoryCondition = registerClickEventOnSlot(slot, handler.asNative(coroutineScope)) - -/** - * Handle the click event on a specific slot. - * @receiver Inventory where the handler will be used. - * @param slot Slot that should be clicked. - * @param handler Handler that will be called when the slot is clicked. - * @return Condition of interaction with the inventory. - */ -public fun AbstractInventory.registerClickEventOnSlot(slot: Int, handler: InventoryCondition): InventoryCondition { - val condition = InventoryCondition { player, clickedSlot, clickType, result -> - if (clickedSlot == slot) { - handler.accept(player, clickedSlot, clickType, result) - } - } - addInventoryCondition(condition) - return condition -} - -/** - * Add a handler when the player click on the item. - * @receiver Inventory. - * @param item Item that should be clicked. - * @param identifier Allows to identify if an item is equivalent to another. - * @param coroutineScope Coroutine scope where the handler will be called. - * @param handler Handler that will be called when the item is clicked. - */ -public fun AbstractInventory.registerClickEventOnItemSuspend( - item: ItemStack, - identifier: ItemComparator = ItemComparator.EQUALS, - coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope, - handler: InventoryConditionSuspend -): InventoryCondition = registerClickEventOnItem(item, identifier, handler.asNative(coroutineScope)) - -/** - * Add a handler when the player click on the item. - * @receiver Inventory. - * @param item Item that should be clicked. - * @param identifier Allows to identify if an item is equivalent to another. - * @param handler Handler that will be called when the item is clicked. - */ -public fun AbstractInventory.registerClickEventOnItem( - item: ItemStack, - identifier: ItemComparator = ItemComparator.EQUALS, - handler: InventoryCondition -): InventoryCondition { - val condition = InventoryCondition { player, clickedSlot, clickType, result -> - if (identifier.areSame(item, result.clickedItem)) { - handler.accept(player, clickedSlot, clickType, result) - } - } - addInventoryCondition(condition) - return condition -} - -/** - * Add the item to the inventory on the specific slot and add a handler when the player click on the item. - * @receiver Inventory. - * @param slot Slot where the item will be added. - * @param item Item that will be added. - * @param identifier Allows to identify if an item is equivalent to another. - * @param coroutineScope Coroutine scope where the handler will be called. - * @param handler Handler that will be called when the item is clicked. - */ -public fun AbstractInventory.setItemStackSuspend( - slot: Int, - item: ItemStack, - identifier: ItemComparator = ItemComparator.EQUALS, - coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope, - handler: InventoryConditionSuspend -): InventoryCondition = setItemStack(slot, item, identifier, handler.asNative(coroutineScope)) - -/** - * Add the item to the inventory on the specific slot and add a handler when the player click on the item. - * @receiver Inventory. - * @param slot Slot where the item will be added. - * @param item Item that will be added. - * @param identifier Allows to identify if an item is equivalent to another. - * @param handler Handler that will be called when the item is clicked. - */ -public fun AbstractInventory.setItemStack( - slot: Int, - item: ItemStack, - identifier: ItemComparator = ItemComparator.EQUALS, - handler: InventoryCondition, -): InventoryCondition { - this.setItemStack(slot, item) - return registerClickEventOnItem(item, identifier, handler) -} - -/** - * Know if the slot has no item. - * @receiver Inventory checked. - * @param slot Slot checked. - * @return `true` if the slot is empty, `false` otherwise. - */ -public fun AbstractInventory.slotIsEmpty(slot: Int): Boolean = getItemStack(slot).isAir - -/** - * Get the first available slot. - * @receiver Inventory. - * @return The slot number or `-1` if there is no available slot. - */ -public fun AbstractInventory.firstAvailableSlot(): Int = this.itemStacks.indexOfFirst { it.isAir } - -/** - * Add the item on the first available slot and add a handler when the player click on the item. - * If there is no available slot, the item will not be added. - * @receiver Inventory. - * @param item Item that should be added. - * @param identifier Allows to identify if an item is equivalent to another. - * @param coroutineScope Coroutine scope where the handler will be called. - * @param handler Handler that will be called when the item is clicked. - * @return The created handler or `null` if there is no available slot. - */ -public fun AbstractInventory.addItemStackSuspend( - item: ItemStack, - identifier: ItemComparator = ItemComparator.EQUALS, - coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope, - handler: InventoryConditionSuspend -): InventoryCondition? = addItemStack(item, identifier, handler.asNative(coroutineScope)) - -/** - * Add the item on the first available slot and add a handler when the player click on the item. - * If there is no available slot, the item will not be added. - * @receiver Inventory. - * @param item Item that should be added. - * @param identifier Allows to identify if an item is equivalent to another. - * @param handler Handler that will be called when the item is clicked. - * @return The created handler or `null` if there is no available slot. - */ -public fun AbstractInventory.addItemStack( - item: ItemStack, - identifier: ItemComparator = ItemComparator.EQUALS, - handler: InventoryCondition, -): InventoryCondition? { - return if (addItemStack(item)) { - registerClickEventOnItem(item, identifier, handler) - } else null -} - -/** - * Set the item on the specific slot to go back to the previous inventory. - * @receiver Inventory where the item will be added. - * @param slot Slot where the item will be set. - * @param backInventory Redirection inventory. - * @return Condition of interaction with the inventory. - */ -public fun AbstractInventory.setPreviousButton(slot: Int, backInventory: Inventory): InventoryCondition { - return setItemChangeInventory(slot, backInventory, "< ") -} - -/** - * Set the item on the specific slot to go to the next inventory. - * @receiver Inventory where the item will be added. - * @param slot Slot where the item will be set. - * @param nextInventory Redirection inventory. - * @return Condition of interaction with the inventory. - */ -public fun AbstractInventory.setNextButton(slot: Int, nextInventory: Inventory): InventoryCondition { - return setItemChangeInventory(slot, nextInventory, "> ") -} - -/** - * Set the item on the specific slot to go to the another inventory. - * @receiver Inventory where the item will be added. - * @param slot Slot where the item will be set. - * @param otherInventory Redirection inventory. - * @param textItem Text of the item. - * @return Condition of interaction with the inventory. - */ -public fun AbstractInventory.setItemChangeInventory( - slot: Int, - otherInventory: Inventory, - textItem: String -): InventoryCondition { - val inventoryTitle = otherInventory.title - .color(NamedTextColor.GRAY) - .decoration(TextDecoration.ITALIC, true) - .decoration(TextDecoration.BOLD, false) - - val item = ItemStack.of(Material.ARROW) - .withDisplayName( - Component.text(textItem) - .color(NamedTextColor.GOLD) - .decoration(TextDecoration.BOLD, true) - .append(inventoryTitle) - ) - - return setItemStack(slot, item) { player, _, _, result -> - result.isCancel = true - player.openInventory(otherInventory) - } -} - -/** - * Add a close button to the inventory and a handler to close the inventory. - * The item will override the item on the specific slot. - * @receiver Inventory where the button will be added. - * @param slot Slot where the item will be added. - * @return The created handler. - */ -public fun AbstractInventory.setCloseButton(slot: Int): InventoryCondition { - val closeItem = ItemStack.of(Material.BARRIER) - .withDisplayName(Component.text("❌").color(NamedTextColor.RED)) - - return setItemStack(slot, closeItem) { player, _, _, result -> - result.isCancel = true - player.closeInventory() - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/ItemStackExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/ItemStackExt.kt deleted file mode 100644 index 45a7f96d..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/extension/ItemStackExt.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.rushyverse.api.extension - -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.TextComponent -import net.kyori.adventure.text.format.NamedTextColor -import net.minestom.server.item.ItemStack - -/** - * Format the string [loreString] using [toFormattedLoreSequence] and transform it into a [TextComponent] using [toLore]. - * @receiver builder to transform. - * @param loreString String that will be formatted and transform it to lore components. - * @param lineLength The maximum length of each line. - * @param transform A function that will be applied to each component. - * @return The same builder with the lore modified. - */ -public inline fun ItemStack.Builder.formattedLore( - loreString: String, - lineLength: Int = DEFAULT_LORE_LINE_LENGTH, - crossinline transform: TextComponent.Builder.() -> Unit = { - color(NamedTextColor.GRAY) - } -): ItemStack.Builder = lore(loreString.toFormattedLoreSequence(lineLength).toLore(transform)) - -/** - * Format the string [loreString] using [toFormattedLoreSequence] and transform it into a [TextComponent] using [toLore]. - * @receiver ItemStack to transform. - * @param loreString String that will be formatted and transform it to lore components. - * @param lineLength The maximum length of each line. - * @param transform A function that will be applied to each component. - * @return The same ItemStack with the lore modified. - */ -public inline fun ItemStack.withFormattedLore( - loreString: String, - lineLength: Int = DEFAULT_LORE_LINE_LENGTH, - crossinline transform: TextComponent.Builder.() -> Unit = { - color(NamedTextColor.GRAY) - } -): ItemStack = withLore(loreString.toFormattedLoreSequence(lineLength).toLore(transform)) - -/** - * Define an unique component as lore. - * @receiver ItemStack. - * @param lore Lore to set. - * @return The same item. - */ -public fun ItemStack.withLore(lore: Component): ItemStack = withLore(listOf(lore)) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/ListenerExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/ListenerExt.kt deleted file mode 100644 index d3e0ac5b..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/extension/ListenerExt.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.rushyverse.api.extension - -import com.github.rushyverse.api.coroutine.MinestomSync -import com.github.rushyverse.api.listener.EventListenerSuspend -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import net.minestom.server.event.Event -import net.minestom.server.event.EventNode - -/** - * Allows to handle event in a coroutine context. - * @receiver Event node to register the event listener to. - * @param coroutineScope Coroutine scope where the event will be handled. - * @param handle Handler of the event. - */ -public inline fun EventNode.addListenerSuspend( - coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope, - crossinline handle: suspend (E) -> Unit -) { - addListener(object : EventListenerSuspend(coroutineScope) { - override suspend fun runSuspend(event: E) { - handle(event) - } - - override fun eventType(): Class { - return E::class.java - } - }) -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/PosExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/PosExt.kt deleted file mode 100644 index a3282614..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/extension/PosExt.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.rushyverse.api.extension - -import net.minestom.server.coordinate.Pos -import kotlin.math.pow -import kotlin.math.sqrt - -/** - * Returns the center position between two points. - * @receiver The first position. - * @param other The second position. - * @return The center position between the two points. - */ -public fun Pos.centerRelative(other: Pos): Pos = add(other).div(2.0) - -/** - * Returns whether the given [Pos] is in the cube defined by the two given [Pos]s. - * @receiver The position to check. - * @param min The minimum position of the cube. - * @param max The maximum position of the cube. - * @return `true` if the position is in the cube, `false` otherwise. - */ -public fun Pos.isInCube(min: Pos, max: Pos): Boolean { - return x in min.x..max.x && y in min.y..max.y && z in min.z..max.z -} - -/** - * Returns whether the given [Pos] is in the cylinder defined by the given [positionCylinder], [radius] and height defined by [limitY]. - * @receiver The position to check. - * @param positionCylinder The position of the cylinder. - * @param radius The radius of the cylinder. - * @param limitY The height of the cylinder. - * @return `true` if the position is in the cylinder, `false` otherwise. - */ -public fun Pos.isInCylinder(positionCylinder: Pos, radius: Double, limitY: ClosedRange): Boolean { - val distance = sqrt((x - positionCylinder.x).pow(2.0) + (z - positionCylinder.z).pow(2.0)) - return distance <= radius && y in limitY -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/PropertyExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/PropertyExt.kt deleted file mode 100644 index 177e9a0f..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/extension/PropertyExt.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.rushyverse.api.extension - -import net.minestom.server.network.packet.server.play.PlayerInfoPacket.AddPlayer -import net.minestom.server.network.player.GameProfile - -/** - * Create a [property][GameProfile.Property] for textures. - * @param textures Textures string. - * @param signature Texture signature. - * @return A property for textures. - */ -public fun GameProfileTextureProperty(textures: String, signature: String): GameProfile.Property = - GameProfile.Property("textures", textures, signature) - -/** - * Create a [property][AddPlayer.Property] for textures. - * @param textures Textures string. - * @param signature Texture signature. - * @return A property for textures. - */ -public fun AddPlayerTextureProperty(textures: String, signature: String): AddPlayer.Property = - AddPlayer.Property("textures", textures, signature) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/StringExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/StringExt.kt deleted file mode 100644 index 177cdf15..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/extension/StringExt.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.github.rushyverse.api.extension - -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.TextComponent -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.minimessage.MiniMessage -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver - -/** - * Default max line for a lore line. - * This value is defined by looking with the default Minecraft size application. - */ -public const val DEFAULT_LORE_LINE_LENGTH: Int = 30 - -/** - * Transform a sequence of strings to a component. - * Each string will be transformed into a component and then joined together by a new line. - * @receiver The sequence of strings to transform. - * @param transform The transform function to apply to each string. - * @return A component that contains all the strings. - */ -public inline fun Sequence.toLore( - crossinline transform: TextComponent.Builder.() -> Unit = { - color(NamedTextColor.GRAY) - } -): List { - return map { Component.text().content(it).apply(transform).build() }.toList() -} - -/** - * Transform a collection of strings to a component. - * Each string will be transformed into a component and then joined together by a new line. - * @receiver The collection of strings to transform. - * @param transform A function that will be applied to each component. - * @return A component that contains all the strings. - */ -public inline fun Collection.toLore( - crossinline transform: TextComponent.Builder.() -> Unit = { - color(NamedTextColor.GRAY) - } -): List { - if (isEmpty()) return emptyList() - return map { Component.text().content(it).apply(transform).build() } -} - -/** - * Transform a string into a list of string by cutting it. - * If the string is too large and doesn't have any space, it will be cut each [lineLength] characters and a '-' will be added. - * If the string contains a space, it will be cut at the space. - * @receiver String to transform. - * @param lineLength Max size of each string. - * @return A list with strings with length less or equals to [lineLength]. - */ -public fun String.toFormattedLore(lineLength: Int = DEFAULT_LORE_LINE_LENGTH): List { - return toFormattedLoreSequence(lineLength).toList() -} - -/** - * Transform a string into a sequence of string by cutting. - * If the string is too large and doesn't have any space, it will be cut each [lineLength] characters and a '-' will be added. - * If the string contains a space, it will be cut at the space. - * @receiver String to transform. - * @param lineLength Max size of each string. - * @return A sequence with strings with length less or equals to [lineLength]. - */ -public fun String.toFormattedLoreSequence(lineLength: Int = DEFAULT_LORE_LINE_LENGTH): Sequence { - if (isEmpty()) return emptySequence() - if (length <= lineLength) return sequenceOf(this) - - var index = 0 - return sequence { - while (index < length) { - val nextIndex = index + lineLength - if (nextIndex >= length) { - yield(substring(index)) - break - } - - val substringToNextIndex = substring(index, nextIndex) - val substringBeforeLastSpace = substringToNextIndex.substringBeforeLast(' ') - val nextChar = get(index + substringBeforeLastSpace.length) - - index += if (nextChar.isWhitespace()) { - yield(substringBeforeLastSpace) - // +1 to skip the space - substringBeforeLastSpace.length + 1 - } else { - yield(substringToNextIndex.dropLast(1) + '-') - substringToNextIndex.lastIndex - } - } - } -} - -/** - * Transforms a string into a component using MiniMessage. - * Will set the color according to the tag in the string. - * The [tagResolver] will be used to resolve the custom tags and replace values. - * @receiver The string used to create the component. - * @param tagResolver The tag resolver used to resolve the custom tags. - * @return The component created from the string. - */ -public fun String.asMiniComponent(vararg tagResolver: TagResolver): Component = - MiniMessage.miniMessage().deserialize(this, *tagResolver) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_CommandSender.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_CommandSender.kt new file mode 100644 index 00000000..ee24e26d --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_CommandSender.kt @@ -0,0 +1,36 @@ +package com.github.rushyverse.api.extension + +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.command.CommandSender + +/** + * Send a error message to a sender. + * @receiver Sender that will receive the message. + * @param message Message. + */ +public fun CommandSender.sendMessageError(message: String) { + sendMessage(text { + content(message) + color(NamedTextColor.RED) + }) +} + +/** + * Verify if a sender has several permissions. + * Iterate on all permissions and check the presence with the function [CommandSender.hasPermission]. + * @receiver Sender with permissions. + * @param permissions Bukkit Permissions. + * @return `true` if the sender has all permission, `false` otherwise. + */ +public fun CommandSender.hasPermissions(permissions: Array): Boolean = + permissions.all(this::hasPermission) + +/** + * Verify if a sender has several permissions. + * Iterate on all permissions and check the presence with the function [CommandSender.hasPermission]. + * @receiver Sender with permissions. + * @param permissions Bukkit Permissions. + * @return `true` if the sender has all permission, `false` otherwise. + */ +public fun CommandSender.hasPermissions(permissions: Iterable): Boolean = + permissions.all(this::hasPermission) diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/MathExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_Comparable.kt similarity index 81% rename from src/main/kotlin/com/github/rushyverse/api/extension/MathExt.kt rename to src/main/kotlin/com/github/rushyverse/api/extension/_Comparable.kt index 5a75ed35..4f0cdef5 100644 --- a/src/main/kotlin/com/github/rushyverse/api/extension/MathExt.kt +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_Comparable.kt @@ -2,7 +2,7 @@ package com.github.rushyverse.api.extension /** * Get the lowest and highest value in a specific index. - * The lowest value is placed at the index 0 and highest at the index 1. + * The lowest value is placed at index 0 and highest at index 1. * * @param a First value. * @param b Second value. @@ -12,4 +12,4 @@ public fun > minMaxOf(a: T, b: T): Pair = if (a <= b) { a to b } else { b to a -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/ComponentExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_Component.kt similarity index 57% rename from src/main/kotlin/com/github/rushyverse/api/extension/ComponentExt.kt rename to src/main/kotlin/com/github/rushyverse/api/extension/_Component.kt index 03c1e011..36e012b5 100644 --- a/src/main/kotlin/com/github/rushyverse/api/extension/ComponentExt.kt +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_Component.kt @@ -1,9 +1,91 @@ package com.github.rushyverse.api.extension -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.TextComponent +import net.kyori.adventure.text.* import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.standard.StandardTags import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * MiniMessage instance to serialize components with strict mode. + */ +private val MINI_MESSAGE_STRICT: MiniMessage = MiniMessage.builder() + .strict(true) + .tags(StandardTags.defaults()) + .build() + +/** + * Creates a text component using a builder. + * @param builder Function to build the component with the component builder. + * @return The component built. + */ +public inline fun text(builder: TextComponent.Builder.() -> Unit): TextComponent { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return Component.text().apply(builder).build() +} + +/** + * Append a component build into the current component builder. + * @receiver Component builder. + * @param builder Function to build the child component with a component builder. + * @return The current component builder. + */ +public inline fun TextComponent.Builder.appendText(builder: TextComponent.Builder.() -> Unit): TextComponent.Builder { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return append(Component.text().apply(builder).build()) +} + +/** + * Creates a keybind component using a builder. + * @param builder Function to build the component with the component builder. + * @return The component built. + */ +public inline fun keybind(builder: KeybindComponent.Builder.() -> Unit): KeybindComponent { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return Component.keybind().apply(builder).build() +} + +/** + * Creates a score component using a builder. + * @param builder Function to build the component with the component builder. + * @return The component built. + */ +public inline fun score(builder: ScoreComponent.Builder.() -> Unit): ScoreComponent { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return Component.score().apply(builder).build() +} + +/** + * Creates a block NBT component using builder. + * @param builder Function to build the component with the component builder. + * @return The component built. + */ +public inline fun blockNBT(builder: BlockNBTComponent.Builder.() -> Unit): BlockNBTComponent { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return Component.blockNBT().apply(builder).build() +} + +/** + * Creates an entity NBT component using builder. + * @param builder Function to build the component with the component builder. + * @return The component built. + */ +public inline fun entityNBT(builder: EntityNBTComponent.Builder.() -> Unit): EntityNBTComponent { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return Component.entityNBT().apply(builder).build() +} + +/** + * Creates a translatable component using builder. + * @param builder Function to build the component with the component builder. + * @return The component built. + */ +public inline fun translatable(builder: TranslatableComponent.Builder.() -> Unit): TranslatableComponent { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return Component.translatable().apply(builder).build() +} /** * Transform a component into a legacy text. @@ -70,7 +152,8 @@ public fun Component.withoutUnderlined(): Component = this.decoration(TextDecoration.UNDERLINED, TextDecoration.State.FALSE) /** - * Set as [not set][TextDecoration.State.NOT_SET] the [underlined][TextDecoration.UNDERLINED] decoration of the component. + * Set as [not set][TextDecoration.State.NOT_SET] the + * [underlined][TextDecoration.UNDERLINED] decoration of the component. * @receiver Component to transform. * @return The same component. */ @@ -94,7 +177,8 @@ public fun Component.withoutStrikethrough(): Component = this.decoration(TextDecoration.STRIKETHROUGH, TextDecoration.State.FALSE) /** - * Set as [not set][TextDecoration.State.NOT_SET] the [strikethrough][TextDecoration.STRIKETHROUGH] decoration of the component. + * Set as [not set][TextDecoration.State.NOT_SET] the + * [strikethrough][TextDecoration.STRIKETHROUGH] decoration of the component. * @receiver Component to transform. * @return The same component. */ @@ -117,7 +201,8 @@ public fun Component.withoutObfuscated(): Component = this.decoration(TextDecoration.OBFUSCATED, TextDecoration.State.FALSE) /** - * Set as [not set][TextDecoration.State.NOT_SET] the [obfuscated][TextDecoration.OBFUSCATED] decoration of the component. + * Set as [not set][TextDecoration.State.NOT_SET] the + * [obfuscated][TextDecoration.OBFUSCATED] decoration of the component. * @receiver Component to transform. * @return The same component. */ @@ -149,20 +234,10 @@ public fun Component.undefineDecorations(): Component = undefineBold().undefineItalic().undefineUnderlined().undefineStrikethrough().undefineObfuscated() /** - * Create a new component using the string content. - * @receiver String to transform. - * @param extractUrls If true, will extract urls from the string and apply a clickable effect on them. - * @param extractColors If true, will extract colors from the string and apply them to the component. - * @param colorChar The character used to define a color. - * @return A new text component. - */ -public fun String.toComponent( - extractUrls: Boolean = false, - extractColors: Boolean = true, - colorChar: Char = LegacyComponentSerializer.AMPERSAND_CHAR, -): TextComponent = LegacyComponentSerializer.builder().apply { - if (extractUrls) extractUrls() - if (extractColors) character(colorChar) -} - .build() - .deserialize(this) \ No newline at end of file + * Converts the component to its string representation using MiniMessage. + * + * @receiver The component to convert. + * @param mini The MiniMessage instance to use for serialization. + * @return The string representation of the component. + */ +public fun Component.asString(mini: MiniMessage = MINI_MESSAGE_STRICT): String = mini.serialize(this) diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_CoroutineScope.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_CoroutineScope.kt new file mode 100644 index 00000000..d1d700f5 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_CoroutineScope.kt @@ -0,0 +1,31 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.schedule.SchedulerTask +import kotlinx.coroutines.CoroutineScope +import kotlin.time.Duration + +/** + * Create a new scheduler with a task and run it. + * The [CoroutineScope] for the scheduler is a children of the [CoroutineScope] receiver. + * @receiver CoroutineScope to launch the task. + * @param delay Time to wait between each execution. + * @param body Task executed in the coroutine context each time. + * @return The instance of the scheduler where the task is run. + */ +public fun CoroutineScope.scheduledTask( + delay: Duration, + body: suspend SchedulerTask.Task.() -> Unit +): SchedulerTask = scheduler(delay).apply { + addUnsafe(body = body) + start() +} + +/** + * Create a new scheduler without a task. + * The [CoroutineScope] for the scheduler is a children of the [CoroutineScope] receiver. + * @receiver CoroutineScope to launch the task. + * @param delay Time to wait between each execution. + * @return The instance of the scheduler. + */ +public fun CoroutineScope.scheduler(delay: Duration): SchedulerTask = + SchedulerTask(this, delay) diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_Duration.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_Duration.kt new file mode 100644 index 00000000..454007d5 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_Duration.kt @@ -0,0 +1,164 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.time.FormatTime +import com.github.rushyverse.api.time.INFINITE_SYMBOL +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +/** + * Number of hours in a day. + */ +public const val HOUR_IN_DAY: Int = 24 + +/** + * Number of minutes in an hour. + */ +public const val MINUTE_IN_HOUR: Int = 60 + +/** + * Number of seconds in a minute. + */ +public const val SECOND_IN_MINUTE: Int = 60 + +/** + * Number of milliseconds corresponding to one tick. + */ +public const val MILLISECOND_PER_TICK: Int = 50 + +/** + * Get an instance of [Duration] corresponding to the time of ticks. + * 1 tick corresponding to 50 milliseconds, 20 ticks to 1 second. + */ +public val UInt.ticks: Duration get() = toInt().ticks + +/** + * Get an instance of [Duration] corresponding to the time of ticks. + * 1 tick corresponding to 50 milliseconds, 20 ticks to 1 second. + */ +public val Int.ticks: Duration get() = (this * MILLISECOND_PER_TICK).milliseconds + +/** + * Get an instance of [Duration] corresponding to the time of ticks. + * 1 tick corresponding to 50 milliseconds, 20 ticks to 1 second. + */ +public val ULong.ticks: Duration get() = toLong().ticks + +/** + * Get an instance of [Duration] corresponding to the time of ticks. + * 1 tick corresponding to 50 milliseconds, 20 ticks to 1 second. + */ +public val Long.ticks: Duration get() = (this * MILLISECOND_PER_TICK).milliseconds + +/** + * Get an instance of [Duration] corresponding to the time of ticks. + * 1 tick corresponding to 50 milliseconds, 20 ticks to 1 second. + */ +public val UShort.ticks: Duration get() = toShort().ticks + +/** + * Get an instance of [Duration] corresponding to the time of ticks. + * 1 tick corresponding to 50 milliseconds, 20 ticks to 1 second. + */ +public val Short.ticks: Duration get() = (this * MILLISECOND_PER_TICK).milliseconds + +/** + * Format a [Duration] to a string. + * + * If the duration is infinite, the [infiniteSymbol] will be used, for example `∞h ∞m ∞s`. + * + * Example: + * ```kotlin + * (1.hours + 2.minutes + 3.seconds).format(..) // 01h 02m 03s + * (2.minutes + 3.seconds).format(..) // 02m 03s + * (3.seconds).format(..) // 03s + * ``` + * + * @receiver Duration The duration to format. + * @param format The format to use. + * @param separator Use to separate the hour, minute and second + * @param infiniteSymbol Symbol to use when the duration is infinite. + * @return Formatted string. + */ +public fun Duration.format( + format: FormatTime, + separator: String = " ", + infiniteSymbol: String = INFINITE_SYMBOL +): String { + require(!this.isNegative()) { "Number must be positive" } + return buildString { + if (isInfinite()) { + formatInfiniteTime(format, separator, infiniteSymbol) + } else { + formatTime(format, this@format, separator) + } + + if (endsWith(separator)) { + deleteLast(separator.length) + } + } +} + +/** + * Format a [Duration] to a string. + * + * Example: + * ```kotlin + * formatTime(..) // 10d 01h 02m 03s + * ``` + * @receiver The string builder to append the formatted string. + * @param format The format to use. + * @param separator Use to separate the hour, minute and second + * @return Formatted string. + */ +private fun StringBuilder.formatTime( + format: FormatTime, + duration: Duration, + separator: String = " " +) { + var isFirstUnit = true + format.getDay(duration)?.let { + append(it) + append(separator) + isFirstUnit = false + } + + format.getHour(duration, isFirstUnit)?.let { + append(it) + append(separator) + isFirstUnit = false + } + + format.getMinute(duration, isFirstUnit)?.let { + append(it) + append(separator) + isFirstUnit = false + } + + format.getSecond(duration, isFirstUnit)?.let { + append(it) + } +} + +/** + * Format an infinite time to a string. + * + * Example: + * ```kotlin + * formatInfiniteTime(..) // ∞d ∞h ∞m ∞s + * ``` + * @receiver The string builder to append the formatted string. + * @param format The format to use. + * @param separator Use to separate the hour, minute and second + * @param infiniteSymbol Symbol to use when the duration is infinite. + * @return Formatted string. + */ +private fun StringBuilder.formatInfiniteTime( + format: FormatTime, + separator: String = " ", + infiniteSymbol: String = INFINITE_SYMBOL +) { + format.day?.let { append(it(infiniteSymbol)).append(separator) } + format.hour?.let { append(it(infiniteSymbol)).append(separator) } + format.minute?.let { append(it(infiniteSymbol)).append(separator) } + format.second?.let { append(it(infiniteSymbol)) } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_ItemStack.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_ItemStack.kt new file mode 100644 index 00000000..70341495 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_ItemStack.kt @@ -0,0 +1,269 @@ +package com.github.rushyverse.api.extension + +import org.bukkit.Material +import org.bukkit.inventory.ItemStack +import java.io.ByteArrayOutputStream +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Type of this item. + */ +public var ItemStack.material: Material + get() = type + set(value) { + type = value + } + +/** + * Utility method to build [ItemStack]. + * @param builder Builder function. + * @return Instance of the item who is created. + */ +public inline fun item(builder: ItemStack.() -> Unit): ItemStack { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return ItemStack(builder) +} + +/** + * Utility method to build [ItemStack]. + * @param builder Builder function. + * @return Instance of the item who is created. + */ +public inline fun ItemStack(builder: ItemStack.() -> Unit): ItemStack { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return ItemStack(Material.AIR, builder) +} + +/** + * Utility method to build [ItemStack]. + * @param material Item material. + * @param builder Builder function. + * @return Instance of the item who is created. + */ +public inline fun ItemStack(material: Material, builder: ItemStack.() -> Unit): ItemStack { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return ItemStack(material).apply(builder) +} + +/** + * Filter the array to retrieve items with only a type different of [Material.AIR]. + * @receiver Array of items. + * @return List of item without type [Material.AIR]. + */ +public fun Array.filterNotAir(): List = filter { it.type != Material.AIR } + +/** + * Filter the iterable to retrieve items with only a type different of [Material.AIR]. + * @receiver Iterable of items. + * @return List of item without type [Material.AIR]. + */ +public fun Iterable.filterNotAir(): List = filter { it.type != Material.AIR } + +/** + * Filter the sequence to retrieve items with only a type different of [Material.AIR]. + * @receiver Sequence of items. + * @return Sequence of item without type [Material.AIR]. + */ +public fun Sequence.filterNotAir(): Sequence = filter { it.type != Material.AIR } + +/** + * Get the content of the array to transform it into a map with the index and the item. + * Only the item not null and with a type different of [Material.AIR] is insert into the result. + * @receiver Array of item. + * @return Map of item with index. + */ +public fun Array.itemsIndexed(): Map { + return asIterable().itemsIndexed() +} + +/** + * Get the content of the sequence to transform it into a map with the index and the item. + * Only the item not null and with a type different of [Material.AIR] is insert into the result. + * @receiver Sequence of item. + * @return Map of item with index. + */ +public fun Sequence.itemsIndexed(): Map { + return asIterable().itemsIndexed() +} + +/** + * Get the content of the iterable instance to transform it into a map with the index and the item. + * Only the item not null and with a type different of [Material.AIR] is insert into the result. + * @receiver Iterable of item. + * @return Map of item with index. + */ +public fun Iterable.itemsIndexed(): Map { + val result = mutableMapOf() + forEachIndexed { index, item -> + if (item != null && item.type != Material.AIR) { + result[index] = item + } + } + return result +} + +/** + * Serialize a map of item with index to an encoded String with Base64. + * @receiver Items with indexes. + * @return The String encoded with items and indexes serialized. + */ +public fun Map.serializeToBase64(): String { + return serializeToBytes().encodeBase64ToString() +} + +/** + * Serialize a map of item with index to a byte array. + * @receiver Items with indexes. + * @return Byte array of items and indexes serialized. + */ +public fun Map.serializeToBytes(): ByteArray { + return ByteArrayOutputStream().use { os -> + os.buffered().use { + it.write(size) + + forEach { (index, item) -> + it.write(index) + val bytes = item.serializeAsBytes() + it.write(bytes.size) + it.write(bytes) + } + + it.flush() + os.toByteArray() + } + } +} + +/** + * Deserialize an encoded String with Base64 to a map of items and indexes. + * @receiver Items with indexes serialized. + * @return The map built from the String deserialized. + */ +public fun String.deserializeBase64ItemsIndexed(): Map { + return decodeBase64Bytes().deserializeItemsIndexed() +} + +/** + * Deserialize a byte array to a map of items and indexes. + * @receiver Items with indexes serialized. + * @return The map built from the byte array. + */ +public fun ByteArray.deserializeItemsIndexed(): Map { + return inputStream().buffered().use { + val size = it.read() + val itemsIndexed = HashMap(size) + + for (index in 0 until size) { + itemsIndexed[it.read()] = ItemStack.deserializeBytes(it.readNBytes(it.read())) + } + + itemsIndexed + } +} + +/** + * Serialize an array of item to an encoded String with Base64. + * + * @receiver Items. + * @return The String encoded with items serialized. + */ +public fun Array.serializeToBase64(): String { + return serializeToBytes().encodeBase64ToString() +} + +/** + * Serialize an array of item to a byte array. + * @receiver Items. + * @return Byte array of items serialized. + */ +public fun Array.serializeToBytes(): ByteArray { + return serializeItemsToBytes(iterator()) { size } +} + +/** + * Serialize a collection of items to an encoded String with Base64. + * @receiver Items. + * @return The String encoded with items serialized. + */ +public fun Collection.serializeToBase64(): String { + return serializeToBytes().encodeBase64ToString() +} + +/** + * Serialize a collection of items to a byte array. + * @receiver Items. + * @return Byte array of items serialized. + */ +public fun Collection.serializeToBytes(): ByteArray { + return serializeItemsToBytes(iterator()) { size } +} + +/** + * Serialize a set of item to a byte array. + * @param items Iterator of items. + * @param size Get the size of the iterator. + * @return Byte array of items serialized. + */ +public inline fun serializeItemsToBytes(items: Iterator, size: () -> Int): ByteArray { + return ByteArrayOutputStream().use { os -> + os.buffered().use { + val itSize = size() + it.write(itSize) + + var counter = 0 + while (items.hasNext() && counter < itSize) { + // With some part of the bukkit api, the item array + // is noted as @NotNull, but the content can be null. + val item = items.next() + val bytes = item.serializeAsBytes() + it.write(bytes.size) + it.write(bytes) + counter++ + } + + it.flush() + os.toByteArray() + } + } +} + +/** + * Deserialize an encoded String with Base64 to a list of items. + * @receiver Items serialized. + * @return The list built from the String deserialized. + */ +public fun String.deserializeBase64ItemsToList(): List { + return decodeBase64Bytes().deserializeItemsToList() +} + +/** + * Deserialize a byte array to a list of items. + * @receiver Items serialized. + * @return The list built from the byte array. + */ +public fun ByteArray.deserializeItemsToList(): List { + return inputStream().buffered().use { + List(it.read()) { _ -> ItemStack.deserializeBytes(it.readNBytes(it.read())) } + } +} + +/** + * Deserialize an encoded String with Base64 to an array of items. + * @receiver Items serialized. + * @return The array built from the String deserialized. + */ +public fun String.deserializeBase64ItemsToArray(): Array { + return decodeBase64Bytes().deserializeItemsToArray() +} + +/** + * Deserialize a byte array to an array of items. + * @receiver Items serialized. + * @return The array built from the byte array. + */ +public fun ByteArray.deserializeItemsToArray(): Array { + return inputStream().buffered().use { + Array(it.read()) { _ -> ItemStack.deserializeBytes(it.readNBytes(it.read())) } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_JavaPlugin.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_JavaPlugin.kt new file mode 100644 index 00000000..764a0b2d --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_JavaPlugin.kt @@ -0,0 +1,39 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.item.CraftBuilder +import com.github.shynixn.mccoroutine.bukkit.registerSuspendingEvents +import org.bukkit.NamespacedKey +import org.bukkit.event.Listener +import org.bukkit.plugin.java.JavaPlugin +import java.util.* +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + + +/** + * Register a new recipe craft in the server. + * @receiver Java plugin associated to the recipe. + * @param builder Builder to create recipe. + */ +public inline fun JavaPlugin.registerCraft( + key: String = UUID.randomUUID().toString().lowercase(), + builder: CraftBuilder.() -> Unit +): Boolean { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val craftBuilder = CraftBuilder().apply(builder) + val namespace = NamespacedKey(this, key) + val recipe = craftBuilder.build(namespace) + return server.addRecipe(recipe) +} + +/** + * Register a new suspendable listener for the plugin. + * Support listener with non-suspend and suspend methods. + * @receiver Instance of the plugin that will receive the listener. + * @param listenerBuilder Builder to create a listener. + */ +public inline fun JavaPlugin.registerListener(listenerBuilder: () -> Listener) { + contract { callsInPlace(listenerBuilder, InvocationKind.EXACTLY_ONCE) } + server.pluginManager.registerSuspendingEvents(listenerBuilder(), this) +} + diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_Location.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_Location.kt new file mode 100644 index 00000000..94b7a0a2 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_Location.kt @@ -0,0 +1,58 @@ +package com.github.rushyverse.api.extension + +import org.bukkit.Location +import org.bukkit.World + +/** + * Define the position (x, y, z, pitch, yaw) and the world with the same value as the location in parameter. + * @receiver Location that will have its values modified + * @param location Location where values will be retrieved. + */ +public fun Location.copyFrom(location: Location) { + world = location.world + x = location.x + y = location.y + z = location.z + pitch = location.pitch + yaw = location.yaw +} + +/** + * Create a copy of the location. + * @receiver Location using to create a new location with the same properties. + * @param world The world in which this location resides + * @param x The x-coordinate of this new location + * @param y The y-coordinate of this new location + * @param z The z-coordinate of this new location + * @param yaw The absolute rotation on the x-plane, in degrees + * @param pitch The absolute rotation on the y-plane, in degrees + * @return Location + */ +public fun Location.copy( + world: World = this.world, + x: Double = this.x, + y: Double = this.y, + z: Double = this.z, + yaw: Float = this.yaw, + pitch: Float = this.pitch, +): Location = Location(world, x, y, z, yaw, pitch) + +/** + * Divides each coordinate of the current location by the given value. + * + * @receiver The current location. + * @param value The value to divide each coordinate by. + * @return A new location with the divided coordinates. + */ +public fun Location.divide(value: Number): Location { + val toDouble = value.toDouble() + return copy(x = x / toDouble, y = y / toDouble, z = z / toDouble) +} + +/** + * Returns the center position between two points. + * @receiver The first position. + * @param other The second position. + * @return The center position between the two points. + */ +public fun Location.centerRelative(other: Location): Location = copy(yaw = 0f, pitch = 0f).add(other).divide(2) diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_Material.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_Material.kt new file mode 100644 index 00000000..492cc27a --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_Material.kt @@ -0,0 +1,20 @@ +package com.github.rushyverse.api.extension + +import org.bukkit.Material +import org.bukkit.Tag + +/** + * Checks whether the material is wool. + * + * @receiver Material the material to be checked + * @return `true` if the material is wool, `false` otherwise + */ +public fun Material.isWool(): Boolean = Tag.WOOL.isTagged(this) + +/** + * Checks if the material is a wool carpet. + * + * @receiver The material to check. + * @return `true` if the material is a wool carpet, `false` otherwise. + */ +public fun Material.isWoolCarpet(): Boolean = Tag.WOOL_CARPETS.isTagged(this) diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_MerchantRecipe.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_MerchantRecipe.kt new file mode 100644 index 00000000..b6f1bbec --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_MerchantRecipe.kt @@ -0,0 +1,51 @@ +package com.github.rushyverse.api.extension + +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.MerchantRecipe + +/** + * Constructor helper to create an instance of [MerchantRecipe][org.bukkit.inventory.MerchantRecipe]. + * @param result ItemStack + * @param maxUses The amount by which the demand influences + * the amount of the first ingredient is scaled by the recipe's. + * @param uses Number of times this trade has been used. + * @param experienceReward Whether to reward experience to the player for the trade. + * @param villagerExperience The Amount of experience the villager earns from this trade. + * @param priceMultiplier price multiplier, can never be below zero. + * @param demand This value is periodically updated by the + * villager that owns this merchant recipe based on how often the recipe has + * been used since it has been last restocked in relation to its. + * @param specialPrice This value is dynamically + * updated whenever a player starts and stops trading with a villager that owns + * this merchant recipe. It is based on the player's individual reputation with + * the villager, and the player's currently active status effects (see + * {@link PotionEffectType#HERO_OF_THE_VILLAGE}). The influence of the player's + * reputation on the special price is scaled by the recipe. + * @param ignoreDiscounts Whether all discounts on this trade should be ignored. + * @param ingredients List of ingredients necessary to obtain [result] item. + * @return New instance of [MerchantRecipe][org.bukkit.inventory.MerchantRecipe]. + */ +public fun MerchantRecipe( + result: ItemStack, + maxUses: Int, + uses: Int = 0, + experienceReward: Boolean = false, + villagerExperience: Int = 0, + priceMultiplier: Float = 0.0F, + demand: Int = 0, + specialPrice: Int = 0, + ignoreDiscounts: Boolean = false, + ingredients: List = emptyList() +): MerchantRecipe = MerchantRecipe( + result, + uses, + maxUses, + experienceReward, + villagerExperience, + priceMultiplier, + demand, + specialPrice, + ignoreDiscounts +).also { + it.ingredients = ingredients +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/NumberExt.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_Number.kt similarity index 97% rename from src/main/kotlin/com/github/rushyverse/api/extension/NumberExt.kt rename to src/main/kotlin/com/github/rushyverse/api/extension/_Number.kt index cf6a41c0..ef7e35a2 100644 --- a/src/main/kotlin/com/github/rushyverse/api/extension/NumberExt.kt +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_Number.kt @@ -46,7 +46,7 @@ public fun Int.toRomanNumerals(): String { var remaining = this var i = 0 return buildString { - while(remaining > 0) { + while (remaining > 0) { val romanValue = ROMAN_VALUES[i] repeat(remaining / romanValue) { append(ROMAN_NUMERALS[i]) @@ -55,4 +55,4 @@ public fun Int.toRomanNumerals(): String { i++ } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_PersistentDataHolder.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_PersistentDataHolder.kt new file mode 100644 index 00000000..7d69b82c --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_PersistentDataHolder.kt @@ -0,0 +1,17 @@ +package com.github.rushyverse.api.extension + +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataHolder +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Open the data container and manage info in it. + * @receiver PersistentDataHolder. + * @param block Function to use data container. + * @return Type of the return type. + */ +public inline fun PersistentDataHolder.dataContainer(block: PersistentDataContainer.() -> T): T { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return persistentDataContainer.block() +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_Player.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_Player.kt new file mode 100644 index 00000000..a1404525 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_Player.kt @@ -0,0 +1,38 @@ +package com.github.rushyverse.api.extension + +import com.destroystokyo.paper.profile.PlayerProfile +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Allows editing the profile of the player. + * The method must be called into the server thread. + */ +public inline fun Player.editProfile(editor: PlayerProfile.() -> Unit) { + contract { callsInPlace(editor, InvocationKind.EXACTLY_ONCE) } + playerProfile = playerProfile.apply(editor) +} + +/** + * Check if the [item] is equals to the one of the item in player's hands. + * @receiver Player. + * @param item Item to compare the item in hands. + * @return `true` if present in one of hands, `false` otherwise. + */ +public fun Player.itemInHand(item: ItemStack): Boolean = itemInHand { + it == item +} + +/** + * Use the lambda [finder] to check if a specific item is present in one of both hands. + * @receiver Player. + * @param finder Lambda method to compare item. + * @return `true` if present in one of hands, `false` otherwise. + */ +public inline fun Player.itemInHand(finder: (ItemStack) -> Boolean): Boolean { + contract { callsInPlace(finder, InvocationKind.AT_LEAST_ONCE) } + val inventory = inventory + return finder(inventory.itemInMainHand) || finder(inventory.itemInOffHand) +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_PlayerProfile.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_PlayerProfile.kt new file mode 100644 index 00000000..59c75a5f --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_PlayerProfile.kt @@ -0,0 +1,27 @@ +package com.github.rushyverse.api.extension + +import com.destroystokyo.paper.profile.PlayerProfile +import com.destroystokyo.paper.profile.ProfileProperty + +/** + * Name of the property to set and get textures. + */ +public const val PROPERTY_TEXTURES: String = "textures" + +/** + * Set a new texture for the profile. + * @receiver Profile of a player. + * @param skin Skin in string format. + * @param signature Signature of the skin for validation. + */ +public fun PlayerProfile.setTextures(skin: String, signature: String? = null) { + setProperty(ProfileProperty(PROPERTY_TEXTURES, skin, signature)) +} + +/** + * Find the property containing the textures' data. + * @receiver Profile of a player. + * @return The property with the name, `null` otherwise. + */ +public fun PlayerProfile.getTexturesProperty(): ProfileProperty? = + properties.find { it.name == PROPERTY_TEXTURES } diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_Runnable.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_Runnable.kt new file mode 100644 index 00000000..9da15608 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_Runnable.kt @@ -0,0 +1,43 @@ +package com.github.rushyverse.api.extension + +import com.github.shynixn.mccoroutine.bukkit.SuspendingPlugin +import com.github.shynixn.mccoroutine.bukkit.asyncDispatcher +import com.github.shynixn.mccoroutine.bukkit.minecraftDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import org.bukkit.scheduler.BukkitRunnable + +/** + * Create new bukkit runnable with the body as the function sent in parameter. + * @param task Function that will be processed when the bukkit runnable will be run. + * @return New instance of bukkit runnable. + */ +public inline fun BukkitRunnable(crossinline task: BukkitRunnable.() -> Unit): BukkitRunnable { + return object : BukkitRunnable() { + override fun run() { + task() + } + } +} + +/** + * Execute the function into the primary server thread in bukkit runnable. + * The current coroutine will be suspended. + * @param block Code that will be executed in the primary thread of the server. + * @return T Instance returned after the execution of the task. + */ +public suspend inline fun onPrimaryThread( + plugin: SuspendingPlugin, + noinline block: suspend CoroutineScope.() -> T +): T = withContext(plugin.minecraftDispatcher, block) + +/** + * Execute the function asynchronously into a bukkit thread (other than primary thread). + * The current coroutine will be suspended. + * @param block Code that will be executed in a second thread of bukkit. + * @return T Instance returned after the execution of the task. + */ +public suspend inline fun onAsyncThread( + plugin: SuspendingPlugin, + noinline block: suspend CoroutineScope.() -> T +): T = withContext(plugin.asyncDispatcher, block) diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_String.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_String.kt new file mode 100644 index 00000000..59523834 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_String.kt @@ -0,0 +1,265 @@ +@file:JvmName("StringUtils") +@file:JvmMultifileClass + +package com.github.rushyverse.api.extension + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.TextComponent +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import net.kyori.adventure.text.minimessage.tag.standard.StandardTags +import java.math.BigInteger +import java.util.* + +/** + * MiniMessage instance to deserialize components without strict mode. + */ +public val MINI_MESSAGE_NON_STRICT: MiniMessage = MiniMessage.builder() + .strict(false) + .tags(StandardTags.defaults()) + .build() + +/** + * Length of a UUID. + */ +public const val UUID_SIZE: Int = 36 + +/** + * Number of dashes in a UUID. + */ +private const val NUMBER_DASHES_UUID = 4 + +/** + * Radix for hexadecimal numbers. + */ +private const val HEXADECIMAL_RADIX = 16 + +/** + * The constant value representing the number of bits in a UUID high and low bits. + */ +private const val UUID_HIGH_LOW_BITS: Int = 64 + +/** + * Default max line for a lore line. + * This value is defined by looking with the default Minecraft size application. + */ +public const val DEFAULT_LORE_LINE_LENGTH: Int = 30 + +/** + * Converts the receiver String to Int if possible, otherwise returns the same String. + * + * @receiver The input String. + * @return The converted Int value if successful or the original String itself if not. + */ +public fun String.toIntOrString(): Any = toIntOrNull() ?: this + +/** + * Wraps a given string with a color tag. + * Example: "Hello".wrapColorWith("red") will return `Hello`. + * + * @receiver The string to be wrapped. + * @param color The color to use for wrapping. + * @return The original string wrapped with the color tag. + */ +public infix fun String.withColor(color: String): String = "<$color>$this" + +/*** + * Encodes the specified byte array into a String using the [Base64] encoding scheme. + * @receiver String to encode. + * @return A String containing the resulting Base64 encoded characters. + */ +public fun String.encodeBase64ToString(): String = Base64.getEncoder().encodeToString(this.toByteArray(Charsets.UTF_8)) + +/*** + * Encodes the specified byte array into a String using the [Base64] encoding scheme. + * @receiver Array of byte to encode. + * @return A byte array containing the resulting Base64 encoded characters. + */ +public fun ByteArray.encodeBase64ToString(): String = Base64.getEncoder().encodeToString(this) + +/** + * Decodes a Base64 encoded String into a newly-allocated byte array using the [Base64] encoding scheme. + * @receiver String to decode. + * @return A new String containing the decoded bytes. + */ +public fun String.decodeBase64ToString(): String = Base64.getDecoder().decode(this).decodeToString() + +/** + * Decodes a Base64 encoded String into a newly-allocated byte array using the [Base64] encoding scheme. + * @receiver Array of byte< to decode. + * @return A byte array containing the decoded bytes. + */ +public fun String.decodeBase64Bytes(): ByteArray = Base64.getDecoder().decode(this) + +/** + * Creates a [UUID] from the string standard. + * The string must have the strict format of UUID (with dashes). + * If the string cannot be converted to UUID, returns null. + * @receiver String with UUID format. + * @return The UUID instance equals to the string value or null if the format is not valid. + */ +public fun String.toUUIDStrictOrNull(): UUID? = try { + toUUIDStrict() +} catch (_: Exception) { + null +} + +/** + * Creates a [UUID] from the string standard. + * The string must have the strict format of UUID (with dashes). + * If the string cannot be converted to UUID, throw an exception. + * @receiver String with UUID format. + * @return The UUID instance equals to the string value. + * @throws IllegalArgumentException Exception if the value is not a valid uuid. + */ +@Throws(IllegalArgumentException::class) +public fun String.toUUIDStrict(): UUID = UUID.fromString(this) + +/** + * Creates a [UUID] from a string. + * The string can have dashes or not. + * If the string cannot be converted to UUID, returns null. + * @receiver String with UUID format. + * @return The UUID instance equals to the string value or null if the format is not valid. + */ +public fun String.toUUIDOrNull(): UUID? = try { + toUUID() +} catch (_: Exception) { + null +} + +/** + * Creates a [UUID] from a string. + * The string can have dashes or not. + * If the string cannot be converted to UUID, throw an exception. + * @receiver String with UUID format. + * @return The UUID instance equals to the string value. + * @throws IllegalArgumentException Exception if the value is not a valid uuid. + */ +@Throws(IllegalArgumentException::class) +public fun String.toUUID(): UUID { + val length = this.length + if (length == UUID_SIZE) { + return toUUIDStrict() + } else { + if (length == UUID_SIZE - NUMBER_DASHES_UUID) { // -4 because of dashes + val idHex = BigInteger(this, HEXADECIMAL_RADIX) + return UUID(idHex.shiftRight(UUID_HIGH_LOW_BITS).toLong(), idHex.toLong()) + } + } + throw IllegalArgumentException("Invalid UUID format: $this") +} + +/** + * Transform a sequence of strings to a component. + * Each string will be transformed into a component and then joined together by a new line. + * @receiver The sequence of strings to transform. + * @param transform The transform function to apply to each string. + * @return A component that contains all the strings. + */ +public inline fun Sequence.toLore( + crossinline transform: TextComponent.Builder.() -> Unit = { + color(NamedTextColor.GRAY) + } +): List { + return map { Component.text().content(it).apply(transform).build() }.toList() +} + +/** + * Transform a collection of strings to a component. + * Each string will be transformed into a component and then joined together by a new line. + * @receiver The collection of strings to transform. + * @param transform A function that will be applied to each component. + * @return A component that contains all the strings. + */ +public inline fun Collection.toLore( + crossinline transform: TextComponent.Builder.() -> Unit = { + color(NamedTextColor.GRAY) + } +): List { + if (isEmpty()) return emptyList() + return map { Component.text().content(it).apply(transform).build() } +} + +/** + * Transform a string into a list of string by cutting it. + * If the string is too large and doesn't have any space, + * it will be cut each [lineLength] characters and a '-' will be added. + * If the string contains a space, it will be cut at the space. + * @receiver String to transform. + * @param lineLength Max size of each string. + * @return A list with strings with length less or equals to [lineLength]. + */ +public fun String.toFormattedLore(lineLength: Int = DEFAULT_LORE_LINE_LENGTH): List { + return toFormattedLoreSequence(lineLength).toList() +} + +/** + * Transform a string into a sequence of string by cutting. + * If the string is too large and doesn't have any space, + * it will be cut each [lineLength] characters and a '-' will be added. + * If the string contains a space, it will be cut at the space. + * @receiver String to transform. + * @param lineLength Max size of each string. + * @return A sequence with strings with length less or equals to [lineLength]. + */ +public fun String.toFormattedLoreSequence(lineLength: Int = DEFAULT_LORE_LINE_LENGTH): Sequence { + if (isEmpty()) return emptySequence() + if (length <= lineLength) return sequenceOf(this) + + var index = 0 + return sequence { + while (index < length) { + val nextIndex = index + lineLength + if (nextIndex >= length) { + yield(substring(index)) + break + } + + val substringToNextIndex = substring(index, nextIndex) + val substringBeforeLastSpace = substringToNextIndex.substringBeforeLast(' ') + val nextChar = get(index + substringBeforeLastSpace.length) + + index += if (nextChar.isWhitespace()) { + yield(substringBeforeLastSpace) + // +1 to skip the space + substringBeforeLastSpace.length + 1 + } else { + yield(substringToNextIndex.dropLast(1) + '-') + substringToNextIndex.lastIndex + } + } + } +} + +/** + * Transforms a string into a component using MiniMessage. + * Will set the color according to the tag in the string. + * The [tagResolver] will be used to resolve the custom tags and replace values. + * @receiver The string used to create the component. + * @param tagResolver The tag resolver used to resolve the custom tags. + * @param miniMessage The mini message instance used to parse the string. + * @return The component created from the string. + */ +public fun String.asComponent( + vararg tagResolver: TagResolver, + miniMessage: MiniMessage = MINI_MESSAGE_NON_STRICT, +): Component = miniMessage.deserialize(this, *tagResolver) + +/** + * Deletes the last [size] characters from the `StringBuilder`. + * + * @receiver the [StringBuilder] to delete from. + * @param size the number of characters to delete. + * @return the modified [StringBuilder] after deleting the characters. + * @throws IllegalArgumentException if [StringBuilder] is negative. + */ +public fun StringBuilder.deleteLast(size: Int): StringBuilder { + return when { + size < 0 -> throw IllegalArgumentException("Size must be positive, but was $size") + size == 0 -> this + size >= length -> clear() + else -> delete(length - size, length) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_Villager.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_Villager.kt new file mode 100644 index 00000000..e508bade --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_Villager.kt @@ -0,0 +1,47 @@ +package com.github.rushyverse.api.extension + +import org.bukkit.NamespacedKey +import org.bukkit.entity.Villager +import org.bukkit.persistence.PersistentDataType +import org.bukkit.plugin.Plugin + +/** + * Key of the [NamespacedKey] to define if a villager must be keep his job or lose it. + */ +private const val KEY_KEEP_JOB = "KEEP_JOB" + +/** + * Create an instance of [NamespacedKey] corresponding to the key about the keep of profession for a villager. + * @param plugin Plugin to use for the namespace. + * @return New instance of [NamespacedKey]. + */ +public fun namespacedKeyKeepJob(plugin: Plugin): NamespacedKey = NamespacedKey(plugin, KEY_KEEP_JOB) + +/** + * Check if a villager must be keep his profession. + * @receiver Villager. + * @param plugin Plugin to find the data. + * @return `true` if the villager must be keep his job, `false` otherwise. + */ +public fun Villager.keepProfession(plugin: Plugin): Boolean { + return dataContainer { + get(namespacedKeyKeepJob(plugin), PersistentDataType.BYTE) + } != null +} + +/** + * Define if a villager must be keep his profession or not. + * @receiver Villager. + * @param plugin Plugin to define the data. + * @param keep `true` if the villager must be keep his job, `false` otherwise. + */ +public fun Villager.keepProfession(plugin: Plugin, keep: Boolean) { + dataContainer { + val key = namespacedKeyKeepJob(plugin) + if (keep) { + set(key, PersistentDataType.BYTE, 0) + } else { + remove(key) + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/_World.kt b/src/main/kotlin/com/github/rushyverse/api/extension/_World.kt new file mode 100644 index 00000000..968afd84 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/_World.kt @@ -0,0 +1,60 @@ +package com.github.rushyverse.api.extension + +import kotlinx.coroutines.future.await +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.Block + +/** + * Requests a [Chunk] to be loaded at the given coordinates; + * + * This method makes no guarantee on how fast the chunk will load and will return the chunk when finished. + * + * You should use this method if you need a chunk but do not need it + * immediately, and you wish to let the server control the speed + * of chunk loads, keeping performance in mind. + * + * @receiver World where the chunk will be retrieved. + * @param block Block into the chunk must be retrieved. + * @param gen `true` to generate the chunk if necessary, `false` otherwise. + * @return The chunk retrieved. + */ +public suspend fun World.awaitChunkAt(block: Block, gen: Boolean = true): Chunk = + getChunkAtAsync(block, gen).await() + +/** + * Requests a [Chunk] to be loaded at the given coordinates; + * + * This method makes no guarantee on how fast the chunk will load and will return the chunk when finished. + * + * You should use this method if you need a chunk but do not need it + * immediately, and you wish to let the server control the speed + * of chunk loads, keeping performance in mind. + * + * @receiver World where the chunk will be retrieved. + * @param location Location to load the corresponding chunk from. + * @param gen `true` to generate the chunk if necessary, `false` otherwise. + * @return The chunk retrieved. + */ +public suspend fun World.awaitChunkAt(location: Location, gen: Boolean = true): Chunk = + getChunkAtAsync(location, gen).await() + +/** + * Requests a [Chunk] to be loaded at the given coordinates; + * + * This method makes no guarantee on how fast the chunk will load and will return the chunk when finished. + * + * You should use this method if you need a chunk but do not need it + * immediately, and you wish to let the server control the speed + * of chunk loads, keeping performance in mind. + * + * @receiver World where the chunk will be retrieved. + * @param x X coord. + * @param z Z coord. + * @param gen `true` to generate the chunk if necessary, `false` otherwise. + * @param urgent `true` if the load must have the priority, `false` otherwise. + * @return The chunk retrieved. + */ +public suspend fun World.awaitChunkAt(x: Int, z: Int, gen: Boolean = true, urgent: Boolean = false): Chunk = + getChunkAtAsync(x, z, gen, urgent).await() diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/event/_Cancellable.kt b/src/main/kotlin/com/github/rushyverse/api/extension/event/_Cancellable.kt new file mode 100644 index 00000000..9f593e97 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/event/_Cancellable.kt @@ -0,0 +1,19 @@ +package com.github.rushyverse.api.extension.event + +import org.bukkit.event.Cancellable + +/** + * Extension function allowing to cancel the current process by method calling. + */ +public fun Cancellable.cancel() { + isCancelled = true +} + +/** + * Extension function allowing to cancel the current process by method calling with a condition. + */ +public inline fun T.cancelIf(condition: T.() -> Boolean) { + if (condition()) { + cancel() + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/event/_Event.kt b/src/main/kotlin/com/github/rushyverse/api/extension/event/_Event.kt new file mode 100644 index 00000000..f1f1e25a --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/event/_Event.kt @@ -0,0 +1,17 @@ +package com.github.rushyverse.api.extension.event + +import org.bukkit.entity.Damageable +import org.bukkit.event.entity.EntityDamageEvent + +/** + * Future life of the damaged entity. + * @return If the entity is not damageable returns null, otherwise return the future health. + */ +public fun EntityDamageEvent.finalDamagedHealth(): Double? { + val damaged = entity + return if (damaged is Damageable) { + damaged.health - finalDamage + } else { + null + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/extension/event/_Listener.kt b/src/main/kotlin/com/github/rushyverse/api/extension/event/_Listener.kt new file mode 100644 index 00000000..58afa84c --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/extension/event/_Listener.kt @@ -0,0 +1,94 @@ +package com.github.rushyverse.api.extension.event + +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import org.bukkit.event.Event +import org.bukkit.event.EventPriority +import org.bukkit.event.HandlerList +import org.bukkit.event.Listener +import org.bukkit.plugin.java.JavaPlugin +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.time.Duration + +/** + * Unregister the listener. + */ +public fun Listener.unregister() { + HandlerList.unregisterAll(this) +} + +/** + * Wait events corresponding to an event type. + * If the function [block] doesn't valid an event, stop the listening after the duration of [timeout]. + * Register a new listener to retrieve event. + * + * The current coroutine is suspended. When the coroutine is cancellable or resume, unregister the listener. + * @param plugin Java plugin to register the listener. + * @param priority Priority to register this event at. + * @param ignoreCancelled Whether to pass canceled events or not. + * @param block Function to treat the received event, + * returns `true` to valid the event and stop the listening, `false` otherwise. + */ +public suspend inline fun waitEvent( + plugin: JavaPlugin, + timeout: Duration, + priority: EventPriority = EventPriority.NORMAL, + ignoreCancelled: Boolean = false, + crossinline block: T.() -> Boolean +) { + withTimeoutOrNull(timeout) { + waitEvent( + plugin = plugin, + priority = priority, + ignoreCancelled = ignoreCancelled, + block = block + ) + } +} + +/** + * Wait events corresponding to an event type. + * Register a new listener to retrieve event. + * + * The current coroutine is suspended. When the coroutine is cancellable or resume, unregister the listener. + * @param plugin Java plugin to register the listener. + * @param priority Priority to register this event at. + * @param ignoreCancelled Whether to pass canceled events or not. + * @param block Function to treat the received event, + * returns `true` to valid the event and stop the listening, `false` otherwise. + */ +public suspend inline fun waitEvent( + plugin: JavaPlugin, + priority: EventPriority = EventPriority.NORMAL, + ignoreCancelled: Boolean = false, + crossinline block: T.() -> Boolean +) { + val listener = object : Listener {} + + suspendCancellableCoroutine { cont -> + cont.invokeOnCancellation { + listener.unregister() + } + + plugin.server.pluginManager.registerEvent( + T::class.java, + listener, + priority, + { _, event -> + if (event !is T) return@registerEvent + try { + if (event.block()) { + listener.unregister() + cont.resume(Unit) + } + } catch (ex: Throwable) { + listener.unregister() + cont.resumeWithException(ex) + } + }, + plugin, + ignoreCancelled + ) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/game/GameData.kt b/src/main/kotlin/com/github/rushyverse/api/game/GameData.kt new file mode 100644 index 00000000..0266b918 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/game/GameData.kt @@ -0,0 +1,16 @@ +package com.github.rushyverse.api.game + +/** + * Represents the data of a game. + * This class can be used to communicate any game information across the framework. + * @param type The type of the game. + * @param id The id of the game. + * @param players The players count in the game. + * @param state The state of the game. It is [GameState.WAITING] by default. + */ +public data class GameData( + val type: String, + val id: Int, + var players: Int = 0, + var state: GameState = GameState.WAITING, +) diff --git a/src/main/kotlin/com/github/rushyverse/api/game/GameState.kt b/src/main/kotlin/com/github/rushyverse/api/game/GameState.kt new file mode 100644 index 00000000..7029f34e --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/game/GameState.kt @@ -0,0 +1,33 @@ +package com.github.rushyverse.api.game + +/** + * Represents the various states a game can be in at any given moment. + * Each state corresponds to a different phase in the game's lifecycle. + */ +public enum class GameState { + + /** + * Represents the state where the game is waiting for necessary conditions to start. + * This could be waiting for more players to join, or waiting for some setup process to finish. + */ + WAITING, + + /** + * Represents the state when the game is in the process of starting. + * This is a transitional phase, initialization of game resources, + * a countdown timer before the game starts, etc. + */ + STARTING, + + /** + * Represents the state where the game has officially started. + * Gameplay is active during this state. + */ + STARTED, + + /** + * Represents the state when the game is in the process of ending. + * This is a transitional phase, where final scores might be calculated, game resources might be cleaned up, etc. + */ + ENDING; +} diff --git a/src/main/kotlin/com/github/rushyverse/api/game/SharedGameData.kt b/src/main/kotlin/com/github/rushyverse/api/game/SharedGameData.kt new file mode 100644 index 00000000..f3183603 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/game/SharedGameData.kt @@ -0,0 +1,93 @@ +package com.github.rushyverse.api.game + +/** + * Represents a shared container for game data across multiple games. + * It provides utility functions to fetch player count, game state, and + * allows listeners to be notified of changes in the game data. + */ +public class SharedGameData { + + /** + * A list to hold the data for all active games. + */ + public val games: MutableList = mutableListOf() + + // A set of listeners that get called when there's a change in the game data. + private val onChange: MutableSet<() -> Unit> = mutableSetOf() + + /** + * Calculates the total number of players across all games. + * + * @return Total number of players. + */ + public fun players(): Int = games.sumOf { it.players } + + /** + * Calculates the total number of players in a specific type of game. + * + * @param gameType The type of game to filter by. + * @return Total number of players for the specified game type. + */ + public fun players(gameType: String): Int = games.filter { it.type == gameType }.sumOf { it.players } + + /** + * Retrieves the number of players in a specific game identified by its type and ID. + * + * @param gameType The type of game. + * @param gameId The unique ID of the game. + * @return Number of players in the specified game, or 0 if the game is not found. + */ + public fun players(gameType: String, gameId: Int): Int = + games.firstOrNull { it.type == gameType && it.id == gameId }?.players ?: 0 + + /** + * Retrieves the state of a specific game identified by its type and ID. + * + * @param gameType The type of game. + * @param gameId The unique ID of the game. + * @return GameState of the specified game, or GameState.WAITING if the game is not found. + */ + public fun state(gameType: String, gameId: Int): GameState = + games.distinctBy { it.type == gameType }.firstOrNull { it.id == gameId }?.state ?: GameState.WAITING + + /** + * Counts the number of games for a specific type. + * + * @param gameType The type of game to filter by. + * @return The count of games of the specified type. + */ + public fun games(gameType: String): Int = games.count { it.type == gameType } + + /** + * Subscribes a listener that gets called when the game data changes. + * + * @param unit The callback function to be invoked upon changes. + */ + public fun subscribeOnChange(unit: () -> Unit) { + onChange.add(unit) + } + + /** + * Invokes all the registered listeners to notify about a change in game data. + */ + public fun callOnChange() { + onChange.forEach { it.invoke() } + } + + /** + * Updates an existing game's data or adds a new game to the list. + * After, it notifies all registered listeners about the change. + * + * @param gameData The game data to be updated or added. + */ + public fun saveUpdate(gameData: GameData) { + val index = games.indexOf(gameData) + if (index == -1) { + games.add(gameData) + } else { + games[index] = gameData + } + + callOnChange() + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/game/stats/KillableStats.kt b/src/main/kotlin/com/github/rushyverse/api/game/stats/KillableStats.kt new file mode 100644 index 00000000..7b9b7ed9 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/game/stats/KillableStats.kt @@ -0,0 +1,25 @@ +package com.github.rushyverse.api.game.stats + +/** + * Represents statistics associated with players that can be killed within a game. + * These statistics include kill and death counts, and also provides a method to calculate the score based on them. + */ +public open class KillableStats( + /** + * The number of kills achieved by the entity/player. + */ + public var kills: Int = 0, + + /** + * The number of times the entity/player has been killed. + */ + public var deaths: Int = 0 +) : Stats { + + public override fun calculateScore(): Int { + val score = kills - deaths + if (score < 0) + return 0 + return score + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/game/stats/Stats.kt b/src/main/kotlin/com/github/rushyverse/api/game/stats/Stats.kt new file mode 100644 index 00000000..18487569 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/game/stats/Stats.kt @@ -0,0 +1,16 @@ +package com.github.rushyverse.api.game.stats + +/** + * A functional interface that represents game statistics. + * Implementing classes/entities are expected to provide a mechanism + * to calculate a score based on their specific game-related stats. + */ +public fun interface Stats { + + /** + * Calculates the score based on the implementing class's/game entity's statistics. + * + * @return The calculated score as an integer value. + */ + public fun calculateScore(): Int +} diff --git a/src/main/kotlin/com/github/rushyverse/api/game/stats/WinnableStats.kt b/src/main/kotlin/com/github/rushyverse/api/game/stats/WinnableStats.kt new file mode 100644 index 00000000..a9efb3cc --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/game/stats/WinnableStats.kt @@ -0,0 +1,24 @@ +package com.github.rushyverse.api.game.stats + +/** + * Represents statistics associated with entities or players that participate in win-lose scenarios within a game. + * These statistics include win and loss counts, and also provides a method to calculate the score based on them. + */ +public open class WinnableStats( + /** + * The number of times the player has won. + */ + public var wins: Int = 0, + /** + * The number of times the player has lost. + */ + public var loses: Int = 0 +) : Stats { + + override fun calculateScore(): Int { + val score = wins - loses + if (score < 0) + return 0 + return score + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/game/team/TeamType.kt b/src/main/kotlin/com/github/rushyverse/api/game/team/TeamType.kt new file mode 100644 index 00000000..cf325cc9 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/game/team/TeamType.kt @@ -0,0 +1,40 @@ +package com.github.rushyverse.api.game.team + +import com.github.rushyverse.api.APIPlugin.Companion.BUNDLE_API +import com.github.rushyverse.api.translation.SupportedLanguage +import com.github.rushyverse.api.translation.Translator +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextColor +import java.util.* + +/** + * Enum that defines the supported team types for a game, each associated with a specific color. + * + * @property color The color associated with the team, represented by a `TextColor` object. + */ +public enum class TeamType( + public val color: TextColor +) { + + WHITE(NamedTextColor.WHITE), + RED(NamedTextColor.RED), + BLUE(NamedTextColor.BLUE), + GREEN(NamedTextColor.GREEN), + YELLOW(NamedTextColor.YELLOW), + PURPLE(NamedTextColor.LIGHT_PURPLE), + AQUA(NamedTextColor.AQUA), + ORANGE(NamedTextColor.GOLD), + BLACK(NamedTextColor.BLACK); + + /** + * Provides the translated name of the team based on the provided locale. + * + * @param translator The translation provider that fetches translations from a bundle of files. + * @param locale The target locale for the translation, with a default of English. + * @return The translated name of the team. + */ + public fun name( + translator: Translator, + locale: Locale = SupportedLanguage.ENGLISH.locale + ): String = translator.get("team.${name.lowercase()}", locale, bundleName = BUNDLE_API) +} diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt deleted file mode 100644 index d375335b..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImage.kt +++ /dev/null @@ -1,254 +0,0 @@ -package com.github.rushyverse.api.image - -import com.github.rushyverse.api.image.exception.ImageAlreadyLoadedException -import com.github.rushyverse.api.image.exception.ImageNotLoadedException -import com.github.rushyverse.api.image.exception.ItemFramesAlreadyExistException -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.future.asDeferred -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.Entity -import net.minestom.server.entity.EntityType -import net.minestom.server.entity.metadata.other.ItemFrameMeta -import net.minestom.server.instance.Instance -import net.minestom.server.item.ItemStack -import net.minestom.server.item.Material -import net.minestom.server.item.metadata.MapMeta -import net.minestom.server.map.framebuffers.LargeGraphics2DFramebuffer -import net.minestom.server.network.packet.server.SendablePacket -import org.jetbrains.annotations.Blocking -import java.awt.geom.AffineTransform -import java.awt.image.BufferedImage -import java.io.InputStream -import javax.imageio.ImageIO -import kotlin.properties.Delegates - -/** - * Read an image from the resources and build the packets to send to the players. - * @see loadImageAsPacketsFromInputStream - * @receiver Object to display image on the server. - * @param resourceImage Path of the image in the resources. - * @param modifyTransform Function to modify the transform of the image. - * @return The packets list to send to players. - */ -@Blocking -public fun MapImage.loadImageAsPacketsFromResources( - resourceImage: String, - modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} -): Array { - val inputStream = MapImage::class.java.getResourceAsStream("/$resourceImage") - ?: error("Unable to retrieve the image $resourceImage in resources.") - - return inputStream.buffered().use { loadImageAsPacketsFromInputStream(it, modifyTransform) } -} - -/** - * Read an image from an input stream and build the packets to send to the players. - * **This method does not close the provided [inputStream] after the read operation has completed. - * It is the responsibility of the caller to close the stream, if desired.** - * @see [MapImage.loadImageAsPackets] - * @receiver Object to display image on the server. - * @param inputStream Input stream to retrieve the image's data. - * @param modifyTransform Function to modify the transform of the image. - * @return The packets list to send to players. - */ -@Blocking -public fun MapImage.loadImageAsPacketsFromInputStream( - inputStream: InputStream, - modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} -): Array { - val image = ImageIO.read(inputStream) - return loadImageAsPackets(image, modifyTransform) -} - -/** - * A class that allows you to create an Image as Map Item Frame on the server. - * @property packets The packets list to send to new players. - * @property itemFramesPerLine The width blocks size desired for the item frame. The value define the number of item frames by line. - * @property itemFramesPerColumn The height blocks size desired for the item frame. The value define the number of item frames by column. - * @property numberOfItemFrames The number of item frames needed to display the image. - * @property imageLoaded `true` if the image is loaded, `false` otherwise. - * @property itemFrames The list of item frames created. - */ -public class MapImage { - - public companion object { - - /** - * The number of pixels per item frame is 128x128. - */ - public const val MAP_ITEM_FRAME_PIXELS: Int = 128 - - /** - * The number of pixels per item frame is 128. - * So to improve the performance, we will use the bitwise operator to divide by 128. - */ - private const val MAP_ITEM_FRAME_PIXELS_BITWISE = 7 - } - - public var packets: Array? = null - private set - - public var itemFramesPerLine: Int by Delegates.notNull() - private set - - public var itemFramesPerColumn: Int by Delegates.notNull() - private set - - public val imageLoaded: Boolean - get() = packets != null - - private var _itemFrames: List? = null - - public val itemFrames: List? - get() = _itemFrames - - private val numberOfItemFrames: Int - get() = itemFramesPerLine * itemFramesPerColumn - - /** - * Create the packets list to send to new players. - * The result is stored in the [packets] property. - * - * **This method does not close the provided [inputStream] after the read operation has completed. - * It is the responsibility of the caller to close the stream, if desired.** - * - * @param image The image to display. - * @param modifyTransform The function to apply transformation to the image. By default, the image is turned upside down. - * For example, to rotate the image of 90° clockwise, you can use the following code: - * ``` - * // 'this' is the AffineTransform instance. - * // 'it' is the Image instance. - * rotate(Math.toRadians(90.0), it.width / 2.0, it.height / 2.0) - * ``` - * @return The packets list to send to players. - */ - public fun loadImageAsPackets( - image: BufferedImage, - modifyTransform: AffineTransform.(BufferedImage) -> Unit = {} - ): Array { - if (imageLoaded) { - throw ImageAlreadyLoadedException("An image is already loaded using this instance.") - } - - val imageWidth = image.width - val imageHeight = image.height - // We need to round the value to the nearest integer. - // For example : - // If the image is 1x1, we need 1 item frame by line and 1 item frame by column. - // If the image is 129x129, we need 2 item frames by line and 2 item frames by column. - // If the image is 129x128, we need 2 item frames by line and 1 item frame by column. - itemFramesPerLine = (imageWidth + MAP_ITEM_FRAME_PIXELS - 1) ushr MAP_ITEM_FRAME_PIXELS_BITWISE - itemFramesPerColumn = (imageHeight + MAP_ITEM_FRAME_PIXELS - 1) ushr MAP_ITEM_FRAME_PIXELS_BITWISE - - val transform = AffineTransform.getScaleInstance(1.0, 1.0).apply { - modifyTransform(image) - } - - val framebuffer = LargeGraphics2DFramebuffer(imageWidth, imageHeight).apply { - renderer.drawRenderedImage(image, transform) - } - - return createPackets(framebuffer).also { packets = it } - } - - /** - * Creates packets from the image. - * @param framebuffer The frame buffer to convert as packets. - * @return The list of packets. - */ - private fun createPackets(framebuffer: LargeGraphics2DFramebuffer): Array { - val itemFramesPerLine = itemFramesPerLine - return Array(numberOfItemFrames) { - val x = it % itemFramesPerLine - val y = it / itemFramesPerLine - framebuffer.createSubView( - x shl MAP_ITEM_FRAME_PIXELS_BITWISE, - y shl MAP_ITEM_FRAME_PIXELS_BITWISE - ).preparePacket(it) - } - } - - /** - * Create necessary item frames on which the image will be displayed. - * - * **Before calling this method, you must have loaded an image using [loadImageAsPackets].** - * @param instance The instance where you want to create the frame. - * @param pos The position of the frame. - * @param orientation The orientation of the frame. - * @param metaModifier The function to modify the item frame meta. - */ - public suspend fun createItemFrames( - instance: Instance, - pos: Pos, - orientation: ItemFrameMeta.Orientation, - metaModifier: ItemFrameMeta.() -> Unit = { - isInvisible = true - } - ): List { - if (!imageLoaded) { - throw ImageNotLoadedException("An image must be loaded before creating the item frames.") - } - if (atLeastOneItemFrameIsPresent()) { - throw ItemFramesAlreadyExistException("The item frames are already present in the instance.") - } - if (numberOfItemFrames == 0) { - return emptyList().also { _itemFrames = it } - } - - val imageMath = MapImageMath.getFromOrientation(orientation) - val beginX = pos.blockX() - val beginY = pos.blockY() - val beginZ = pos.blockZ() - - // Workaround to avoid unpredictable rotation of the item frames. - val yaw = imageMath.yaw - val pitch = imageMath.pitch - - val entities = List(numberOfItemFrames) { frameNumber -> - Entity(EntityType.ITEM_FRAME).apply { - with(entityMeta as ItemFrameMeta) { - setNotifyAboutChanges(false) - - item = ItemStack.builder(Material.FILLED_MAP) - .meta(MapMeta::class.java) { it.mapId(frameNumber) } - .build() - - this.orientation = orientation - metaModifier() - - setNotifyAboutChanges(true) - } - } - } - - entities.mapIndexed { frameNumber, entity -> - val x = imageMath.computeX(beginX, frameNumber, itemFramesPerLine) - val y = imageMath.computeY(beginY, frameNumber, itemFramesPerLine) - val z = imageMath.computeZ(beginZ, frameNumber, itemFramesPerLine) - entity.setInstance(instance, Pos(x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch)).asDeferred() - }.awaitAll() - - return entities.also { _itemFrames = it } - } - - /** - * Remove all item frames linked to the image. - * Do nothing if the item frames are not present. - * Will set the [itemFrames] property to `null`. - * @see [Entity.remove] - */ - public fun removeItemFrames() { - val itemFrames = itemFrames ?: return - itemFrames.forEach(Entity::remove) - _itemFrames = null - } - - /** - * Check if all item frames are present. - * If at least one item frame is not present, the function will return `false`. - * @return `true` if at least one item frame is present, `false` otherwise. - */ - private fun atLeastOneItemFrameIsPresent(): Boolean { - return itemFrames?.any { !it.isRemoved } ?: return false - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt b/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt deleted file mode 100644 index c18cded3..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/image/MapImageMath.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.github.rushyverse.api.image - -import net.minestom.server.entity.metadata.other.ItemFrameMeta - -/** - * This class is used to calculate the position of the map image. - * The [yaw] and [pitch] values are used to fix the orientation of the item frame. - * Check https://github.com/Minestom/Minestom/issues/760. - * @property yaw Get the pitch for the orientation. - * This is a workaround to fix the orientation of the item frame. - * Without these values, the item frame will be rotated in another direction after several seconds. - * @property pitch Get the pitch for the orientation. - * This is a workaround to fix the orientation of the item frame. - * Without these values, the item frame will be rotated in another direction after several seconds. - */ -public sealed interface MapImageMath { - - public companion object { - - /** - * Link the item frame orientation to the [MapImageMath] instance. - */ - private val orientations = mapOf( - ItemFrameMeta.Orientation.DOWN to Down, - ItemFrameMeta.Orientation.UP to Up, - ItemFrameMeta.Orientation.NORTH to North, - ItemFrameMeta.Orientation.SOUTH to South, - ItemFrameMeta.Orientation.WEST to West, - ItemFrameMeta.Orientation.EAST to East - ) - - /** - * Get the [MapImageMath] linked to the orientation. - * @param orientation The orientation of the item frame. - * @return The [MapImageMath] for the orientation. - */ - public fun getFromOrientation(orientation: ItemFrameMeta.Orientation): MapImageMath { - return orientations[orientation] ?: throw IllegalArgumentException("Unsupported orientation: $orientation") - } - } - - public val yaw: Float - public val pitch: Float - - /** - * Compute the x position of the item frame. - * @param beginX Initial x position. - * @param frameNumber Number of the item frame. - * @param itemFramesPerLine Number of blocks by line. - * @return The x position of the item frame. - */ - public fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int - - /** - * Compute the y position of the item frame. - * @param beginY Initial y position. - * @param frameNumber Number of the item frame. - * @param itemFramesPerLine Number of blocks by line. - * @return The y position of the item frame. - */ - public fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int - - /** - * Compute the z position of the item frame. - * @param beginZ Initial z position. - * @param frameNumber Number of the item frame. - * @param itemFramesPerLine Number of blocks by line. - * @return The z position of the item frame. - */ - public fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int - - /** - * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.UP]. - */ - public object Up : MapImageMath { - override val yaw: Float = 0f - override val pitch: Float = 270f - - override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginX + (frameNumber % itemFramesPerLine) - - override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginY - - override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ + (frameNumber / itemFramesPerLine) - } - - /** - * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.DOWN]. - */ - public object Down : MapImageMath { - override val yaw: Float = 0f - override val pitch: Float = 90f - - override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginX + (frameNumber % itemFramesPerLine) - - override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginY - - override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ - (frameNumber / itemFramesPerLine) - } - - /** - * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.NORTH]. - */ - public object North : MapImageMath { - override val yaw: Float = 180f - override val pitch: Float = 0f - - override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginX - (frameNumber % itemFramesPerLine) - - override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginY - (frameNumber / itemFramesPerLine) - - override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ - } - - /** - * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.SOUTH]. - */ - public object South : MapImageMath { - override val yaw: Float = 0f - override val pitch: Float = 0f - - override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginX + (frameNumber % itemFramesPerLine) - - override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginY - (frameNumber / itemFramesPerLine) - - override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ - } - - /** - * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.EAST]. - */ - public object East : MapImageMath { - override val yaw: Float = 270f - override val pitch: Float = 0f - - override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginX - - override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginY - (frameNumber / itemFramesPerLine) - - override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ - (frameNumber % itemFramesPerLine) - } - - /** - * Use to calculate the position of the item frame when the orientation is [ItemFrameMeta.Orientation.WEST]. - */ - public object West : MapImageMath { - override val yaw: Float = 90f - override val pitch: Float = 0f - - override fun computeX(beginX: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginX - - override fun computeY(beginY: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginY - (frameNumber / itemFramesPerLine) - - override fun computeZ(beginZ: Int, frameNumber: Int, itemFramesPerLine: Int): Int = - beginZ + (frameNumber % itemFramesPerLine) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/image/exception/ImageNotLoadedException.kt b/src/main/kotlin/com/github/rushyverse/api/image/exception/ImageNotLoadedException.kt deleted file mode 100644 index b8c5a38d..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/image/exception/ImageNotLoadedException.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.rushyverse.api.image.exception - -/** - * Exception when an issue occurs with an image. - */ -public open class ImageException(message: String) : Exception(message) - -/** - * Exception thrown when an image is already loaded. - */ -public open class ImageAlreadyLoadedException(message: String) : ImageException(message) - -/** - * Exception thrown when an image is not loaded. - */ -public open class ImageNotLoadedException(message: String) : ImageException(message) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/image/exception/ItemFramesAlreadyExistException.kt b/src/main/kotlin/com/github/rushyverse/api/image/exception/ItemFramesAlreadyExistException.kt deleted file mode 100644 index 0d495f69..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/image/exception/ItemFramesAlreadyExistException.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.rushyverse.api.image.exception - -/** - * Exception thrown when item frames are already loaded and present in an instance. - */ -public open class ItemFramesAlreadyExistException(message: String) : Exception(message) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/item/CraftBuilder.kt b/src/main/kotlin/com/github/rushyverse/api/item/CraftBuilder.kt new file mode 100644 index 00000000..cb52af5f --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/item/CraftBuilder.kt @@ -0,0 +1,226 @@ +package com.github.rushyverse.api.item + +import com.github.rushyverse.api.extension.item +import com.github.rushyverse.api.item.exception.CraftResultMissingException +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.ShapedRecipe +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Width of the craft table. + */ +private const val CRAFT_TABLE_WIDTH = 3 + +/** + * Height of the craft table. + */ +private const val CRAFT_TABLE_HEIGHT = 3 + +/** + * Size of the craft table. + */ +private const val CRAFT_TABLE_SIZE = CRAFT_TABLE_WIDTH * CRAFT_TABLE_HEIGHT + +/** + * Slots with index corresponding to the index defined in minecraft for the craft table. + * @property index Index in the craft table. + */ +public enum class CraftSlot(public val index: UInt) { + + /** + * Top left position on the crafting table. + */ + TopLeft(0u), + + /** + * Top position on the crafting table. + */ + Top(1u), + + /** + * Top right position on the crafting table. + */ + TopRight(2u), + + /** + * Center left position on the crafting table. + */ + CenterLeft(3u), + + /** + * Center position on the crafting table. + */ + Center(4u), + + /** + * Center right position on the crafting table. + */ + CenterRight(5u), + + /** + * Bottom left position on the crafting table. + */ + BottomLeft(6u), + + /** + * Bottom position on the crafting table. + */ + Bottom(7u), + + /** + * Bottom right position on the crafting table. + */ + BottomRight(8u) +} + +/** + * Builder to create a [ShapedRecipe]. + * @property craft Array to define the position for each item. + * @property result Result item of the crafting. + */ +public class CraftBuilder { + + /** + * Storage of the item with the position assigned for the recipe's shape. + * The top left position is defined by 0 and bottom right 8 + */ + private val craft: Array = arrayOfNulls(CRAFT_TABLE_SIZE) + + /** + * Result item of the craft. + */ + public var result: ItemStack? = null + + /** + * Define an item stack with the material as a type at a position on the craft table. + * @param positions Positions of the item. + * @param material Item type. + * @return The item created from the material. + */ + public fun set(positions: Array, material: Material): ItemStack { + return ItemStack(material).also { + set(positions, item = it) + } + } + + /** + * Define the item stack at a position on the craft table. + * @param positions Positions of the item. + * @param builder Item builder. + * @return The item built from the builder. + */ + public inline fun set(positions: Array, builder: ItemStack.() -> Unit): ItemStack { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return item(builder).also { + set(positions, item = it) + } + } + + /** + * Define the item stack at a position on the craft table. + * @param positions Positions of the item. + * @param item Item. + */ + public fun set(positions: Array, item: ItemStack) { + positions.forEach { + set(it, item) + } + } + + /** + * Define the item stack at a position on the craft table. + * @param position Position of the item. + * @param item Item. + */ + public fun set(position: CraftSlot, item: ItemStack) { + craft[position.index.toInt()] = item + } + + /** + * Define the value of the [result] property. + * An item is built from the builder and assign it as a result of the craft. + * @param builder Item builder. + * @return The item built from the builder. + */ + public inline fun result(builder: ItemStack.() -> Unit): ItemStack { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return item(builder).also(this::result) + } + + /** + * Define the value of the [result] property. + * @param item Item assign to the result of the craft. + */ + public fun result(item: ItemStack) { + result = item + } + + /** + * Build the [ShapedRecipe] associated to the craft schema defined by the method [set]. + * @param key The unique recipe key. + * @return Shape of the recipe built from the data of [craft]. + */ + public fun build(key: NamespacedKey): ShapedRecipe { + val itemResult = result ?: throw CraftResultMissingException() + val shaped = ShapedRecipe(key, itemResult) + + val keyItems = associateKeyWithItem() + val lines = keysToCraftLines(keyItems) + + shaped.shape(*lines) + setNotNullIngredient(keyItems, shaped) + + return shaped + } + + /** + * Set the ingredients in the [shaped] only if it is not null. + * @param keyItems Map of association about keys and items. + * @param shaped Shape where the ingredient will be set. + */ + private fun setNotNullIngredient( + keyItems: List>, + shaped: ShapedRecipe + ) { + keyItems.asSequence() + .distinctBy { it.first } + .forEach { (key, item) -> + if (item != null) { + shaped.setIngredient(key, item) + } + } + } + + /** + * Transform the set of keys to a list of lines for the craft format. + * The keys are chunked by size of 3 (3 items by line). + * @param keys Keys associated to items. + * @return Array of 3 [String] containing each 3 [Char]. + */ + private fun keysToCraftLines(keys: List>): Array = + keys + .asSequence() + // [a,b,c,d,e,f,g,h,i] + .map { it.first } + // ["abcdefghi"] + .joinToString(separator = "") + // ["abc", "def", "ghi"] + .chunked(CRAFT_TABLE_WIDTH) + .toTypedArray() + + /** + * Associated a key for each item in [craft]. + * If two items are similar, they will have the same key. + * @return Map with the key associate to an item. + */ + private fun associateKeyWithItem(): List> { + val itemKeys = LinkedHashMap(craft.size) + var keyIncrement = 'A' + return craft.map { + (if (it == null) ' ' else itemKeys.getOrPut(it) { keyIncrement++ }) to it + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/item/InventoryConditionSuspend.kt b/src/main/kotlin/com/github/rushyverse/api/item/InventoryConditionSuspend.kt deleted file mode 100644 index 036331b2..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/item/InventoryConditionSuspend.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.github.rushyverse.api.item - -import com.github.rushyverse.api.coroutine.MinestomSync -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import net.minestom.server.entity.Player -import net.minestom.server.inventory.click.ClickType -import net.minestom.server.inventory.condition.InventoryCondition -import net.minestom.server.inventory.condition.InventoryConditionResult - -/** - * Converts this [InventoryConditionSuspend] to a [InventoryCondition]. - * When the inventory condition is called, the code will be executed in the current thread. - * However, when the first suspension point is reached, the code will be executed in a thread obtained using [coroutineScope]. - * @receiver Inventory condition suspendable. - * @param coroutineScope Coroutine scope where the inventory condition will be handled. - * @return The native inventory condition. - */ -public fun InventoryConditionSuspend.asNative(coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope): InventoryCondition { - return InventoryCondition { player, clickedSlot, clickType, result -> - coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { - this@asNative.accept(player, clickedSlot, clickType, result) - } - } - -} - -/** - * Allows to handle the click event on a specific slot in a coroutine context. - */ -public fun interface InventoryConditionSuspend { - - /** - * Handler of the click event on a specific slot in a coroutine scope. - * According to the implementation, [inventoryConditionResult] can be ignored. - * If the value of [inventoryConditionResult] is changed before the suspension point, the value will be used. - * If the value of [inventoryConditionResult] is changed after the suspension point, the value could be ignored. - * @param player Player who clicked in the inventory. - * @param slot Slot clicked, can be -999 if the click is out of the inventory. - * @param clickType Click type. - * @param inventoryConditionResult Result of this callback. - */ - public suspend fun accept( - player: Player, - slot: Int, - clickType: ClickType, - inventoryConditionResult: InventoryConditionResult - ) - -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/item/ItemComparator.kt b/src/main/kotlin/com/github/rushyverse/api/item/ItemComparator.kt deleted file mode 100644 index 18fa8a4b..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/item/ItemComparator.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.rushyverse.api.item - -import net.minestom.server.item.ItemStack - -/** - * Allows to identify if an item is equivalent to another. - */ -public fun interface ItemComparator { - - public companion object { - /** - * Use the method [ItemStack.isSimilar] to identify if an item is equivalent to another. - */ - public val SIMILAR: ItemComparator = ItemComparator(ItemStack::isSimilar) - - /** - * Use the method [ItemStack.equals] to identify if an item is equivalent to another. - */ - public val EQUALS: ItemComparator = ItemComparator(ItemStack::equals) - } - - /** - * Check if both items are equivalent. - * @param item1 First item. - * @param item2 Second item. - * @return `true` if both items are equivalent, `false` otherwise. - */ - public fun areSame(item1: ItemStack, item2: ItemStack): Boolean -} diff --git a/src/main/kotlin/com/github/rushyverse/api/item/exception/CraftResultMissingException.kt b/src/main/kotlin/com/github/rushyverse/api/item/exception/CraftResultMissingException.kt new file mode 100644 index 00000000..475327b6 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/item/exception/CraftResultMissingException.kt @@ -0,0 +1,6 @@ +package com.github.rushyverse.api.item.exception + +/** + * Exception when the result item is not defined. + */ +public class CraftResultMissingException : IllegalStateException("The result item for the craft must be defined") diff --git a/src/main/kotlin/com/github/rushyverse/api/koin/CraftContext.kt b/src/main/kotlin/com/github/rushyverse/api/koin/CraftContext.kt new file mode 100644 index 00000000..ca6f8cbb --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/koin/CraftContext.kt @@ -0,0 +1,186 @@ +package com.github.rushyverse.api.koin + +import com.github.rushyverse.api.APIPlugin +import com.github.rushyverse.api.APIPlugin.Companion.ID_API +import org.koin.core.Koin +import org.koin.core.KoinApplication +import org.koin.core.context.KoinContext +import org.koin.core.error.KoinAppAlreadyStartedException +import org.koin.core.module.Module +import org.koin.core.qualifier.Qualifier +import org.koin.dsl.KoinAppDeclaration +import org.koin.dsl.ModuleDeclaration +import org.koin.dsl.module +import org.koin.mp.KoinPlatformTools +import kotlin.collections.set + +/** + * Wrapper for [org.koin.dsl.module] that immediately loads the module for the current [Koin] instance. + * @param id App id to find the dedicated koin instance. + * @param createdAtStart `true` to execute declaration directly, or `false` to load with lazy way. + * @param moduleDeclaration Declaration of the module + * @return The new module created. + */ +public fun loadModule( + id: String, + createdAtStart: Boolean = false, + moduleDeclaration: ModuleDeclaration +): Module { + return module(createdAtStart, moduleDeclaration).also { CraftContext.loadKoinModules(id, it) } +} + +/** + * Injects an instance of the specified type T from the Koin context of the [APIPlugin]. Allows to retrieve + * shared instances between plugins. + * @returnA lazy delegate of type T representing the injected instance. + */ +public inline fun inject( + qualifier: Qualifier? = null, + mode: LazyThreadSafetyMode = KoinPlatformTools.defaultLazyMode(), +): Lazy = lazy(mode) { CraftContext.get(ID_API).get(qualifier) } + +/** + * Injects an instance of the specified type T from the Koin context defined for the [id]. + * The [id] can be the id of the plugin to retrieve instance linked to the plugin. + * If the instance is not found, the [idFallback] will be used to retrieve the instance. + * + * @param id The id of the memory container to retrieve the instance from. + * @param idFallback The id of the memory container to retrieve the instance from if the first one is not found. + * @return A lazy delegate of type T representing the injected instance. + */ +public inline fun inject( + id: String, + idFallback: String = ID_API, + qualifier: Qualifier? = null, + mode: LazyThreadSafetyMode = KoinPlatformTools.defaultLazyMode(), +): Lazy = lazy(mode) { + CraftContext.get(id).getOrNull(qualifier) ?: CraftContext.get(idFallback).get(qualifier) +} + +/** + * A copy of [KoinContext] to retrieve koin instance for each application. + * This contains the [KoinApplication] and its [Koin] instance for dependency injection. + * + * @see org.koin.core.context.GlobalContext + */ +public object CraftContext { + + /** + * [Koin] instanced linked to an app id. + */ + private val _koins: MutableMap> = mutableMapOf() + + /** + * [Koin] instanced linked to an app id. + */ + public val koins: Map> = _koins + + /** + * Gets the [Koin] instance for an app. + * @param id App id to find the dedicated koin instance. + * @return The koin instance. + * @throws IllegalStateException [KoinApplication] has not yet been started. + */ + public fun get(id: String): Koin = getOrNull(id) ?: error("KoinApplication has not been started for the id [$id]") + + /** + * Gets the [Koin] instance or null if the [KoinApplication] has not yet been started. + * @param id App id to find the dedicated koin instance. + * @return Koin? + */ + public fun getOrNull(id: String): Koin? = _koins[id]?.second + + /** Closes and removes the current [Koin] instance. */ + public fun stopKoin(id: String) { + synchronized(this) { + val koinInstance = _koins[id] ?: return@synchronized + koinInstance.second.close() + _koins -= id + } + } + + /** + * Starts using the provided [KoinAppDeclaration] to create the [KoinApplication] for this context. + * + * @param appDeclaration The application declaration to start with. + * + * @throws KoinAppAlreadyStartedException The [KoinApplication] has already been instantiated. + */ + public fun startKoin(id: String, appDeclaration: KoinAppDeclaration = {}): KoinApplication = synchronized(this) { + val koinApplication = KoinApplication.init().apply(appDeclaration) + return@synchronized startKoin(id, koinApplication) + } + + /** + * Starts using the provided [KoinApplication] as the current one for this context. + * + * @param koinApplication The application to start with. + * + * @throws KoinAppAlreadyStartedException The [KoinApplication] has already been instantiated. + */ + public fun startKoin(id: String, koinApplication: KoinApplication): KoinApplication = synchronized(this) { + register(id, koinApplication) + koinApplication.createEagerInstances() + + return koinApplication + } + + /** + * Registers a [KoinApplication] to as the current one for this context. + * + * @param koinApplication The application to registers. + * + * @throws KoinAppAlreadyStartedException The [KoinApplication] has already been instantiated. + */ + private fun register(id: String, koinApplication: KoinApplication) { + if (getOrNull(id) != null) { + throw KoinAppAlreadyStartedException("Koin Application has already been started for id [$id]") + } + + _koins[id] = koinApplication to koinApplication.koin + } + + /** + * Loads a module into the [Koin] instance. + * + * @param module The module to load. + */ + public fun loadKoinModules(id: String, module: Module) { + synchronized(this) { + loadKoinModules(id, listOf(module)) + } + } + + /** + * Loads modules into the [Koin] instance. + * + * @param modules The modules to load. + */ + public fun loadKoinModules(id: String, modules: List) { + synchronized(this) { + get(id).loadModules(modules) + } + } + + /** + * Unloads a module from the [Koin] instance. + * + * @param module The module to unload. + */ + public fun unloadKoinModules(id: String, module: Module) { + synchronized(this) { + unloadKoinModules(id, listOf(module)) + } + } + + /** + * Unloads modules from the [Koin] instance. + * + * @param modules The modules to unload. + */ + public fun unloadKoinModules(id: String, modules: List) { + synchronized(this) { + get(id).unloadModules(modules) + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/listener/EventListenerSuspend.kt b/src/main/kotlin/com/github/rushyverse/api/listener/EventListenerSuspend.kt deleted file mode 100644 index a1a6acba..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/listener/EventListenerSuspend.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.rushyverse.api.listener - -import com.github.rushyverse.api.coroutine.MinestomSync -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import net.minestom.server.event.Event -import net.minestom.server.event.EventListener - -/** - * Allows to handle event in a coroutine context. - * @param E Type of the event to handle. - * @property coroutineScope Coroutine scope where the event will be handled. - */ -public abstract class EventListenerSuspend( - private val coroutineScope: CoroutineScope = Dispatchers.MinestomSync.scope -) : EventListener { - - override fun run(event: E): EventListener.Result { - coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { - runSuspend(event) - } - return EventListener.Result.SUCCESS - } - - /** - * Handler of the event in a coroutine scope. - * Before the first suspension point, the code will be executed in the main thread. - * When the first suspension point is reached, the code will be executed in a thread obtained using [coroutineScope]. - * @param event Event that was fired. - */ - protected abstract suspend fun runSuspend(event: E) -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/listener/NPCListenerBuilder.kt b/src/main/kotlin/com/github/rushyverse/api/listener/NPCListenerBuilder.kt deleted file mode 100644 index 5eb55f27..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/listener/NPCListenerBuilder.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.rushyverse.api.listener - -import com.github.rushyverse.api.entity.NPCEntity -import net.minestom.server.entity.Player -import net.minestom.server.event.Event -import net.minestom.server.event.EventListener -import net.minestom.server.event.EventNode -import net.minestom.server.event.player.PlayerEntityInteractEvent - -/** - * Builder to create an event node to listen to NPC events. - */ -public object NPCListenerBuilder { - - /** - * Create an event node to listen to NPC events. - * @return A new event node. - */ - public fun createEventNode(): EventNode { - return EventNode.all("npc").apply { - addInteractListener(this) - } - } - - /** - * Add a listener to the node to listen when a player interacts with an NPC. - * @param node Event node to add the listener. - */ - private fun addInteractListener(node: EventNode) { - node.addListener( - EventListener.builder(PlayerEntityInteractEvent::class.java) - .filter { it.target is NPCEntity && it.hand == Player.Hand.MAIN } - .handler { - val npc = it.target as NPCEntity - npc.onInteract(it) - } - .build() - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/listener/PlayerListener.kt b/src/main/kotlin/com/github/rushyverse/api/listener/PlayerListener.kt new file mode 100644 index 00000000..8ec3ae3c --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/listener/PlayerListener.kt @@ -0,0 +1,64 @@ +package com.github.rushyverse.api.listener + +import com.github.rushyverse.api.Plugin +import com.github.rushyverse.api.coroutine.exception.SilentCancellationException +import com.github.rushyverse.api.koin.inject +import com.github.rushyverse.api.player.Client +import com.github.rushyverse.api.player.ClientManager +import com.github.rushyverse.api.player.exception.ClientAlreadyExistsException +import kotlinx.coroutines.cancel +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent + +/** + * Main listener to manager instance of clients for the player entering and exiting the server. + * @property plugin Java plugin. + * @property clients Client manager to store and remove client instances. + */ +public class PlayerListener( + private val plugin: Plugin +) : Listener { + + private val clients: ClientManager by inject(plugin.id) + + /** + * Handle the join event to create and store a new client. + * The client will be linked to the player. + * @param event Event. + */ + @EventHandler(priority = EventPriority.LOWEST) + public suspend fun onJoin(event: PlayerJoinEvent) { + val player = event.player + createAndSaveClient(player) + } + + /** + * Create a new instance of a client and store it into [clients]. + * If a client is already found for the player, throw an exception. + * @param player Player linked to the client. + * @return The instance of the client. + */ + private suspend fun createAndSaveClient(player: Player): Client { + val client = plugin.createClient(player) + if (clients.putIfAbsent(player, client) != null) { + throw ClientAlreadyExistsException("A client linked to the player already exists.") + } + return client + } + + /** + * Handle the quit event to remove the client linked to the player leaving. + * The life cycle of the client will be canceled. + * @param event Event. + */ + @EventHandler(priority = EventPriority.HIGHEST) + public suspend fun onQuit(event: PlayerQuitEvent) { + val player = event.player + val client = clients.removeClient(player) ?: return + client.cancel(SilentCancellationException("The player ${player.name} (${player.uniqueId}) left")) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/listener/VillagerListener.kt b/src/main/kotlin/com/github/rushyverse/api/listener/VillagerListener.kt new file mode 100644 index 00000000..f5d659cf --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/listener/VillagerListener.kt @@ -0,0 +1,25 @@ +package com.github.rushyverse.api.listener + +import com.github.rushyverse.api.extension.keepProfession +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.entity.VillagerCareerChangeEvent +import org.bukkit.plugin.java.JavaPlugin + +/** + * Listener to manage villager state. + * @property plugin Plugin. + */ +public class VillagerListener(private val plugin: JavaPlugin) : Listener { + + + /** + * When a villager will have his profession changed, check if a specific tag is present into the entity. + * If the tag is present, the event will be canceled, otherwise the career will be changed. + * @param event Event when a villager will lose his job. + */ + @EventHandler + public fun onChangeCareer(event: VillagerCareerChangeEvent) { + event.isCancelled = event.entity.keepProfession(plugin) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/listener/api/LanguageListener.kt b/src/main/kotlin/com/github/rushyverse/api/listener/api/LanguageListener.kt new file mode 100644 index 00000000..3b8d3b00 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/listener/api/LanguageListener.kt @@ -0,0 +1,25 @@ +package com.github.rushyverse.api.listener.api + +import com.github.rushyverse.api.koin.inject +import com.github.rushyverse.api.player.language.LanguageManager +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerQuitEvent + +/** + * Listener to manage language data when a player enters or leaves the server. + */ +public class LanguageListener : Listener { + + private val languageManager: LanguageManager by inject() + + /** + * Listen [PlayerQuitEvent] to remove the player from the language manager. + * @param event Event. + */ + @EventHandler + public suspend fun onQuit(event: PlayerQuitEvent) { + val player = event.player + languageManager.remove(player) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/listener/api/ScoreboardListener.kt b/src/main/kotlin/com/github/rushyverse/api/listener/api/ScoreboardListener.kt new file mode 100644 index 00000000..06794e97 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/listener/api/ScoreboardListener.kt @@ -0,0 +1,25 @@ +package com.github.rushyverse.api.listener.api + +import com.github.rushyverse.api.koin.inject +import com.github.rushyverse.api.player.scoreboard.ScoreboardManager +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerQuitEvent + +/** + * Listener to manage scoreboard data when a player enters or leaves the server. + */ +public class ScoreboardListener : Listener { + + private val scoreboardManager: ScoreboardManager by inject() + + /** + * Listen [PlayerQuitEvent] to remove the player from the scoreboard manager. + * @param event Event. + */ + @EventHandler + public suspend fun onQuit(event: PlayerQuitEvent) { + val player = event.player + scoreboardManager.remove(player) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/player/Client.kt b/src/main/kotlin/com/github/rushyverse/api/player/Client.kt new file mode 100644 index 00000000..0127a2d3 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/player/Client.kt @@ -0,0 +1,77 @@ +package com.github.rushyverse.api.player + +import com.github.rushyverse.api.delegate.DelegatePlayer +import com.github.rushyverse.api.extension.asComponent +import com.github.rushyverse.api.koin.inject +import com.github.rushyverse.api.player.exception.PlayerNotFoundException +import com.github.rushyverse.api.player.language.LanguageManager +import com.github.rushyverse.api.player.scoreboard.ScoreboardManager +import com.github.rushyverse.api.translation.SupportedLanguage +import fr.mrmicky.fastboard.adventure.FastBoard +import kotlinx.coroutines.CoroutineScope +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import org.bukkit.entity.Player +import java.util.* + +/** + * Client to store and manage data about player. + * @property playerUUID Player's uuid. + * @property player Player linked to the client. + */ +public open class Client( + public val playerUUID: UUID, + coroutineScope: CoroutineScope, +) : CoroutineScope by coroutineScope { + + private val scoreboardManager: ScoreboardManager by inject() + + private val languageManager: LanguageManager by inject() + + public val player: Player? by DelegatePlayer(playerUUID) + + /** + * Retrieve the instance of player. + * If the player is not found from the server, thrown an exception. + * @return The instance of player. + */ + public fun requirePlayer(): Player = + player ?: throw PlayerNotFoundException("The player cannot be retrieved from the server") + + /** + * Send a message to the player. + * @param text The message as component. + */ + public fun send(text: Component) { + requirePlayer().sendMessage(text) + } + + /** + * Send a message to the player. + * The message will be converted to [Component] with the standard [TagResolver] of MiniMessage. + * ``` + * // Example + * send("Hello $playerName") + * ``` + * + * @param message The string message. + */ + public fun send(message: String) { + send(message.asComponent()) + } + + + /** + * Retrieve the scoreboard of the player. + * The scoreboard will be created if it doesn't exist. + * @return The scoreboard of the player. + */ + public suspend fun scoreboard(): FastBoard = scoreboardManager.getOrCreate(requirePlayer()) + + /** + * Get the language of the player. + * @return The language of the player. + */ + public suspend fun lang(): SupportedLanguage = languageManager.get(requirePlayer()) + +} diff --git a/src/main/kotlin/com/github/rushyverse/api/player/ClientManager.kt b/src/main/kotlin/com/github/rushyverse/api/player/ClientManager.kt new file mode 100644 index 00000000..0c88e8a6 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/player/ClientManager.kt @@ -0,0 +1,98 @@ +package com.github.rushyverse.api.player + +import org.bukkit.entity.Player + +/** + * Get a client from the player instance. + * @param player Player. + * @return The client linked to a player. + */ +public suspend inline fun ClientManager.getTypedClient(player: Player): T = getClient(player) as T + +/** + * Get a client from the key linked to a player. + * @param key Key to find a client. + * @return The client linked to a key. + */ +public suspend inline fun ClientManager.getTypedClient(key: String): T = getClient(key) as T + +/** + * Get a client from the player instance. + * @param player Player. + * @return The client linked to a player, `null` if not found. + */ +public suspend inline fun ClientManager.getTypedClientOrNull(player: Player): T? = + getClientOrNull(player) as T? + +/** + * Get a client from the key linked to a player. + * @param key Key to find a client. + * @return The client linked to a player, `null` if not found. + */ +public suspend inline fun ClientManager.getTypedClientOrNull(key: String): T? = + getClientOrNull(key) as T? + +/** + * Manage the existing client present in the server. + * @property clients Synchronized mutable map of clients as value and name of player as a key. + */ +public interface ClientManager { + + public val clients: Map + + /** + * Put a new client in the server. + * @param client New client added. + * @return The previous value associated with a key, or null there is none. + */ + public suspend fun put(player: Player, client: Client): Client? + + /** + * Put a new client in the server if no client is linked to the player. + * @param client New client added. + * @return The previous value associated with a key, or null there is none. + */ + public suspend fun putIfAbsent(player: Player, client: Client): Client? + + /** + * Remove a client from the server by a Player. + * @param player Player linked to a Client. + * @return The client that was removed null otherwise. + */ + public suspend fun removeClient(player: Player): Client? + + /** + * Get a client from the player instance. + * @param player Player. + * @return The client linked to a player. + */ + public suspend fun getClient(player: Player): Client + + /** + * Get a client from the key linked to a player. + * @param key Key to find a client. + * @return The client linked to a key. + */ + public suspend fun getClient(key: String): Client + + /** + * Get a client from the player instance. + * @param player Player. + * @return The client linked to a player, `null` if not found. + */ + public suspend fun getClientOrNull(player: Player): Client? + + /** + * Get a client from the key linked to a player. + * @param key Key to find a client. + * @return The client linked to a player, `null` if not found. + */ + public suspend fun getClientOrNull(key: String): Client? + + /** + * Check if a client is linked to a player. + * @param player Player. + * @return `true` if there is a client for the player, `false` otherwise. + */ + public suspend fun contains(player: Player): Boolean +} diff --git a/src/main/kotlin/com/github/rushyverse/api/player/ClientManagerImpl.kt b/src/main/kotlin/com/github/rushyverse/api/player/ClientManagerImpl.kt new file mode 100644 index 00000000..23aa19de --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/player/ClientManagerImpl.kt @@ -0,0 +1,64 @@ +package com.github.rushyverse.api.player + +import com.github.rushyverse.api.player.exception.ClientNotFoundException +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.bukkit.entity.Player + +/** + * Manage the existing client present in the server. + * The clients are stored with the name of the player. + * @property _clients Synchronized mutable map of clients as value and name of player as a key. + */ +public class ClientManagerImpl : ClientManager { + + private val mutex = Mutex() + + /** + * All clients in server linked by the name of player. + */ + private val _clients = mutableMapOf() + + override val clients: Map = _clients + + override suspend fun put( + player: Player, + client: Client + ): Client? = mutex.withLock { + _clients.put(player.name, client) + } + + override suspend fun putIfAbsent( + player: Player, + client: Client + ): Client? = mutex.withLock { + _clients.putIfAbsent(getKey(player), client) + } + + override suspend fun removeClient(player: Player): Client? = mutex.withLock { + _clients.remove(getKey(player)) + } + + override suspend fun getClient(player: Player): Client = getClient(getKey(player)) + + override suspend fun getClient(key: String): Client = + getClientOrNull(key) ?: throw ClientNotFoundException("No client is linked to the name [$key]") + + override suspend fun getClientOrNull(player: Player): Client? = getClientOrNull(getKey(player)) + + override suspend fun getClientOrNull(key: String): Client? = mutex.withLock { + _clients[key] + } + + /** + * Key use for the Map + * @param p Player that has the key + * @return The key for the Map + */ + private fun getKey(p: Player): String = p.name + + override suspend fun contains(player: Player): Boolean = mutex.withLock { + _clients.containsKey(getKey(player)) + } + +} diff --git a/src/main/kotlin/com/github/rushyverse/api/player/exception/ClientAlreadyExistsException.kt b/src/main/kotlin/com/github/rushyverse/api/player/exception/ClientAlreadyExistsException.kt new file mode 100644 index 00000000..08c10d50 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/player/exception/ClientAlreadyExistsException.kt @@ -0,0 +1,6 @@ +package com.github.rushyverse.api.player.exception + +/** + * Exception if a client already exists for a player. + */ +public class ClientAlreadyExistsException(message: String? = null) : RuntimeException(message) diff --git a/src/main/kotlin/com/github/rushyverse/api/player/exception/ClientNotFoundException.kt b/src/main/kotlin/com/github/rushyverse/api/player/exception/ClientNotFoundException.kt new file mode 100644 index 00000000..8bb132eb --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/player/exception/ClientNotFoundException.kt @@ -0,0 +1,6 @@ +package com.github.rushyverse.api.player.exception + +/** + * Exception if a client cannot be found. + */ +public class ClientNotFoundException(message: String? = null) : RuntimeException(message) diff --git a/src/main/kotlin/com/github/rushyverse/api/player/exception/PlayerNotFoundException.kt b/src/main/kotlin/com/github/rushyverse/api/player/exception/PlayerNotFoundException.kt new file mode 100644 index 00000000..aa1c8c52 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/player/exception/PlayerNotFoundException.kt @@ -0,0 +1,6 @@ +package com.github.rushyverse.api.player.exception + +/** + * Exception if a player is not into the server. + */ +public class PlayerNotFoundException(message: String? = null) : RuntimeException(message) diff --git a/src/main/kotlin/com/github/rushyverse/api/player/exception/PlayerQuitException.kt b/src/main/kotlin/com/github/rushyverse/api/player/exception/PlayerQuitException.kt new file mode 100644 index 00000000..ba47291b --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/player/exception/PlayerQuitException.kt @@ -0,0 +1,9 @@ +package com.github.rushyverse.api.player.exception + +import kotlin.coroutines.cancellation.CancellationException + +/** + * Exception throw when the player quits the server. + * Allows canceling all coroutines linked to a player. + */ +public class PlayerQuitException(message: String? = null) : CancellationException(message) diff --git a/src/main/kotlin/com/github/rushyverse/api/player/language/LanguageManager.kt b/src/main/kotlin/com/github/rushyverse/api/player/language/LanguageManager.kt new file mode 100644 index 00000000..0eebda05 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/player/language/LanguageManager.kt @@ -0,0 +1,58 @@ +package com.github.rushyverse.api.player.language + +import com.github.rushyverse.api.translation.SupportedLanguage +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.bukkit.entity.Player + +/** + * Manages the languages for players within the game. + * This class ensures thread-safe operations on the languages by using mutex locks. + */ +public class LanguageManager { + + /** + * Mutex used to ensure thread-safe operations on the language map. + */ + private val mutex = Mutex() + + /** + * Private mutable map storing languages associated with player names. + */ + private val _languages = mutableMapOf() + + /** + * Public immutable view of the languages map. + */ + public val languages: Map = _languages + + /** + * Retrieves the languages for the specified player or creates a new one if it doesn't exist. + * This function is thread-safe and uses mutex locks to ensure atomic operations. + * + * @param player The player for whom the language is to be retrieved or created. + * @return The language associated with the player. + */ + public suspend fun get(player: Player): SupportedLanguage = mutex.withLock { + _languages.getOrDefault(player.name, SupportedLanguage.ENGLISH) + } + + /** + * Sets the language for the specified player. + * @param player The player for whom the language is to be set. + * @param lang The language to set. + */ + public suspend fun set(player: Player, lang: SupportedLanguage) { + mutex.withLock { _languages[player.name] = lang } + } + + /** + * Removes and deletes the language associated with the specified player. + * This function is thread-safe and uses mutex locks to ensure atomic operations. + * + * @param player The player whose language is to be removed. + */ + public suspend fun remove(player: Player) { + mutex.withLock { _languages.remove(player.name) } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/player/scoreboard/ScoreboardManager.kt b/src/main/kotlin/com/github/rushyverse/api/player/scoreboard/ScoreboardManager.kt new file mode 100644 index 00000000..344ecbb2 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/player/scoreboard/ScoreboardManager.kt @@ -0,0 +1,51 @@ +package com.github.rushyverse.api.player.scoreboard + +import fr.mrmicky.fastboard.adventure.FastBoard +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.bukkit.entity.Player + +/** + * Manages the scoreboards for players within the game. + * This class ensures thread-safe operations on the scoreboards by using mutex locks. + */ +public class ScoreboardManager { + + /** + * Mutex used to ensure thread-safe operations on the scoreboards map. + */ + private val mutex = Mutex() + + /** + * Private mutable map storing scoreboards associated with player names. + */ + private val _scoreboards = mutableMapOf() + + /** + * Public immutable view of the scoreboards map. + */ + public val scoreboards: Map = _scoreboards + + /** + * Retrieves the scoreboard for the specified player or creates a new one if it doesn't exist. + * This function is thread-safe and uses mutex locks to ensure atomic operations. + * + * @param player The player for whom the scoreboard is to be retrieved or created. + * @return The scoreboard associated with the player. + */ + public suspend fun getOrCreate(player: Player): FastBoard = mutex.withLock { + _scoreboards.getOrPut(player.name) { + FastBoard(player) + } + } + + /** + * Removes and deletes the scoreboard associated with the specified player. + * This function is thread-safe and uses mutex locks to ensure atomic operations. + * + * @param player The player whose scoreboard is to be removed. + */ + public suspend fun remove(player: Player) { + mutex.withLock { _scoreboards.remove(player.name) }?.delete() + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/position/AbstractArea.kt b/src/main/kotlin/com/github/rushyverse/api/position/AbstractArea.kt deleted file mode 100644 index 92d5b38b..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/position/AbstractArea.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.rushyverse.api.position - -import net.minestom.server.entity.Entity -import java.util.* - -/** - * An area that contains entities. - * @param E The type of entity. - * @property _entitiesInArea The mutable entities in the area. - * @property entitiesInArea The entities in the area. - */ -public abstract class AbstractArea : IArea { - - private val _entitiesInArea: MutableSet = Collections.synchronizedSet(mutableSetOf()) - - override val entitiesInArea: Set - get() = _entitiesInArea - - /** - * Update the entities in the area. - * @param inArea The entities in the area. - * @return A pair of the entities that were added and the entities that were removed. - */ - protected fun update(inArea: Set): Pair, Collection> { - val enter = inArea - entitiesInArea - val quit = entitiesInArea - inArea - - _entitiesInArea.clear() - _entitiesInArea.addAll(inArea) - return enter to quit - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/position/CubeArea.kt b/src/main/kotlin/com/github/rushyverse/api/position/CubeArea.kt deleted file mode 100644 index 631c56a9..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/position/CubeArea.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.github.rushyverse.api.position - -import com.github.rushyverse.api.extension.centerRelative -import com.github.rushyverse.api.extension.isInCube -import com.github.rushyverse.api.extension.minMaxOf -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.Entity -import net.minestom.server.instance.Instance - -/** - * A cuboid area defined by two positions. - * @param E Type of entity. - * @property entityClass Class of the entity. - * @property min Minimum position. - * @property max Maximum position. - * @property position Center position of the cube - */ -public class CubeArea( - public val entityClass: Class, - public override var instance: Instance, - position1: Pos, - position2: Pos -) : AbstractArea(), IAreaLocatable { - - public companion object { - public inline operator fun invoke( - instance: Instance, - position1: Pos, - position2: Pos - ): CubeArea = CubeArea(E::class.java, instance, position1, position2) - } - - override var position: Pos - get() = max.centerRelative(min) - set(value) { - // The new position becomes the center of the cube. - val halfSize = max.centerRelative(min) - min = value.sub(halfSize) - max = value.add(halfSize) - } - - public var min: Pos - private set - - public var max: Pos - private set - - init { - val (x1, x2) = minMaxOf(position1.x(), position2.x()) - val (y1, y2) = minMaxOf(position1.y(), position2.y()) - val (z1, z2) = minMaxOf(position1.z(), position2.z()) - this.min = Pos(x1, y1, z1) - this.max = Pos(x2, y2, z2) - } - - override fun updateEntitiesInArea(): Pair, Collection> { - return update(instance.entities - .asSequence() - .filterIsInstance(entityClass) - .filter { it.position.isInCube(min, max) } - .toSet() - ) - } -} - - diff --git a/src/main/kotlin/com/github/rushyverse/api/position/CylinderArea.kt b/src/main/kotlin/com/github/rushyverse/api/position/CylinderArea.kt deleted file mode 100644 index e36b6bf2..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/position/CylinderArea.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.github.rushyverse.api.position - -import com.github.rushyverse.api.extension.isInCylinder -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.Entity -import net.minestom.server.instance.Instance - -/** - * An area defined by a cylinder shape. - * @param E Type of entity. - * @property entityClass Class of the entity. - * @property limitY Limit of the y-axis. - * @property radius Radius. - */ -public class CylinderArea( - public val entityClass: Class, - public override var instance: Instance, - public override var position: Pos, - radius: Double, - public var limitY: ClosedRange, -) : AbstractArea(), IAreaLocatable { - - public companion object { - public inline operator fun invoke( - instance: Instance, - position: Pos, - radius: Double, - limitY: ClosedRange - ): CylinderArea = CylinderArea(E::class.java, instance, position, radius, limitY) - } - - public var radius: Double = radius - set(value) { - verifyNewRadiusValue(value) - field = value - } - - init { - verifyNewRadiusValue(radius) - } - - /** - * Verifies that the new radius value is greater than or equal to 0.0. - * @param value New radius value. - */ - private fun verifyNewRadiusValue(value: Double) { - require(value >= 0.0) { "Radius must be greater than or equal to 0.0" } - } - - override fun updateEntitiesInArea(): Pair, Collection> { - val cylinderPosition = position - return update( - instance.entities - .asSequence() - .filterIsInstance(entityClass) - .filter { it.position.isInCylinder(cylinderPosition, radius, limitY) } - .toSet() - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/position/IArea.kt b/src/main/kotlin/com/github/rushyverse/api/position/IArea.kt deleted file mode 100644 index 8da856a2..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/position/IArea.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.github.rushyverse.api.position - -import net.minestom.server.entity.Entity - -/** - * An area that contains entities. - * @param E The type of entity. - * @property entitiesInArea The entities in the area. - */ -public interface IArea { - - public val entitiesInArea: Set - - /** - * Compute and update the entities in the area. - * @return A pair of the entities that were added and the entities that were removed. - */ - public fun updateEntitiesInArea(): Pair, Collection> -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/position/IAreaLocatable.kt b/src/main/kotlin/com/github/rushyverse/api/position/IAreaLocatable.kt deleted file mode 100644 index 19e7b34a..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/position/IAreaLocatable.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.rushyverse.api.position - -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.Entity -import net.minestom.server.instance.Instance - -/** - * An area with a position into an instance. - * @param E Type of entity. - * @property instance Instance where is located the area. - * @property position Position of the area. - */ -public interface IAreaLocatable : IArea { - - public var instance: Instance - - public var position: Pos -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/position/MultiArea.kt b/src/main/kotlin/com/github/rushyverse/api/position/MultiArea.kt deleted file mode 100644 index 2688a8f3..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/position/MultiArea.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.github.rushyverse.api.position - -import net.minestom.server.entity.Entity - -/** - * An area corresponding to multiple areas. - * @param E Type of entity. - * @property _areas Mutable collection of areas. - * @property areas Collection of areas. - */ -public class MultiArea(areas: MutableSet> = mutableSetOf()) : AbstractArea() { - - private val _areas: MutableSet> = areas - public val areas: Set> get() = _areas - - /** - * Adds an area. - * @param area Area to add. - * @return `true` if the area was added, `false` otherwise. - */ - public fun addArea(area: IArea): Boolean = _areas.add(area) - - /** - * Removes an area. - * @param area Area to remove. - * @return `true` if the area was removed, `false` otherwise. - */ - public fun removeArea(area: IArea): Boolean = _areas.remove(area) - - /** - * Removes all areas. - */ - public fun removeAllAreas() { - _areas.clear() - } - - override fun updateEntitiesInArea(): Pair, Collection> { - return update( - areas.asSequence() - .onEach { it.updateEntitiesInArea() } - .flatMap { it.entitiesInArea } - .toSet() - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/position/SphereArea.kt b/src/main/kotlin/com/github/rushyverse/api/position/SphereArea.kt deleted file mode 100644 index 4f0fd4c0..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/position/SphereArea.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.rushyverse.api.position - -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.Entity -import net.minestom.server.instance.Instance - -/** - * An area defined by a sphere shape. - * @param E Type of entity. - * @property entityClass Class of the entity. - * @property radius Radius. - */ -public class SphereArea( - public val entityClass: Class, - public override var instance: Instance, - public override var position: Pos, - radius: Double -) : AbstractArea(), IAreaLocatable { - - public companion object { - public inline operator fun invoke( - instance: Instance, - position: Pos, - radius: Double - ): SphereArea = SphereArea(E::class.java, instance, position, radius) - } - - public var radius: Double = radius - set(value) { - verifyNewRadiusValue(value) - field = value - } - - init { - verifyNewRadiusValue(radius) - } - - /** - * Verifies that the new radius value is greater than or equal to 0.0. - * @param value New radius value. - */ - private fun verifyNewRadiusValue(value: Double) { - require(value >= 0.0) { "Radius must be greater than or equal to 0.0" } - } - - override fun updateEntitiesInArea(): Pair, Collection> { - return update(instance.getNearbyEntities(position, radius).asSequence().filterIsInstance(entityClass).toSet()) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/schedule/AbstractScheduler.kt b/src/main/kotlin/com/github/rushyverse/api/schedule/AbstractScheduler.kt new file mode 100644 index 00000000..7c7b230b --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/schedule/AbstractScheduler.kt @@ -0,0 +1,42 @@ +package com.github.rushyverse.api.schedule + +import kotlinx.coroutines.* + +/** + * Allows scheduling a task into a coroutine from the [coroutineScope]. + * @property coroutineScope Scope to start the job. + * @property job Instance of the job launched. + * @property running `true` if the task is running, `false` otherwise. + */ +public abstract class AbstractScheduler( + protected val coroutineScope: CoroutineScope +) : Scheduler { + + protected var job: Job? = null + + override val running: Boolean + get() = job?.isActive == true + + override fun start() { + require(!running) { "The scheduling is already running" } + job = coroutineScope.launch { + run() + } + } + + /** + * Main body of the scheduler. + * Will create the loop to execute tasks. + */ + public abstract suspend fun run() + + override fun cancel(cause: CancellationException?) { + job?.cancel(cause) + job = null + } + + override suspend fun cancelAndJoin() { + job?.cancelAndJoin() + job = null + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/schedule/Scheduler.kt b/src/main/kotlin/com/github/rushyverse/api/schedule/Scheduler.kt new file mode 100644 index 00000000..4fe0a53c --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/schedule/Scheduler.kt @@ -0,0 +1,29 @@ +package com.github.rushyverse.api.schedule + +import kotlinx.coroutines.CancellationException + +/** + * Schedule to start and stop a task using coroutine. + * @property running `true` if the scheduler is running, `false` otherwise. + */ +public interface Scheduler { + + public val running: Boolean + + /** + * Start the task. + * Throw an exception if the task is already running. + */ + public fun start() + + /** + * Stop the task. + * @param cause Reason of the cancellation. + */ + public fun cancel(cause: CancellationException? = null) + + /** + * Stop the task and wait the end of last execution. + */ + public suspend fun cancelAndJoin() +} diff --git a/src/main/kotlin/com/github/rushyverse/api/schedule/SchedulerTask.kt b/src/main/kotlin/com/github/rushyverse/api/schedule/SchedulerTask.kt new file mode 100644 index 00000000..55d25767 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/schedule/SchedulerTask.kt @@ -0,0 +1,228 @@ +package com.github.rushyverse.api.schedule + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import mu.KotlinLogging +import java.util.* +import java.util.concurrent.CancellationException +import kotlin.time.Duration + +private val log = KotlinLogging.logger { } + +/** + * Allows executing code while the task is not canceled. + * The [tasks] contains several body functions that one will be executed after each [delay]. + * @property delay Time to wait between each execution. + * @property _tasks Task executed in the coroutine context each time. + * @property tasks Immutable list of a task executed in the coroutine context each time. + * @property mutex Mutex to synchronized data of the scheduler. + * @property nextTaskIndex Next index of the task that will be executed. + */ +public class SchedulerTask( + coroutineScope: CoroutineScope, + public var delay: Duration, + private var delayBefore: Boolean = false, + private var stopWhenNoTask: Boolean = true +) : AbstractScheduler(coroutineScope) { + + /** + * Task of the scheduler. + * @property id Id of the task. + * @property parent Scheduler task where is registered the task. + * @property body Lambda function. + */ + public inner class Task( + public val id: String = UUID.randomUUID().toString(), + public val parent: SchedulerTask, + public val body: suspend Task.() -> Unit + ) { + + /** + * Run the [body] function. + */ + public suspend fun run() { + body() + } + + /** + * Remove the task from the parent. + */ + public suspend fun remove(): Boolean { + return parent.remove(id) + } + } + + private var nextTaskIndex = 0 + + private val _tasks = ArrayList() + public val tasks: List = _tasks + + private val mutex = Mutex() + + override suspend fun run() { + if (delayBefore) { + delay(delay) + } + + coroutineScope { + while (isActive) { + val task = getNextTaskAndShiftCursor() + if (task != null) { + runTask(task) + } + delay(delay) + } + } + } + + /** + * Lock the mutex to search the next task that will be executed. + * After have found the task, move the cursor [nextTaskIndex]. + * @return `null` if no task found into the list, the task otherwise. + */ + private suspend fun getNextTaskAndShiftCursor(): Task? = mutex.withLock { + var task = _tasks.getOrNull(nextTaskIndex) + + if (task == null) { + task = _tasks.firstOrNull() + nextTaskIndex = 1 + } else { + nextTaskIndex++ + } + task + } + + /** + * Run the task in try catch to protect the scheduler. + * @param task Task that will be run. + */ + private suspend fun runTask(task: Task) { + try { + task.run() + } catch (t: Throwable) { + log.error("Error during process of the task $task", t) + } + } + + /** + * Add a new body should be executed in the scheduler. + * @param id Id of the task created. + * @param body Lambda function. + * @return Task created. + */ + public suspend fun add(id: String = UUID.randomUUID().toString(), body: suspend Task.() -> Unit): Task = + mutex.withLock { + addUnsafe(id, body) + } + + /** + * Add a new body should be executed in the scheduler. + * The operation is made with possible asynchronous effect if the scheduler is running. + * It's recommended to use this function when you are sure that the scheduler is not running. + * @param id Id of the task created. + * @param body Lambda function. + * @return Task created. + */ + public fun addUnsafe(id: String = UUID.randomUUID().toString(), body: suspend Task.() -> Unit): Task { + return createTask(id, body).also { + _tasks += it + } + } + + /** + * Add a new body should be executed in the scheduler. + * @param index Index at which the specified element is to be inserted. + * @param id Id of the task created. + * @param body Lambda function. + * @return Task created. + */ + public suspend fun addAt( + index: Int, + id: String = UUID.randomUUID().toString(), + body: suspend Task.() -> Unit + ): Task = + mutex.withLock { addAtUnsafe(index, id, body) } + + /** + * Add a new body should be executed in the scheduler. + * The operation is made with possible asynchronous effect if the scheduler is running. + * It's recommended to use this function when you are sure that the scheduler is not running. + * @param index Index at which the specified element is to be inserted. + * @param id Id of the task created. + * @param body Lambda function. + * @return Task created. + */ + public fun addAtUnsafe(index: Int, id: String = UUID.randomUUID().toString(), body: suspend Task.() -> Unit): Task = + createTask(id, body).also { + _tasks.add(index, it) + } + + /** + * Create a children task. + * @param id Id of the task created. + * @param body Lambda function linked to the task. + * @return Task created. + */ + private fun createTask( + id: String, + body: suspend Task.() -> Unit + ) = Task(id, this, body) + + /** + * Remove a task from the scheduler. + * @param id Id of the task registered into the scheduler. + * @return `true` if a task has been removed, `false` otherwise. + */ + public suspend fun remove(id: String): Boolean = mutex.withLock { + removeUnsafe(id) + } + + /** + * Remove a task from the scheduler. + * The operation is made with possible asynchronous effect if the scheduler is running. + * It's recommended to use this function when you are sure that the scheduler is not running. + * @param id Id of the task registered into the scheduler. + * @return `true` if a task has been removed, `false` otherwise. + */ + public fun removeUnsafe(id: String): Boolean { + val indexOfTask = _tasks.indexOfFirst { it.id == id } + if (indexOfTask == -1) return false + removeAtUnsafe(indexOfTask) + return true + } + + /** + * Remove a task from the scheduler. + * Throws exception if no task is present at the index. + * @param index Index of the task that will be removed. + */ + public suspend fun removeAt(index: Int) { + mutex.withLock { + removeAtUnsafe(index) + } + } + + /** + * Remove the task at the index. + * Throws exception if no task is present at the index. + * The operation is made with possible asynchronous effect if the scheduler is running. + * It's recommended to use this function when you are sure that the scheduler is not running. + * If necessary, shift the next task to not skip one task. + * @param index Index of the task. + */ + public fun removeAtUnsafe(index: Int) { + _tasks.removeAt(index) + if (tasks.isEmpty()) { + if (stopWhenNoTask) { + cancel(CancellationException("No tasks left. The scheduler is disabled")) + } + nextTaskIndex = 0 + } else if (index < nextTaskIndex) { + nextTaskIndex-- + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/ComponentSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/ComponentSerializer.kt new file mode 100644 index 00000000..fa655ada --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/ComponentSerializer.kt @@ -0,0 +1,30 @@ +package com.github.rushyverse.api.serializer + +import com.github.rushyverse.api.extension.asComponent +import com.github.rushyverse.api.extension.asString +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.kyori.adventure.text.Component + +/** + * Serializer for [Component]. + */ +public object ComponentSerializer : KSerializer { + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( + "component", + PrimitiveKind.STRING + ) + + override fun serialize(encoder: Encoder, value: Component) { + encoder.encodeString(value.asString()) + } + + override fun deserialize(decoder: Decoder): Component { + return decoder.decodeString().asComponent() + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/DyeColorSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/DyeColorSerializer.kt new file mode 100644 index 00000000..4672fd96 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/DyeColorSerializer.kt @@ -0,0 +1,8 @@ +package com.github.rushyverse.api.serializer + +import org.bukkit.DyeColor + +/** + * Serializer for [DyeColor]. + */ +public object DyeColorSerializer : EnumSerializer("dyeColor", DyeColor.entries) diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/EnchantmentSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/EnchantmentSerializer.kt new file mode 100644 index 00000000..ff41623b --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/EnchantmentSerializer.kt @@ -0,0 +1,36 @@ +package com.github.rushyverse.api.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.bukkit.NamespacedKey +import org.bukkit.enchantments.Enchantment + +/** + * Serializer for [Enchantment]. + */ +public object EnchantmentSerializer : KSerializer { + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( + "enchantment", + PrimitiveKind.STRING + ) + + override fun serialize(encoder: Encoder, value: Enchantment) { + NamespacedSerializer.serialize(encoder, value.key) + } + + override fun deserialize(decoder: Decoder): Enchantment { + val key = NamespacedSerializer.deserialize(decoder) as NamespacedKey + return Enchantment.getByKey(key) + ?: throw SerializationException( + "Unable to find enchantment with namespaced key: $key. Valid enchantments are: ${ + Enchantment.values().joinToString { it.key.asString() } + }" + ) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/EnumSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/EnumSerializer.kt new file mode 100644 index 00000000..640d17cb --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/EnumSerializer.kt @@ -0,0 +1,51 @@ +package com.github.rushyverse.api.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.enums.EnumEntries + +/** + * EnumSerializer is a serializer used to serialize and deserialize enum values. + * + * @param T The type of the enum class. + * @property values The list of enum values to be serialized or deserialized. + * @property descriptor The serial descriptor for the enum serializer. + * @constructor Creates an instance of EnumSerializer with the given serial name and enum values. + */ +public open class EnumSerializer>( + serialName: String, + private val values: EnumEntries +) : KSerializer { + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(serialName, PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: T) { + encoder.encodeString(value.name) + } + + override fun deserialize(decoder: Decoder): T { + return findEnumValue(decoder.decodeString()) + } + + /** + * Finds the matching enum value for a given decoded string. + * Will transform all spaces to underscore and uppercase all letters. + * So for example, "foo bar" will be transformed to "FOO_BAR". + * + * @param decoded The decoded string used to search for the matching enum value. + * @return The matching enum value. + * @throws SerializationException if no matching enum value is found. + */ + public fun findEnumValue(decoded: String): T { + val name = decoded.uppercase().replace(" ", "_") + return values.firstOrNull { it.name.uppercase() == name } + ?: throw SerializationException("Invalid enum value: $decoded. Valid values are: ${ + values.joinToString { it.name } + }") + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/ItemFlagSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/ItemFlagSerializer.kt new file mode 100644 index 00000000..81e729ef --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/ItemFlagSerializer.kt @@ -0,0 +1,8 @@ +package com.github.rushyverse.api.serializer + +import org.bukkit.inventory.ItemFlag + +/** + * Serializer for [ItemFlag]. + */ +public object ItemFlagSerializer : EnumSerializer("itemFlag", ItemFlag.entries) diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/ItemFrameMetaOrientationSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/ItemFrameMetaOrientationSerializer.kt deleted file mode 100644 index c967f8a3..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/serializer/ItemFrameMetaOrientationSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.rushyverse.api.serializer - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import net.minestom.server.entity.metadata.other.ItemFrameMeta - -/** - * Serializer for [ItemFrameMeta.Orientation]. - * To deserialize the orientation, it will be case-insensitive. - */ -public object ItemFrameMetaOrientationSerializer : KSerializer { - - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("orientation", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: ItemFrameMeta.Orientation) { - encoder.encodeString(value.name) - } - - override fun deserialize(decoder: Decoder): ItemFrameMeta.Orientation { - val decodeString = decoder.decodeString() - return ItemFrameMeta.Orientation.values() - .find { it.name.equals(decodeString, true) } - ?: throw SerializationException("Invalid orientation") - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/ItemStackSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/ItemStackSerializer.kt new file mode 100644 index 00000000..fe314dd4 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/ItemStackSerializer.kt @@ -0,0 +1,222 @@ +package com.github.rushyverse.api.serializer + +import com.destroystokyo.paper.Namespaced +import com.github.rushyverse.api.extension.ItemStack +import com.github.rushyverse.api.extension.getTexturesProperty +import com.github.rushyverse.api.extension.setTextures +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.* +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.block.banner.Pattern +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.Damageable +import org.bukkit.inventory.ItemFlag +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.BannerMeta +import org.bukkit.inventory.meta.SkullMeta +import java.util.* + +/** + * Serializer for [ItemStack]. + */ +public object ItemStackSerializer : KSerializer { + + private val materialSerializer: KSerializer get() = MaterialSerializer + + private val amountSerializer: KSerializer get() = Int.serializer() + + private val enchantmentsSerializer: KSerializer?> = + MapSerializer(EnchantmentSerializer, Int.serializer()).nullable + + private val unbreakableSerializer: KSerializer = Boolean.serializer().nullable + + private val customModelSerializer: KSerializer = Int.serializer().nullable + + private val destroyableKeysSerializer: KSerializer?> = + ListSerializer(NamespacedSerializer).nullable + + private val placeableKeysSerializer: KSerializer?> get() = destroyableKeysSerializer + + private val displayNameSerializer: KSerializer = ComponentSerializer.nullable + + private val loreSerializer: KSerializer?> = ListSerializer(ComponentSerializer).nullable + + private val durabilitySerializer: KSerializer = Double.serializer().nullable + + private val textureSerializer: KSerializer = String.serializer().nullable + + private val patternsSerializer: KSerializer?> = ListSerializer(PatternSerializer).nullable + + private val flagsSerializer: KSerializer?> = ListSerializer(ItemFlagSerializer).nullable + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("itemstack") { + element("material", materialSerializer.descriptor) + element("amount", amountSerializer.descriptor) + element("enchantments", enchantmentsSerializer.descriptor) + element("unbreakable", unbreakableSerializer.descriptor) + element("customModel", customModelSerializer.descriptor) + element("destroyableKeys", destroyableKeysSerializer.descriptor) + element("placeableKeys", placeableKeysSerializer.descriptor) + element("displayName", displayNameSerializer.descriptor) + element("lore", loreSerializer.descriptor) + element("durability", durabilitySerializer.descriptor) + element("texture", textureSerializer.descriptor) + element("patterns", patternsSerializer.descriptor) + element("flags", flagsSerializer.descriptor) + } + + override fun serialize(encoder: Encoder, value: ItemStack) { + val itemMeta = if (value.hasItemMeta()) value.itemMeta else null + + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, materialSerializer, value.type) + encodeSerializableElement(descriptor, 1, amountSerializer, value.amount) + encodeSerializableElement(descriptor, 2, enchantmentsSerializer, value.enchantments) + + if (itemMeta == null) return@encodeStructure + + encodeSerializableElement(descriptor, 3, unbreakableSerializer, itemMeta.isUnbreakable) + encodeSerializableElement(descriptor, 4, customModelSerializer, itemMeta.let { + if (it.hasCustomModelData()) it.customModelData else null + }) + encodeSerializableElement( + descriptor, + 5, + destroyableKeysSerializer, + itemMeta.let { if (it.hasDestroyableKeys()) it.destroyableKeys.toList() else null } + ) + encodeSerializableElement( + descriptor, + 6, + placeableKeysSerializer, + itemMeta.let { if (it.hasPlaceableKeys()) it.placeableKeys.toList() else null } + ) + encodeSerializableElement(descriptor, 7, displayNameSerializer, itemMeta.let { + if (it.hasDisplayName()) it.displayName() else null + }) + encodeSerializableElement(descriptor, 8, loreSerializer, itemMeta.let { + if (it.hasLore()) it.lore() else null + }) + encodeSerializableElement( + descriptor, + 9, + durabilitySerializer, + itemMeta.let { it as? Damageable }?.health + ) + encodeSerializableElement( + descriptor, + 10, + textureSerializer, + itemMeta.let { it as? SkullMeta }?.playerProfile?.getTexturesProperty()?.value + ) + encodeSerializableElement( + descriptor, + 11, + patternsSerializer, + itemMeta.let { it as? BannerMeta }?.patterns + ) + encodeSerializableElement(descriptor, 12, flagsSerializer, itemMeta.itemFlags.toList()) + } + } + + override fun deserialize(decoder: Decoder): ItemStack { + return decoder.decodeStructure(descriptor) { + var material: Material? = null + var amount = 1 + var enchantments: Map? = null + var unbreakable: Boolean? = null + var customModel: Int? = null + var destroyableKeys: Collection? = null + var placeableKeys: Collection? = null + var displayName: Component? = null + var lore: Collection? = null + var flags: Collection? = null + // For item + var durability: Double? = null + // For Skull item + var texture: String? = null + // For banner item + var patterns: List? = null + + if (decodeSequentially()) { + material = decodeSerializableElement(descriptor, 0, materialSerializer) + amount = decodeSerializableElement(descriptor, 1, amountSerializer) + enchantments = decodeSerializableElement(descriptor, 2, enchantmentsSerializer) + unbreakable = decodeSerializableElement(descriptor, 3, unbreakableSerializer) + customModel = decodeSerializableElement(descriptor, 4, customModelSerializer) + destroyableKeys = decodeSerializableElement(descriptor, 5, destroyableKeysSerializer) + placeableKeys = decodeSerializableElement(descriptor, 6, placeableKeysSerializer) + displayName = decodeSerializableElement(descriptor, 7, displayNameSerializer) + lore = decodeSerializableElement(descriptor, 8, loreSerializer) + durability = decodeSerializableElement(descriptor, 9, durabilitySerializer) + texture = decodeSerializableElement(descriptor, 10, textureSerializer) + patterns = decodeSerializableElement(descriptor, 11, patternsSerializer) + flags = decodeSerializableElement(descriptor, 12, flagsSerializer) + } else { + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> material = decodeSerializableElement(descriptor, index, materialSerializer) + 1 -> amount = decodeSerializableElement(descriptor, index, amountSerializer) + 2 -> enchantments = decodeSerializableElement( + descriptor, + index, + enchantmentsSerializer + ) + + 3 -> unbreakable = decodeSerializableElement(descriptor, index, unbreakableSerializer) + 4 -> customModel = decodeSerializableElement(descriptor, index, customModelSerializer) + 5 -> destroyableKeys = decodeSerializableElement(descriptor, index, destroyableKeysSerializer) + 6 -> placeableKeys = decodeSerializableElement(descriptor, index, placeableKeysSerializer) + 7 -> displayName = decodeSerializableElement(descriptor, index, displayNameSerializer) + 8 -> lore = decodeSerializableElement(descriptor, index, loreSerializer) + 9 -> durability = decodeSerializableElement(descriptor, index, durabilitySerializer) + 10 -> texture = decodeSerializableElement(descriptor, index, textureSerializer) + 11 -> patterns = decodeSerializableElement(descriptor, index, patternsSerializer) + 12 -> flags = decodeSerializableElement(descriptor, index, flagsSerializer) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + } + + material ?: throw SerializationException("The field material is missing") + + ItemStack(material) { + this.amount = amount + this.editMeta { + enchantments?.forEach { (enchant, level) -> + it.addEnchant(enchant, level, true) + } + unbreakable?.also(it::setUnbreakable) + customModel?.also(it::setCustomModelData) + destroyableKeys?.also(it::setDestroyableKeys) + placeableKeys?.also(it::setPlaceableKeys) + displayName?.also(it::displayName) + lore?.run { toList().also(it::lore) } + flags?.also { itemFlags -> it.addItemFlags(*itemFlags.toTypedArray()) } + + when (it) { + is Damageable -> durability?.also(it::damage) + + is SkullMeta -> texture?.let { texture -> + val profile = Bukkit.createProfile(UUID.randomUUID()) + profile.setTextures(texture) + it.playerProfile = profile + } + + is BannerMeta -> patterns?.also(it::setPatterns) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/PosSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/LocationSerializer.kt similarity index 77% rename from src/main/kotlin/com/github/rushyverse/api/serializer/PosSerializer.kt rename to src/main/kotlin/com/github/rushyverse/api/serializer/LocationSerializer.kt index 077d5684..fc2fe073 100644 --- a/src/main/kotlin/com/github/rushyverse/api/serializer/PosSerializer.kt +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/LocationSerializer.kt @@ -7,24 +7,30 @@ import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.* -import net.minestom.server.coordinate.Pos +import org.bukkit.Bukkit +import org.bukkit.Location /** - * Serializer for [Pos]. + * Serializer for [Location]. */ -public object PosSerializer : KSerializer { +public object LocationSerializer : KSerializer { /** * Serializer for the coordinates x, y or z. */ - private val coordinateSerializer get() = Double.serializer() + private val coordinateSerializer: KSerializer get() = Double.serializer() /** * Serializer for the rotations yaw or pitch. */ - private val rotationSerializer get() = Float.serializer().nullable + private val rotationSerializer: KSerializer = Float.serializer().nullable - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("pos") { + /** + * Serializer for the world. + */ + private val worldSerializer: KSerializer = String.serializer().nullable + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("location") { val coordinateSerializer = coordinateSerializer val rotationSerializer = rotationSerializer @@ -33,9 +39,10 @@ public object PosSerializer : KSerializer { element("z", coordinateSerializer.descriptor) element("yaw", rotationSerializer.descriptor) element("pitch", rotationSerializer.descriptor) + element("world", worldSerializer.descriptor) } - override fun serialize(encoder: Encoder, value: Pos) { + override fun serialize(encoder: Encoder, value: Location) { val coordinateSerializer = coordinateSerializer val rotationSerializer = rotationSerializer @@ -45,10 +52,11 @@ public object PosSerializer : KSerializer { encodeSerializableElement(descriptor, 2, coordinateSerializer, value.z) encodeSerializableElement(descriptor, 3, rotationSerializer, value.yaw) encodeSerializableElement(descriptor, 4, rotationSerializer, value.pitch) + encodeSerializableElement(descriptor, 5, worldSerializer, value.world?.name) } } - override fun deserialize(decoder: Decoder): Pos { + override fun deserialize(decoder: Decoder): Location { val coordinateSerializer = coordinateSerializer val rotationSerializer = rotationSerializer @@ -58,6 +66,7 @@ public object PosSerializer : KSerializer { var z: Double? = null var yaw: Float? = null var pitch: Float? = null + var world: String? = null if (decodeSequentially()) { x = decodeSerializableElement(descriptor, 0, coordinateSerializer) @@ -65,6 +74,7 @@ public object PosSerializer : KSerializer { z = decodeSerializableElement(descriptor, 2, coordinateSerializer) yaw = decodeSerializableElement(descriptor, 3, rotationSerializer) pitch = decodeSerializableElement(descriptor, 4, rotationSerializer) + world = decodeSerializableElement(descriptor, 5, worldSerializer) } else { while (true) { when (val index = decodeElementIndex(descriptor)) { @@ -73,13 +83,15 @@ public object PosSerializer : KSerializer { 2 -> z = decodeSerializableElement(descriptor, index, coordinateSerializer) 3 -> yaw = decodeSerializableElement(descriptor, index, rotationSerializer) 4 -> pitch = decodeSerializableElement(descriptor, index, rotationSerializer) + 5 -> world = decodeSerializableElement(descriptor, index, worldSerializer) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index") } } } - Pos( + Location( + world?.let { Bukkit.getWorld(it) }, x ?: throw SerializationException("The field x is missing"), y ?: throw SerializationException("The field y is missing"), z ?: throw SerializationException("The field z is missing"), @@ -88,4 +100,4 @@ public object PosSerializer : KSerializer { ) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/MaterialSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/MaterialSerializer.kt new file mode 100644 index 00000000..0019992d --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/MaterialSerializer.kt @@ -0,0 +1,8 @@ +package com.github.rushyverse.api.serializer + +import org.bukkit.Material + +/** + * Serializer for [Material]. + */ +public object MaterialSerializer : EnumSerializer("material", Material.entries) diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/NamespacedSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/NamespacedSerializer.kt new file mode 100644 index 00000000..49f2779a --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/NamespacedSerializer.kt @@ -0,0 +1,53 @@ +package com.github.rushyverse.api.serializer + +import com.destroystokyo.paper.Namespaced +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.bukkit.NamespacedKey + +/** + * Serializer for [Namespaced]. + */ +public object NamespacedSerializer : KSerializer { + + /** + * The default separator used to separate the namespace and the key. + */ + private const val DEFAULT_SEPARATOR = ":" + + /** + * Regex used to replace uppercase letters with "_[a-z]". + */ + private val regexUppercase = Regex("([A-Z])") + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( + "namespaced", + PrimitiveKind.STRING + ) + + override fun serialize(encoder: Encoder, value: Namespaced) { + encoder.encodeString(value.namespace + DEFAULT_SEPARATOR + value.key) + } + + override fun deserialize(decoder: Decoder): Namespaced { + return decoder.decodeString().let { decodedString -> + val namespacedString = decodedString.split(DEFAULT_SEPARATOR).map { + // Replace " " to "_". Example: blue wool -> blue_wool + it.replace(' ', '_') + // Replace "A-Z" to "_a-z". Example: blueWool -> blue_wool + .replace(regexUppercase) { matchResult -> "_${matchResult.value.lowercase()}" } + + } + + if (namespacedString.size == 2) { + NamespacedKey(namespacedString[0], namespacedString[1]) + } else { + NamespacedKey.minecraft(namespacedString[0]) + } + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/PatternSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/PatternSerializer.kt new file mode 100644 index 00000000..f85d2e8d --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/PatternSerializer.kt @@ -0,0 +1,58 @@ +package com.github.rushyverse.api.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.* +import org.bukkit.DyeColor +import org.bukkit.block.banner.Pattern +import org.bukkit.block.banner.PatternType + +/** + * Serializer for [Pattern]. + */ +public object PatternSerializer : KSerializer { + + private val typeSerializer: PatternTypeSerializer get() = PatternTypeSerializer + + private val dyeColorSerializer: DyeColorSerializer get() = DyeColorSerializer + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("pattern") { + element("color", dyeColorSerializer.descriptor) + element("type", typeSerializer.descriptor) + } + + override fun serialize(encoder: Encoder, value: Pattern) { + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, dyeColorSerializer, value.color) + encodeSerializableElement(descriptor, 1, typeSerializer, value.pattern) + } + } + + override fun deserialize(decoder: Decoder): Pattern { + return decoder.decodeStructure(descriptor) { + var color: DyeColor? = null + var type: PatternType? = null + + if (decodeSequentially()) { + color = decodeSerializableElement(descriptor, 0, dyeColorSerializer) + type = decodeSerializableElement(descriptor, 1, typeSerializer) + } else { + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> color = decodeSerializableElement(descriptor, index, dyeColorSerializer) + 1 -> type = decodeSerializableElement(descriptor, index, typeSerializer) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + } + + Pattern( + color ?: throw SerializationException("The field color is missing"), + type ?: throw SerializationException("The field type is missing") + ) + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/PatternTypeSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/PatternTypeSerializer.kt new file mode 100644 index 00000000..7e937e04 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/PatternTypeSerializer.kt @@ -0,0 +1,32 @@ +package com.github.rushyverse.api.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.bukkit.block.banner.PatternType + +/** + * Serializer for [PatternType]. + */ +public object PatternTypeSerializer : KSerializer { + + private val enumSerializer = EnumSerializer("patternTypeEnum", PatternType.entries) + + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor( + "patternType", + PrimitiveKind.STRING + ) + + override fun serialize(encoder: Encoder, value: PatternType) { + encoder.encodeString(value.identifier) + } + + override fun deserialize(decoder: Decoder): PatternType { + val key = decoder.decodeString() + return PatternType.getByIdentifier(key.lowercase()) ?: enumSerializer.findEnumValue(key) + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/serializer/RangeDoubleSerializer.kt b/src/main/kotlin/com/github/rushyverse/api/serializer/RangeDoubleSerializer.kt new file mode 100644 index 00000000..7300d305 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/serializer/RangeDoubleSerializer.kt @@ -0,0 +1,63 @@ +package com.github.rushyverse.api.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.* + +/** + * Serializer for [ClosedRange] with [Double] value. + */ +public object RangeDoubleSerializer : KSerializer> { + + /** + * Serializer for [Double]. + */ + private val doubleSerializer: KSerializer get() = Double.serializer() + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("range") { + val doubleSerializer = doubleSerializer + + element("start", doubleSerializer.descriptor) + element("end", doubleSerializer.descriptor) + } + + override fun serialize(encoder: Encoder, value: ClosedRange) { + val doubleSerializer = doubleSerializer + + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, doubleSerializer, value.start) + encodeSerializableElement(descriptor, 1, doubleSerializer, value.endInclusive) + } + } + + override fun deserialize(decoder: Decoder): ClosedRange { + val doubleSerializer = doubleSerializer + + return decoder.decodeStructure(descriptor) { + var start: Double? = null + var end: Double? = null + + if (decodeSequentially()) { + start = decodeSerializableElement(descriptor, 0, doubleSerializer) + end = decodeSerializableElement(descriptor, 1, doubleSerializer) + } else { + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> start = decodeSerializableElement(descriptor, index, doubleSerializer) + 1 -> end = decodeSerializableElement(descriptor, index, doubleSerializer) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + } + + start ?: throw SerializationException("The field start is missing") + end ?: throw SerializationException("The field end is missing") + + start..end + } + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/time/FormatTime.kt b/src/main/kotlin/com/github/rushyverse/api/time/FormatTime.kt new file mode 100644 index 00000000..41f517ad --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/time/FormatTime.kt @@ -0,0 +1,221 @@ +package com.github.rushyverse.api.time + +import com.github.rushyverse.api.APIPlugin +import com.github.rushyverse.api.extension.HOUR_IN_DAY +import com.github.rushyverse.api.extension.MINUTE_IN_HOUR +import com.github.rushyverse.api.extension.SECOND_IN_MINUTE +import com.github.rushyverse.api.extension.toIntOrString +import com.github.rushyverse.api.translation.Translator +import java.util.* +import kotlin.time.Duration + +/** + * String used to represent an infinite duration. + */ +public const val INFINITE_SYMBOL: String = "∞" + +/** + * Data class that represents a time format. + * It provides properties and companion functions to create time formats. + * + * @property second A function that formats the seconds. + * @property minute A function that formats the minutes. + * @property hour A function that formats the hours. + * @property day A function that formats the days. + * @property prefixSingleDigitWithZero Whether to prefix single digit time units with a zero. + * @property acceptZero Whether to accept zero as a value for the time units. + * If false, the time unit will not be displayed if the value is zero. + * @property beginAtZero Whether to begin the time format at the first non-zero time unit. + * @constructor Creates a new instance of FormatTime. + */ +public data class FormatTime( + val second: FormatPartTime? = null, + val minute: FormatPartTime? = null, + val hour: FormatPartTime? = null, + val day: FormatPartTime? = null, + val prefixSingleDigitWithZero: Boolean = DEFAULT_PREFIX_SINGLE_DIGIT_WITH_ZERO, + val acceptZero: Boolean = DEFAULT_ACCEPT_ZERO, + val beginAtZero: Boolean = DEFAULT_BEGIN_AT_ZERO +) { + + public companion object { + + private const val DEFAULT_PREFIX_SINGLE_DIGIT_WITH_ZERO = true + + private const val DEFAULT_ACCEPT_ZERO = true + + private const val DEFAULT_BEGIN_AT_ZERO = false + + private const val ZERO = "0" + + /** + * Regex used to get the number in a string. + * Will create groups named `text1`, `number` and `text2` to get the text before and after the number. + * The number found has to be a single digit. + */ + private val patternTranslationWithSingleDigitTime = Regex("^(?\\D*)(?\\d)(?\\D*)$") + + /** + * Constructs a FormatTime object for displaying time in long format. + * The long format is used for displaying time in a more readable way. + * @param translator The translator used for retrieving translations. + * @param locale The desired locale for the translations. + * @param bundle The name of the translation bundle to use. + * @property prefixSingleDigitWithZero Whether to prefix single digit time units with a zero. + * @property acceptZero Whether to accept zero as a value for the time units. + * If false, the time unit will not be displayed if the value is zero. + * @property beginAtZero Whether to begin the time format at the first non-zero time unit. + * @return A FormatTime object configured for long format. + */ + public fun long( + translator: Translator, + locale: Locale, + bundle: String = APIPlugin.BUNDLE_API, + prefixSingleDigitWithZero: Boolean = DEFAULT_PREFIX_SINGLE_DIGIT_WITH_ZERO, + acceptZero: Boolean = DEFAULT_ACCEPT_ZERO, + beginAtZero: Boolean = DEFAULT_BEGIN_AT_ZERO + ): FormatTime = FormatTime( + second = { translator.get("time.second.long", locale, arrayOf(it.toIntOrString()), bundle) }, + minute = { translator.get("time.minute.long", locale, arrayOf(it.toIntOrString()), bundle) }, + hour = { translator.get("time.hour.long", locale, arrayOf(it.toIntOrString()), bundle) }, + day = { translator.get("time.day.long", locale, arrayOf(it.toIntOrString()), bundle) }, + prefixSingleDigitWithZero = prefixSingleDigitWithZero, + acceptZero = acceptZero, + beginAtZero = beginAtZero + ) + + /** + * Constructs a FormatTime object for displaying time in short format. + * The short format is used for displaying time in a more compact way. + * @param translator The translator used for retrieving translations. + * @param locale The desired locale for the translations. + * @param bundle The name of the translation bundle to use. + * @property prefixSingleDigitWithZero Whether to prefix single digit time units with a zero. + * @property acceptZero Whether to accept zero as a value for the time units. + * If false, the time unit will not be displayed if the value is zero. + * @property beginAtZero Whether to begin the time format at the first non-zero time unit. + * @return A FormatTime object configured for short format. + */ + public fun short( + translator: Translator, + locale: Locale, + bundle: String = APIPlugin.BUNDLE_API, + prefixSingleDigitWithZero: Boolean = DEFAULT_PREFIX_SINGLE_DIGIT_WITH_ZERO, + acceptZero: Boolean = DEFAULT_ACCEPT_ZERO, + beginAtZero: Boolean = DEFAULT_BEGIN_AT_ZERO + ): FormatTime = FormatTime( + second = { translator.get("time.second.short", locale, arrayOf(it.toIntOrString()), bundle) }, + minute = { translator.get("time.minute.short", locale, arrayOf(it.toIntOrString()), bundle) }, + hour = { translator.get("time.hour.short", locale, arrayOf(it.toIntOrString()), bundle) }, + day = { translator.get("time.day.short", locale, arrayOf(it.toIntOrString()), bundle) }, + prefixSingleDigitWithZero = prefixSingleDigitWithZero, + acceptZero = acceptZero, + beginAtZero = beginAtZero + ) + } + + /** + * Get the day formatted time for the given duration. + * @param duration The duration to format. + * @return The formatted time if [day] is not null, null otherwise. + */ + public fun getDay(duration: Duration): String? { + day?.let { + val value = duration.inWholeDays + return getTimeOrZeroFormatted(it, value, true) + } + return null + } + + /** + * Get the hour formatted time for the given duration. + * @param duration The duration to format. + * @param isFirstUnit Whether the hour is the first unit to be displayed. + * @return The formatted time if [hour] is not null, null otherwise. + */ + public fun getHour(duration: Duration, isFirstUnit: Boolean): String? { + hour?.let { + val value = if (isFirstUnit) duration.inWholeHours else duration.inWholeHours % HOUR_IN_DAY + return getTimeOrZeroFormatted(it, value, isFirstUnit) + } + return null + } + + /** + * Get the minute formatted time for the given duration. + * @param duration The duration to format. + * @param isFirstUnit Whether the minute is the first unit to be displayed. + * @return The formatted time if [minute] is not null, null otherwise. + */ + public fun getMinute(duration: Duration, isFirstUnit: Boolean): String? { + minute?.let { + val value = if (isFirstUnit) duration.inWholeMinutes else duration.inWholeMinutes % MINUTE_IN_HOUR + return getTimeOrZeroFormatted(it, value, isFirstUnit) + } + return null + } + + /** + * Get the second formatted time for the given duration. + * @param duration The duration to format. + * @param isFirstUnit Whether the second is the first unit to be displayed. + * @return The formatted time if [second] is not null, null otherwise. + */ + public fun getSecond(duration: Duration, isFirstUnit: Boolean): String? { + second?.let { + val value = if (isFirstUnit) duration.inWholeSeconds else duration.inWholeSeconds % SECOND_IN_MINUTE + return getTimeOrZeroFormatted(it, value, isFirstUnit) + } + return null + } + + /** + * Get the formatted time for the given duration. + * If [time] is greater than 0, the formatted time will be the formatted [time]. + * If [time] is 0, and it is the first unit to be displayed, the formatted time will be 0. + * If [time] is 0, and [acceptZero] is true, the formatted time will be 0. + * @param format Format to use. + * @param time Time to format. + * @param isFirstUnit Whether the time is the first unit to be displayed. + * @return The formatted time if [format] is not null, null otherwise. + */ + private fun getTimeOrZeroFormatted(format: FormatPartTime, time: Long, isFirstUnit: Boolean): String? { + return when { + time > 0 -> time.toString() + isFirstUnit -> if (beginAtZero && acceptZero) ZERO else null + acceptZero -> ZERO + else -> null + }?.let { adapt(format(it)) } + } + + /** + * Adapts a string to the format according to the object configuration. + * @param string Value to adapt. + * @return The adapted string, can be the same as the input string. + */ + private fun adapt(string: String): String { + return if (prefixSingleDigitWithZero) { + prefixSingleDigitWithZero(string) + } else { + string + } + } + + /** + * Adds a prefix zero for single-digit numbers in a given string. + * + * @param string The input string. + * @return The modified string with prefix zero for single-digit numbers. + */ + private fun prefixSingleDigitWithZero(string: String): String { + return string.replace(patternTranslationWithSingleDigitTime) { matchResult -> + val (text1, number, text2) = matchResult.destructured + "${text1}0${number}${text2}" + } + } +} + +/** + * Type of function used to format a [Duration] part to a string. + */ +public typealias FormatPartTime = (String) -> String diff --git a/src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleNotRegisteredException.kt b/src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleNotRegisteredException.kt index 5fc5442a..93761db1 100644 --- a/src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleNotRegisteredException.kt +++ b/src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleNotRegisteredException.kt @@ -3,9 +3,9 @@ package com.github.rushyverse.api.translation import java.util.* /** - * Exception used when the system try to use a resource bundle not registered. + * Exception used when the system tries to use a resource bundle not registered. * @param bundleName Name of the bundle. * @param locale Locale searched. */ public class ResourceBundleNotRegisteredException(public val bundleName: String, public val locale: Locale) : - RuntimeException("The bundle [$bundleName] for locale [$locale] is not registered.") \ No newline at end of file + RuntimeException("The bundle [$bundleName] for locale [$locale] is not registered.") diff --git a/src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslationsProvider.kt b/src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslator.kt similarity index 75% rename from src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslationsProvider.kt rename to src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslator.kt index 2d3eb276..81c48e89 100644 --- a/src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslationsProvider.kt +++ b/src/main/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslator.kt @@ -6,13 +6,13 @@ import java.util.* /** * Loads the [ResourceBundle] called [bundleName] for all supported locales from [SupportedLanguage]. - * @see ResourceBundleTranslationsProvider.registerResourceBundle + * @see ResourceBundleTranslator.registerResourceBundle */ -public fun ResourceBundleTranslationsProvider.registerResourceBundleForSupportedLocales( +public fun ResourceBundleTranslator.registerResourceBundleForSupportedLocales( bundleName: String, loader: (String, Locale) -> ResourceBundle ) { - SupportedLanguage.values().forEach { + SupportedLanguage.entries.forEach { registerResourceBundle(bundleName, it.locale, loader) } } @@ -23,28 +23,24 @@ private val logger = KotlinLogging.logger { } * Translation provider backed by Java's [ResourceBundle]s. This makes use of `.properties` files that are standard * across the Java ecosystem. */ -public open class ResourceBundleTranslationsProvider : TranslationsProvider() { +public open class ResourceBundleTranslator(defaultBundle: String) : Translator(defaultBundle) { private val bundles: MutableMap, ResourceBundle> = mutableMapOf() - override fun get(key: String, locale: Locale, bundleName: String): String { - return getBundle(locale, bundleName).getString(key) - } - - override fun translate( + override fun get( key: String, locale: Locale, - bundleName: String, - replacements: Array + args: Array, + bundleName: String ): String { val string = try { - get(key, locale, bundleName) + getBundle(locale, bundleName).getString(key) } catch (e: MissingResourceException) { - logger.error("Unable to find translation for key '$key' in bundles: '$bundleName'") + logger.error("Unable to find translation for key '$key' in bundles: '$bundleName'", e) return key } - return MessageFormat(string, locale).format(replacements) + return MessageFormat(string, locale).format(args) } /** @@ -76,10 +72,10 @@ public open class ResourceBundleTranslationsProvider : TranslationsProvider() { } /** - * Create the key to retrieve bundle. + * Create the key to retrieve a bundle. * @param bundleName Name of the bundle. * @param locale Locale. * @return The key created. */ private fun createKey(bundleName: String, locale: Locale) = bundleName to locale -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/rushyverse/api/translation/SupportedLanguage.kt b/src/main/kotlin/com/github/rushyverse/api/translation/SupportedLanguage.kt index d8a32446..4f7d5ee4 100644 --- a/src/main/kotlin/com/github/rushyverse/api/translation/SupportedLanguage.kt +++ b/src/main/kotlin/com/github/rushyverse/api/translation/SupportedLanguage.kt @@ -2,13 +2,47 @@ package com.github.rushyverse.api.translation import java.util.* - /** - * List of supported locales to translate keys. + * Enumerates the set of languages supported for translation. + * + * Each entry in this enum represents a specific language and locale combination, + * providing both a human-readable display name and a [Locale] object for internal use. */ -public enum class SupportedLanguage(public val displayName: String, public val locale: Locale) { +public enum class SupportedLanguage( + /** + * A human-readable display name for the language. + */ + public val displayName: String, + /** + * Locale object representing the language and country code combination. + * This locale can be used with localization and internationalization libraries + * to format and retrieve translations. + */ + public val locale: Locale +) { + /** + * English language for the United Kingdom. + */ ENGLISH("English", Locale("en", "gb")), - FRENCH("Français", Locale("fr", "fr")) -} \ No newline at end of file + /** + * French language for France. + */ + FRENCH("Français", Locale("fr", "fr")), + + /** + * Spanish language for Spain. + */ + SPANISH("Español", Locale("es", "es")), + + /** + * German language for Germany. + */ + GERMAN("Deutsch", Locale("de", "de")), + + /** + * Chinese language for China. + */ + CHINESE("Chinese", Locale("zh", "cn")) +} diff --git a/src/main/kotlin/com/github/rushyverse/api/translation/TranslationsProvider.kt b/src/main/kotlin/com/github/rushyverse/api/translation/TranslationsProvider.kt deleted file mode 100644 index 82c81bb9..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/translation/TranslationsProvider.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.github.rushyverse.api.translation - -import java.util.* - -/** - * Translation provider interface, in charge of taking string keys and returning translated strings. - */ -public abstract class TranslationsProvider { - - /** - * Get a translation by key from the given locale and bundle name. - */ - public abstract fun get(key: String, locale: Locale, bundleName: String): String - - /** - * Get a formatted translation using the provided arguments. - */ - public abstract fun translate( - key: String, - locale: Locale, - bundleName: String, - replacements: Array - ): String - - /** - * Get a formatted translation using the provided arguments. - */ - public fun translate( - key: String, - locale: Locale, - bundleName: String - ): String = translate(key, locale, bundleName, emptyArray()) - - /** - * Get a formatted translation using the provided arguments. - */ - public fun translate( - key: String, - locale: Locale, - bundleName: String, - replacements: Collection - ): String = translate(key, locale, bundleName, replacements.toTypedArray()) -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/translation/Translator.kt b/src/main/kotlin/com/github/rushyverse/api/translation/Translator.kt new file mode 100644 index 00000000..1e109c59 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/translation/Translator.kt @@ -0,0 +1,70 @@ +package com.github.rushyverse.api.translation + +import com.github.rushyverse.api.extension.asComponent +import net.kyori.adventure.text.Component +import java.util.* + +/** + * Get a translation using the provided arguments. + * @receiver Translator The translator to use. + * @param key Key in the bundle to find the translation for. + * @param locale Language to translate to. + * @param bundleName Name of the bundle to use, by default [Translator.defaultBundle]. + * @return The translated string or the key if no translation was found in a [Component]. + */ +public fun Translator.getComponent( + key: String, + locale: Locale, + bundleName: String = defaultBundle, +): Component = getComponent(key, locale, emptyArray(), bundleName) + +/** + * Get a translation using the provided arguments. + * @receiver Translator The translator to use. + * @param key Key in the bundle to find the translation for. + * @param locale Language to translate to. + * @param args Arguments to format the translation with. + * @param bundleName Name of the bundle to use, by default [Translator.defaultBundle]. + * @return The translated string or the key if no translation was found in a [Component]. + */ +public fun Translator.getComponent( + key: String, + locale: Locale, + args: Array = emptyArray(), + bundleName: String = defaultBundle +): Component = get(key, locale, args, bundleName).asComponent() + +/** + * Translation provider interface, in charge of taking string keys and returning translated strings. + * @property defaultBundle The default bundle to use for translations. + */ +public abstract class Translator(public val defaultBundle: String) { + + /** + * Get a translation using the provided arguments. + * @param key Key in the bundle to find the translation for. + * @param locale Language to translate to. + * @param bundleName Name of the bundle to use, by default [defaultBundle]. + * @return The translated string or the key if no translation was found. + */ + public fun get( + key: String, + locale: Locale, + bundleName: String = defaultBundle + ): String = get(key, locale, emptyArray(), bundleName) + + /** + * Get a formatted translation using the provided arguments. + * @param key Key in the bundle to find the translation for. + * @param locale Language to translate to. + * @param args Arguments to format the translation with. + * @param bundleName Name of the bundle to use, by default [defaultBundle]. + * @return The translated string or the key if no translation was found. + */ + public abstract fun get( + key: String, + locale: Locale, + args: Array = emptyArray(), + bundleName: String = defaultBundle + ): String +} diff --git a/src/main/kotlin/com/github/rushyverse/api/utils/FileUtils.kt b/src/main/kotlin/com/github/rushyverse/api/utils/FileUtils.kt deleted file mode 100644 index 06ecaffd..00000000 --- a/src/main/kotlin/com/github/rushyverse/api/utils/FileUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.rushyverse.api.utils - -import java.io.File - -/** - * Get the current directory where is executed the program. - */ -public val workingDirectory: File - get() = File(System.getProperty("user.dir")) \ No newline at end of file diff --git a/src/main/kotlin/com/github/rushyverse/api/world/Area.kt b/src/main/kotlin/com/github/rushyverse/api/world/Area.kt new file mode 100644 index 00000000..2f7d2247 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/world/Area.kt @@ -0,0 +1,29 @@ +package com.github.rushyverse.api.world + +import org.bukkit.Location + +/** + * Checks if the given Location is within the specified Area. + * + * @receiver The Location object. + * @param area The Area to check against. + * @return true if the Location is within the Area, false otherwise. + */ +public infix fun Location.isIn(area: Area): Boolean = area.isInArea(this) + +/** + * Represents an area in the world. + * @property location The location of the area. + */ +public interface Area { + + public var location: Location + + /** + * Determines if a given location is within the specified area. + * + * @param location The location to check. + * @return `true` if the location is within the area, `false` otherwise. + */ + public fun isInArea(location: Location): Boolean +} diff --git a/src/main/kotlin/com/github/rushyverse/api/world/CubeArea.kt b/src/main/kotlin/com/github/rushyverse/api/world/CubeArea.kt new file mode 100644 index 00000000..e7865b42 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/world/CubeArea.kt @@ -0,0 +1,131 @@ +package com.github.rushyverse.api.world + +import com.github.rushyverse.api.extension.centerRelative +import com.github.rushyverse.api.extension.copy +import com.github.rushyverse.api.extension.minMaxOf +import com.github.rushyverse.api.serializer.LocationSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.* +import org.bukkit.Location + +/** + * Serializer class for [CubeArea] objects. + */ +public object CubeAreaSerializer : KSerializer { + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("cube") { + val locationDescriptor = LocationSerializer.descriptor + element("location1", locationDescriptor) + element("location2", locationDescriptor) + } + + override fun deserialize(decoder: Decoder): CubeArea { + val locationSerializer = LocationSerializer + + return decoder.decodeStructure(descriptor) { + var loc1: Location? = null + var loc2: Location? = null + + if (decodeSequentially()) { + loc1 = decodeSerializableElement(descriptor, 0, locationSerializer) + loc2 = decodeSerializableElement(descriptor, 1, locationSerializer) + } else { + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> loc1 = decodeSerializableElement(descriptor, index, locationSerializer) + 1 -> loc2 = decodeSerializableElement(descriptor, index, locationSerializer) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + } + + CubeArea( + loc1 ?: throw SerializationException("The field location1 is missing"), + loc2 ?: throw SerializationException("The field location2 is missing"), + ) + } + } + + override fun serialize(encoder: Encoder, value: CubeArea) { + val locationSerializer = LocationSerializer + + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, locationSerializer, value.min) + encodeSerializableElement(descriptor, 1, locationSerializer, value.max) + } + } + +} + +/** + * A cuboid area defined by two positions. + * @property min Minimum position. + * @property max Maximum position. + */ +@Serializable(with = CubeAreaSerializer::class) +public class CubeArea(loc1: Location, loc2: Location) : Area { + + public override var location: Location + get() = max.centerRelative(min) + set(value) { + // The new position becomes the center of the cube. + val halfSize = max.centerRelative(min) + min = value.copy().subtract(halfSize) + max = value.copy().add(halfSize) + } + + public var min: Location + private set + + public var max: Location + private set + + + init { + val world1 = loc1.world + val world2 = loc2.world + require(world1 === world2) { "Locations must be in the same world" } + + val (x1, x2) = minMaxOf(loc1.x, loc2.x) + val (y1, y2) = minMaxOf(loc1.y, loc2.y) + val (z1, z2) = minMaxOf(loc1.z, loc2.z) + this.min = Location(world1, x1, y1, z1) + this.max = Location(world2, x2, y2, z2) + } + + public override fun isInArea(location: Location): Boolean { + val min = min + val max = max + return min.world === location.world && + location.x in min.x..max.x && + location.y in min.y..max.y && + location.z in min.z..max.z + } + + override fun toString(): String { + return "CubeArea(min=$min, max=$max)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CubeArea + + if (min != other.min) return false + if (max != other.max) return false + + return true + } + + override fun hashCode(): Int { + var result = min.hashCode() + result = 31 * result + max.hashCode() + return result + } +} diff --git a/src/main/kotlin/com/github/rushyverse/api/world/CylinderArea.kt b/src/main/kotlin/com/github/rushyverse/api/world/CylinderArea.kt new file mode 100644 index 00000000..d1c0f24f --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/world/CylinderArea.kt @@ -0,0 +1,176 @@ +package com.github.rushyverse.api.world + +import com.github.rushyverse.api.serializer.LocationSerializer +import com.github.rushyverse.api.serializer.RangeDoubleSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.* +import org.bukkit.Location +import kotlin.math.pow +import kotlin.math.sqrt + +/** + * Serializer class for [CylinderArea] objects. + */ +public object CylinderAreaSerializer : KSerializer { + + /** + * Serializer for [Location]. + */ + private val locationSerializer get() = LocationSerializer + + /** + * Serializer for [Double]. + */ + private val doubleSerializer get() = Double.serializer() + + /** + * Serializer for [ClosedRange] with [Double] value. + */ + private val rangeDoubleSerializer get() = RangeDoubleSerializer + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("cylinder") { + element("location", locationSerializer.descriptor) + element("radius", doubleSerializer.descriptor) + element("height", rangeDoubleSerializer.descriptor) + } + + override fun deserialize(decoder: Decoder): CylinderArea { + return decoder.decodeStructure(descriptor) { + var location: Location? = null + var radius: Double? = null + var height: ClosedRange? = null + + if (decodeSequentially()) { + location = decodeSerializableElement(descriptor, 0, locationSerializer) + radius = decodeSerializableElement(descriptor, 1, doubleSerializer) + height = decodeSerializableElement(descriptor, 2, rangeDoubleSerializer) + } else { + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> location = decodeSerializableElement(descriptor, index, locationSerializer) + 1 -> radius = decodeSerializableElement(descriptor, index, doubleSerializer) + 2 -> height = decodeSerializableElement(descriptor, index, rangeDoubleSerializer) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + } + + CylinderArea( + location ?: throw SerializationException("The field location is missing"), + radius ?: throw SerializationException("The field radius is missing"), + height ?: throw SerializationException("The field height is missing"), + ) + } + } + + override fun serialize(encoder: Encoder, value: CylinderArea) { + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, locationSerializer, value.location) + encodeSerializableElement(descriptor, 1, doubleSerializer, value.radius) + encodeSerializableElement(descriptor, 2, rangeDoubleSerializer, value.height) + } + } + +} + +/** + * Represents a cylindrical area in a three-dimensional space. + * + * @property location The location of the center of the cylinder. + * @property radius The radius of the cylinder. Must be greater than or equal to 0.0. + * @property height The range of valid heights for the cylinder. + * @constructor Creates a CylinderArea object with the specified location, radius, and height range. + */ +@Serializable(with = CylinderAreaSerializer::class) +public class CylinderArea( + override var location: Location, + radius: Double, + public val height: ClosedRange, +) : Area { + + public var radius: Double = radius + set(value) { + assertRadiusValue(value) + field = value + } + + init { + assertRadiusValue(radius) + } + + /** + * Verifies that the new radius value is greater than or equal to 0.0. + * @param value New radius value. + */ + private fun assertRadiusValue(value: Double) { + require(value >= 0.0) { "Radius must be greater than or equal to 0.0" } + } + + override fun isInArea(location: Location): Boolean { + val areaLocation = this.location + return isSameWorld(location, areaLocation) + && isInRadius(location, areaLocation) + && isInHeight(location) + } + + /** + * Determines if two locations are in the same world. + * + * @param location The first location. + * @param areaLocation The second location. + * @return `true` if both locations are in the same world, `false` otherwise. + */ + private fun isSameWorld(location: Location, areaLocation: Location): Boolean = + location.world === areaLocation.world + + /** + * Checks if the given location is within the specified height range. + * + * @param location The location to check. + * @return `true` if the location is within the height range, `false` otherwise. + */ + private fun isInHeight(location: Location): Boolean = + location.y in height + + /** + * Determines if a given location is within a specified radius of an area location. + * + * @param location The location to check. + * @param areaLocation The area location to compare against. + * @return `true` if the location is within the radius of the area location, `false` otherwise. + */ + private fun isInRadius(location: Location, areaLocation: Location): Boolean = + sqrt((location.x - areaLocation.x).pow(2.0) + (location.z - areaLocation.z).pow(2.0)) <= radius + + override fun toString(): String { + return "CylinderArea(location=$location, height=$height, radius=$radius)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CylinderArea + + if (location != other.location) return false + if (height != other.height) return false + if (radius != other.radius) return false + + return true + } + + override fun hashCode(): Int { + var result = location.hashCode() + result = 31 * result + height.hashCode() + result = 31 * result + radius.hashCode() + return result + } + + +} diff --git a/src/main/kotlin/com/github/rushyverse/api/world/SphereArea.kt b/src/main/kotlin/com/github/rushyverse/api/world/SphereArea.kt new file mode 100644 index 00000000..aec05a10 --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/world/SphereArea.kt @@ -0,0 +1,128 @@ +package com.github.rushyverse.api.world + +import com.github.rushyverse.api.serializer.LocationSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.* +import org.bukkit.Location +import kotlin.math.pow + +/** + * Serializer class for [SphereArea] objects. + */ +public object SphereAreaSerializer : KSerializer { + + /** + * Serializer for [Location]. + */ + private val locationSerializer get() = LocationSerializer + + /** + * Serializer for [Double]. + */ + private val doubleSerializer get() = Double.serializer() + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("sphere") { + element("location", locationSerializer.descriptor) + element("radius", doubleSerializer.descriptor) + } + + override fun deserialize(decoder: Decoder): SphereArea { + return decoder.decodeStructure(descriptor) { + var location: Location? = null + var radius: Double? = null + + if (decodeSequentially()) { + location = decodeSerializableElement(descriptor, 0, locationSerializer) + radius = decodeSerializableElement(descriptor, 1, doubleSerializer) + } else { + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> location = decodeSerializableElement(descriptor, index, locationSerializer) + 1 -> radius = decodeSerializableElement(descriptor, index, doubleSerializer) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + } + + SphereArea( + location ?: throw SerializationException("The field location is missing"), + radius ?: throw SerializationException("The field radius is missing"), + ) + } + } + + override fun serialize(encoder: Encoder, value: SphereArea) { + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, locationSerializer, value.location) + encodeSerializableElement(descriptor, 1, doubleSerializer, value.radius) + } + } + +} + +/** + * Represents a spherical area with a certain radius. + * + * @property location The center location of the sphere. + * @property radius The radius of the sphere. + */ +@Serializable(with = SphereAreaSerializer::class) +public class SphereArea( + override var location: Location, + radius: Double, +) : Area { + + public var radius: Double = radius + set(value) { + assertRadiusValue(value) + field = value + } + + init { + assertRadiusValue(radius) + } + + /** + * Verifies that the new radius value is greater than or equal to 0.0. + * @param value New radius value. + */ + private fun assertRadiusValue(value: Double) { + require(value >= 0.0) { "Radius must be greater than or equal to 0.0" } + } + + override fun isInArea(location: Location): Boolean { + val areaLocation = this.location + return location.world === areaLocation.world // Same world + && location.distanceSquared(areaLocation) <= radius.pow(2) // Distance is less than or equal to radius + } + + override fun toString(): String { + return "SphereArea(location=$location, radius=$radius)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SphereArea + + if (location != other.location) return false + if (radius != other.radius) return false + + return true + } + + override fun hashCode(): Int { + var result = location.hashCode() + result = 31 * result + radius.hashCode() + return result + } + + +} diff --git a/src/main/kotlin/com/github/rushyverse/api/world/exception/WorldDifferentException.kt b/src/main/kotlin/com/github/rushyverse/api/world/exception/WorldDifferentException.kt new file mode 100644 index 00000000..cbeeaa1e --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/world/exception/WorldDifferentException.kt @@ -0,0 +1,22 @@ +package com.github.rushyverse.api.world.exception + +import org.bukkit.World + +/** + * Represents an exception that is thrown when two provided worlds are not the same or are incompatible + * for a specific operation. + * + * This can be used in contexts where operations rely on the assumption that entities, objects, or + * other items belong to the same world, and any mismatch would result in unexpected behavior. + * + * @property world1 The first world involved in the check, which may be the source or origin. + * @property world2 The second world involved in the check, which may be the destination or target. + * @param message Optional error message detailing the context or reason for the exception. + * If not provided, a default message may be utilized. + */ +public class WorldDifferentException( + public val world1: World? = null, + public val world2: World? = null, + message: String? = null +) : + RuntimeException(message ?: "The worlds ${world1?.name} and ${world2?.name} are different.") diff --git a/src/main/kotlin/com/github/rushyverse/api/world/exception/WorldNotFoundException.kt b/src/main/kotlin/com/github/rushyverse/api/world/exception/WorldNotFoundException.kt new file mode 100644 index 00000000..0de63f1a --- /dev/null +++ b/src/main/kotlin/com/github/rushyverse/api/world/exception/WorldNotFoundException.kt @@ -0,0 +1,6 @@ +package com.github.rushyverse.api.world.exception + +/** + * Exception if a world is not into the server. + */ +public class WorldNotFoundException(message: String? = null) : RuntimeException(message) diff --git a/src/main/resources/api_en_GB.properties b/src/main/resources/api_en_GB.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/resources/api_fr_FR.properties b/src/main/resources/api_fr_FR.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/resources/api_translate.properties b/src/main/resources/api_translate.properties new file mode 100644 index 00000000..786403e2 --- /dev/null +++ b/src/main/resources/api_translate.properties @@ -0,0 +1,21 @@ +team.white=White +team.red=Red +team.blue=Blue +team.green=Green +team.yellow=Yellow +team.purple=Purple +team.aqua=Aqua +team.black=Black +player.join.team={0} joined {1} team +time.day.short={0}d +time.hour.short={0}h +time.minute.short={0}m +time.second.short={0}s +time.day.long={0} {0, plural, one {day} other {days}} +time.hour.long={0} {0, plural, one {hour} other {hours}} +time.minute.long={0} {0, plural, one {minute} other {minutes}} +time.second.long={0} {0, plural, one {second} other {seconds}} +team.blue.member=Blue +team.green.member=Green +team.purple.member=Purple +team.white.member=White diff --git a/src/main/resources/api_translate_de_DE.properties b/src/main/resources/api_translate_de_DE.properties new file mode 100644 index 00000000..57cd4d0d --- /dev/null +++ b/src/main/resources/api_translate_de_DE.properties @@ -0,0 +1,21 @@ +team.blue.member=Blau +team.green.member=Grün +team.green=Grün +team.yellow=Gelb +team.blue=Blau +team.red=Rot +team.purple=Lila +team.aqua=Aqua +team.black=Schwarz +team.white.member=Weiß +team.white=Weiß +team.purple.member=Lila +time.day.short={0}T +time.hour.short={0}S +time.minute.short={0}min +time.second.short={0}s +time.day.long={0} {0, plural, one {Tag} other {Tage}} +time.hour.long={0} {0, plural, one {Stunde} other {Stunden}} +time.minute.long={0} {0, plural, one {Minute} other {Minuten}} +time.second.long={0} {0, plural, one {Sekunde} other {Sekunden}} +player.join.team={0} ist dem Team {1} beigetreten \ No newline at end of file diff --git a/src/main/resources/api_translate_en_GB.properties b/src/main/resources/api_translate_en_GB.properties new file mode 100644 index 00000000..0e9bc610 --- /dev/null +++ b/src/main/resources/api_translate_en_GB.properties @@ -0,0 +1,21 @@ +time.day.long={0} {0, plural, one {day} other {days}} +time.day.short={0}d +time.hour.short={0}h +time.minute.short={0}m +time.second.short={0}s +time.hour.long={0} {0, plural, one {hour} other {hours}} +time.minute.long={0} {0, plural, one {minute} other {minutes}} +time.second.long={0} {0, plural, one {second} other {seconds}} +team.red=Red +team.white=White +team.blue=Blue +team.green=Green +team.yellow=Yellow +team.purple=Purple +team.aqua=Aqua +team.black=Black +player.join.team={0} joined {1} team +team.blue.member=Blue +team.green.member=Green +team.purple.member=Purple +team.white.member=White diff --git a/src/main/resources/api_translate_es_ES.properties b/src/main/resources/api_translate_es_ES.properties new file mode 100644 index 00000000..64a0f3ec --- /dev/null +++ b/src/main/resources/api_translate_es_ES.properties @@ -0,0 +1,21 @@ +time.day.short={0}d +time.hour.short={0}h +time.minute.short={0}m +time.second.short={0}s +time.day.long={0} {0, plural, one {día} other {días}} +time.hour.long={0} {0, plural, one {hora} other {horas}} +time.minute.long={0} {0, plural, one {minuto} other {minutos}} +time.second.long={0} {0, plural, one {segundo} other {segundos}} +team.black=Negro +team.blue.member=Azul +team.blue=Azul +team.green=Verde +team.green.member=Verde +team.white.member=Blanco +team.white=Blanco +team.red=Rojo +team.yellow=Amarillo +team.aqua=Agua +team.purple=Violeta +team.purple.member=Violeta +player.join.team={0} se unió al equipo {1} diff --git a/src/main/resources/api_translate_fr_FR.properties b/src/main/resources/api_translate_fr_FR.properties new file mode 100644 index 00000000..4b147315 --- /dev/null +++ b/src/main/resources/api_translate_fr_FR.properties @@ -0,0 +1,21 @@ +team.white=Blanche +team.white.member=Blanc +team.red=Rouge +team.blue=Bleue +team.blue.member=Bleu +team.green=Verte +team.green.member=Vert +team.yellow=Jaune +team.purple=Violette +team.purple.member=Violet +team.aqua=Aqua +team.black=Noire +player.join.team={0} a rejoint l'équipe {1} +time.day.short={0}j +time.hour.short={0}h +time.minute.short={0}m +time.second.short={0}s +time.day.long={0} {0, plural, zero {jour} one {jour} other {jours}} +time.hour.long={0} {0, plural, zero {heure} one {heure} other {heures}} +time.minute.long={0} {0, plural, zero {minute} one {minute} other {minutes}} +time.second.long={0} {0, plural, zero {seconde} one {seconde} other {secondes}} diff --git a/src/main/resources/api_translate_zh_CN.properties b/src/main/resources/api_translate_zh_CN.properties new file mode 100644 index 00000000..343d223b --- /dev/null +++ b/src/main/resources/api_translate_zh_CN.properties @@ -0,0 +1,21 @@ +time.day.short={0}天 +time.hour.short={0}小时 +time.minute.short={0}分 +time.second.short={0}秒 +time.day.long={0}天 +time.hour.long={0}小时 +time.minute.long={0}分 +time.second.long={0}秒 +team.white.member=白 +team.purple.member=紫 +team.blue.member=蓝 +team.green.member=绿 +team.aqua=青队 +team.black=黑队 +team.purple=紫队 +team.yellow=黄队 +team.blue=蓝队 +team.red=红队 +team.white=白队 +team.green=绿队 +player.join.team={0} 加入了 {1} 的团队 diff --git a/src/main/resources/api.properties b/src/main/resources/config.yml similarity index 100% rename from src/main/resources/api.properties rename to src/main/resources/config.yml diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 00000000..0e135c1e --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,7 @@ +name: paper-api +version: 1.0 +author: Distractic +api-version: 1.19 +main: com.github.rushyverse.api.APIPlugin +depend: + - CommandAPI \ No newline at end of file diff --git a/src/main/resources/simplelogger.properties b/src/main/resources/simplelogger.properties new file mode 100644 index 00000000..d4ce890b --- /dev/null +++ b/src/main/resources/simplelogger.properties @@ -0,0 +1,2 @@ +org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.showDateTime=true \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/AbstractKoinTest.kt b/src/test/kotlin/com/github/rushyverse/api/AbstractKoinTest.kt new file mode 100644 index 00000000..4a7ea6aa --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/AbstractKoinTest.kt @@ -0,0 +1,46 @@ +package com.github.rushyverse.api + +import com.github.rushyverse.api.koin.CraftContext +import com.github.rushyverse.api.koin.loadModule +import com.github.rushyverse.api.utils.randomString +import io.mockk.every +import io.mockk.mockk +import org.koin.core.module.Module +import org.koin.dsl.ModuleDeclaration +import kotlin.test.AfterTest +import kotlin.test.BeforeTest + +open class AbstractKoinTest { + + lateinit var plugin: Plugin + + private lateinit var pluginId: String + + @BeforeTest + open fun onBefore() { + pluginId = randomString() + CraftContext.startKoin(pluginId) { } + CraftContext.startKoin(APIPlugin.ID_API) { } + + loadTestModule { + plugin = mockk { + every { id } returns pluginId + every { name } returns randomString() + } + single { plugin } + } + } + + @AfterTest + open fun onAfter() { + CraftContext.stopKoin(pluginId) + CraftContext.stopKoin(APIPlugin.ID_API) + } + + fun loadTestModule(moduleDeclaration: ModuleDeclaration): Module = + loadModule(pluginId, moduleDeclaration = moduleDeclaration) + + fun loadApiTestModule(moduleDeclaration: ModuleDeclaration): Module = + loadModule(APIPlugin.ID_API, moduleDeclaration = moduleDeclaration) + +} diff --git a/src/test/kotlin/com/github/rushyverse/api/AbstractTest.kt b/src/test/kotlin/com/github/rushyverse/api/AbstractTest.kt deleted file mode 100644 index 79e7b54f..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/AbstractTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.github.rushyverse.api - -import com.github.rushyverse.api.configuration.* -import com.github.rushyverse.api.utils.getAvailablePort -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.hocon.Hocon -import org.junit.jupiter.api.io.TempDir -import java.io.File -import kotlin.test.AfterTest -import kotlin.test.BeforeTest - -@Serializable -data class TestConfiguration( - override val server: ServerConfiguration -) : IConfiguration - -@SerialName("server") -@Serializable -data class ServerConfiguration( - override val port: Int, - override val world: String, - override val onlineMode: Boolean, - override val velocity: VelocityConfiguration, - override val bungeeCord: BungeeCordConfiguration -) : IServerConfiguration - -abstract class AbstractTest { - - companion object { - private const val PROPERTY_USER_DIR = "user.dir" - const val DEFAULT_WORLD = "world" - } - - @TempDir - lateinit var tmpDirectory: File - - private lateinit var initCurrentDirectory: String - - protected val expectedDefaultConfiguration: TestConfiguration - get() = TestConfiguration( - ServerConfiguration( - 25565, - DEFAULT_WORLD, - false, - VelocityConfiguration(false, ""), - BungeeCordConfiguration(false, emptySet()) - ) - ) - - @BeforeTest - open fun onBefore() { - initCurrentDirectory = System.getProperty(PROPERTY_USER_DIR) - System.setProperty(PROPERTY_USER_DIR, tmpDirectory.absolutePath) - } - - @AfterTest - open fun onAfter() { - System.setProperty(PROPERTY_USER_DIR, initCurrentDirectory) - } - - protected fun fileOfTmpDirectory(fileName: String) = File(tmpDirectory, fileName) - - protected fun configurationToHocon(configuration: TestConfiguration) = - Hocon.encodeToConfig(TestConfiguration.serializer(), configuration) - - protected fun configurationToHoconFile( - configuration: TestConfiguration, - file: File = fileOfTmpDirectory(IConfigurationReader.DEFAULT_CONFIG_FILE_NAME) - ) = - file.writeText(configurationToHocon(configuration).root().render()) - - protected fun copyFolderFromResourcesToFolder(folderName: String, destination: File) { - val folder = File(javaClass.classLoader.getResource(folderName)!!.file) - folder.copyRecursively(destination) - } - - protected fun copyWorldInTmpDirectory( - configuration: TestConfiguration = defaultConfigurationOnAvailablePort() - ) { - val worldFile = fileOfTmpDirectory(configuration.server.world) - copyFolderFromResourcesToFolder(DEFAULT_WORLD, worldFile) - } - - protected fun defaultConfigurationOnAvailablePort() = expectedDefaultConfiguration.let { - it.copy(server = it.server.copy(port = getAvailablePort())) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/RushyServerTest.kt b/src/test/kotlin/com/github/rushyverse/api/RushyServerTest.kt deleted file mode 100644 index c9a08cbd..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/RushyServerTest.kt +++ /dev/null @@ -1,231 +0,0 @@ -package com.github.rushyverse.api - -import com.github.rushyverse.api.command.GamemodeCommand -import com.github.rushyverse.api.command.GiveCommand -import com.github.rushyverse.api.command.KickCommand -import com.github.rushyverse.api.command.StopCommand -import com.github.rushyverse.api.configuration.* -import com.github.rushyverse.api.utils.randomString -import kotlinx.coroutines.test.runTest -import net.minestom.server.MinecraftServer -import net.minestom.server.extras.MojangAuth -import net.minestom.server.extras.bungee.BungeeCordProxy -import net.minestom.server.extras.velocity.VelocityProxy -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import java.io.IOException -import kotlin.test.* - -class TestServer(private val configuration: String? = null) : RushyServer() { - - override suspend fun start() { - start(configuration) { - registerCommands() - } - } -} - -class RushyServerTest : AbstractTest() { - - @AfterTest - override fun onAfter() { - super.onAfter() - if (MinecraftServer.process() != null) { - MinecraftServer.stopCleanly() - } - } - - @Test - fun `should have the correct bundle name`() { - assertEquals("api", RushyServer.API.BUNDLE_API) - } - - @Nested - inner class CreateOrGetConfiguration { - - @Test - fun `should create a configuration file if it doesn't exist`() = runTest { - assertThrows { - TestServer().start() - } - val configurationFile = fileOfTmpDirectory(IConfigurationReader.DEFAULT_CONFIG_FILE_NAME) - assertTrue { configurationFile.isFile } - - val configuration = HoconConfigurationReader().readConfigurationFile(configurationFile) - assertEquals(expectedDefaultConfiguration, configuration) - } - - @Test - fun `should use the configuration file if exists`() = runTest { - val configurationFile = fileOfTmpDirectory(randomString()) - assertTrue { configurationFile.createNewFile() } - - val configuration = defaultConfigurationOnAvailablePort() - configurationToHoconFile(configuration, configurationFile) - - val exception = assertThrows { - TestServer(configurationFile.absolutePath).start() - } - assertEquals(configuration.server.world, exception.file.name) - } - - } - - @Nested - inner class UseConfiguration { - - @Test - fun `should use configuration to turn on the server`() = runTest { - val configuration = defaultConfigurationOnAvailablePort() - val configurationFile = fileOfTmpDirectory(randomString()) - configurationToHoconFile(configuration, configurationFile) - - copyWorldInTmpDirectory(configuration) - - TestServer(configurationFile.absolutePath).start() - - // If no exception is thrown, the world is loaded - assertTrue { MinecraftServer.isStarted() } - - val server = MinecraftServer.getServer() - assertEquals(configuration.server.port, server.port) - assertEquals("0.0.0.0", server.address) - } - } - - @Nested - inner class Command { - - @Test - fun `should load all commands`() = runTest { - copyWorldInTmpDirectory() - TestServer().start() - - val commandManager = MinecraftServer.getCommandManager() - assertContentEquals( - commandManager.commands.asSequence().map { it::class.java }.sortedBy { it.simpleName }.toList(), - sequenceOf( - StopCommand::class.java, - KickCommand::class.java, - GiveCommand::class.java, - GamemodeCommand::class.java - ).sortedBy { it.simpleName }.toList() - ) - } - } - - @Nested - inner class Velocity { - - @BeforeTest - fun onBefore() { - VelocityProxy::class.java.getDeclaredField("enabled").apply { - isAccessible = true - setBoolean(null, false) - } - } - - @Test - fun `should load velocity`() = runTest { - test(true, "secret") - } - - @Test - fun `should not load velocity`() = runTest { - test(false, "") - } - - private suspend fun test(enabled: Boolean, secret: String) { - val defaultConfiguration = expectedDefaultConfiguration - val configuration = expectedDefaultConfiguration.copy( - defaultConfiguration.server.copy( - velocity = VelocityConfiguration(enabled, secret) - ) - ) - configurationToHoconFile(configuration) - copyWorldInTmpDirectory(configuration) - TestServer().start() - - assertEquals(enabled, VelocityProxy.isEnabled()) - } - } - - @Nested - inner class BungeeCord { - - @BeforeTest - fun onBefore() { - BungeeCordProxy::class.java.getDeclaredField("enabled").apply { - isAccessible = true - setBoolean(null, false) - } - BungeeCordProxy.setBungeeGuardTokens(null) - } - - @Test - fun `should load bungeecord`() = runTest { - test(true, "test") - assertTrue(BungeeCordProxy.isValidBungeeGuardToken("test")) - } - - @Test - fun `should not load bungeecord`() = runTest { - test(false, "") - } - - private suspend fun test(enabled: Boolean, secret: String) { - val defaultConfiguration = expectedDefaultConfiguration - val configuration = expectedDefaultConfiguration.copy( - server = defaultConfiguration.server.copy( - bungeeCord = BungeeCordConfiguration(enabled, setOf(secret)) - ) - ) - - configurationToHoconFile(configuration) - copyWorldInTmpDirectory(configuration) - - TestServer().start() - - assertEquals(enabled, BungeeCordProxy.isEnabled()) - assertEquals(enabled, BungeeCordProxy.isBungeeGuardEnabled()) - } - } - - @Nested - inner class OnlineMode { - - @BeforeTest - fun onBefore() { - MojangAuth::class.java.getDeclaredField("enabled").apply { - isAccessible = true - setBoolean(null, false) - } - } - - @Test - fun `should set online mode`() = runTest { - test(true) - } - - @Test - fun `should set offline mode`() = runTest { - test(false) - } - - private suspend fun test(onlineMode: Boolean) { - val defaultConfiguration = expectedDefaultConfiguration - val configuration = expectedDefaultConfiguration.copy( - server = defaultConfiguration.server.copy( - onlineMode = onlineMode - ) - ) - configurationToHoconFile(configuration) - copyWorldInTmpDirectory(configuration) - TestServer().start() - - assertEquals(onlineMode, MojangAuth.isEnabled()) - } - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/command/CommandMessagesTest.kt b/src/test/kotlin/com/github/rushyverse/api/command/CommandMessagesTest.kt deleted file mode 100644 index a161e789..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/command/CommandMessagesTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.github.rushyverse.api.command - -import io.mockk.every -import io.mockk.mockk -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.minestom.server.command.CommandSender -import net.minestom.server.entity.Player -import org.junit.jupiter.api.Nested -import kotlin.test.Test -import kotlin.test.assertEquals - -class CommandMessagesTest { - - @Nested - inner class SendPlayerNotFoundMessage { - - @Test - fun `should send component translatable if sender is player`() { - val sender = mockk() - var component: Component? = null - every { sender.sendMessage(any()) } answers { - component = arg(0) - } - CommandMessages.sendPlayerNotFoundMessage(sender) - - assertEquals(Component.translatable("argument.entity.notfound.player", NamedTextColor.RED), component) - } - - @Test - fun `should send component text if sender is not player`() { - val sender = mockk() - var component: Component? = null - every { sender.sendMessage(any()) } answers { - component = arg(0) - } - CommandMessages.sendPlayerNotFoundMessage(sender) - - assertEquals(Component.text("No player was found", NamedTextColor.RED), component) - } - - } - - @Test - fun `should send missing permission message`() { - val sender = mockk() - var component: Component? = null - every { sender.sendMessage(any()) } answers { - component = arg(0) - } - CommandMessages.sendMissingPermissionMessage(sender) - - assertEquals(Component.translatable("commands.help.failed", NamedTextColor.RED), component) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/configuration/BungeeCordConfigurationTest.kt b/src/test/kotlin/com/github/rushyverse/api/configuration/BungeeCordConfigurationTest.kt deleted file mode 100644 index c606b91a..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/configuration/BungeeCordConfigurationTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.github.rushyverse.api.configuration - -import com.github.rushyverse.api.utils.randomString -import kotlinx.serialization.json.Json -import org.junit.jupiter.api.Nested -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import kotlin.test.assertEquals - -class BungeeCordConfigurationTest { - - @Nested - inner class Serialize { - - @ValueSource(booleans = [true, false]) - @ParameterizedTest - fun `with empty secrets`(enabled: Boolean) { - val configuration = BungeeCordConfiguration(enabled, emptySet()) - val serialize = Json.encodeToString(BungeeCordConfiguration.serializer(), configuration) - assertEquals("{\"enabled\":$enabled,\"secrets\":[]}", serialize) - } - - @ValueSource(booleans = [true, false]) - @ParameterizedTest - fun `with non empty secrets`(enabled: Boolean) { - val secrets = listOf(randomString(), randomString()) - val configuration = BungeeCordConfiguration(enabled, secrets.toSet()) - val serialize = Json.encodeToString(BungeeCordConfiguration.serializer(), configuration) - assertEquals("{\"enabled\":$enabled,\"secrets\":[\"${secrets[0]}\",\"${secrets[1]}\"]}", serialize) - } - - } - - @Nested - inner class Deserialize { - - @ValueSource(booleans = [true, false]) - @ParameterizedTest - fun `with empty secrets`(enabled: Boolean) { - val string = "{\"enabled\":$enabled,\"secrets\":[]}" - val deserialize = Json.decodeFromString(BungeeCordConfiguration.serializer(), string) - assertEquals(BungeeCordConfiguration(enabled, emptySet()), deserialize) - } - - @ValueSource(booleans = [true, false]) - @ParameterizedTest - fun `with non empty secrets`(enabled: Boolean) { - val secrets = listOf(randomString(), randomString()) - val string = "{\"enabled\":$enabled,\"secrets\":[\"${secrets[0]}\",\"${secrets[1]}\"]}" - val deserialize = Json.decodeFromString(BungeeCordConfiguration.serializer(), string) - assertEquals(BungeeCordConfiguration(enabled, secrets.toSet()), deserialize) - } - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/configuration/HoconConfigurationReaderTest.kt b/src/test/kotlin/com/github/rushyverse/api/configuration/HoconConfigurationReaderTest.kt deleted file mode 100644 index 852539ce..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/configuration/HoconConfigurationReaderTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.github.rushyverse.api.configuration - -import com.github.rushyverse.api.AbstractTest -import com.github.rushyverse.api.TestConfiguration -import com.typesafe.config.ConfigFactory -import kotlinx.serialization.hocon.decodeFromConfig -import net.minestom.server.coordinate.Pos -import org.junit.jupiter.api.Nested -import java.io.File -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class HoconConfigurationReaderTest : AbstractTest() { - - private lateinit var file: File - - @BeforeTest - override fun onBefore() { - super.onBefore() - file = fileOfTmpDirectory("test.conf") - } - - @Nested - inner class HoconDefaultConfiguration { - - @Test - fun `should support Pos serializer`() { - val hocon = HoconConfigurationReader.hoconDefault - - val expected = Pos(1.0, 2.0, 3.0, 4f, 5f) - file.writeText( - """ - { - x: 1.0 - y: 2.0 - z: 3.0 - yaw: 4.0 - pitch: 5.0 - } - """.trimIndent() - ) - - val configFile = ConfigFactory.parseFile(file) - val result: Pos = hocon.decodeFromConfig(configFile) - - assertEquals(expected, result) - } - - } - - @Nested - inner class ReadConfigurationFileWithReifiedTypeParameter { - - @Test - fun `should read configuration file`() { - configurationToHoconFile(expectedDefaultConfiguration, file) - val configuration = HoconConfigurationReader() - val result: TestConfiguration = configuration.readConfigurationFile(file) - assertEquals(expectedDefaultConfiguration, result) - } - - } - - @Nested - inner class ReadConfigurationFileWithClassParameter { - - @Test - fun `should read configuration file`() { - configurationToHoconFile(expectedDefaultConfiguration, file) - val configuration = HoconConfigurationReader() - val result = configuration.readConfigurationFile(TestConfiguration::class, file) - assertEquals(expectedDefaultConfiguration, result) - } - - } - - @Nested - inner class ReadConfigurationFileWithSerializerParameter { - - @Test - fun `should read configuration file`() { - configurationToHoconFile(expectedDefaultConfiguration, file) - val configuration = HoconConfigurationReader() - val result = configuration.readConfigurationFile(TestConfiguration.serializer(), file) - assertEquals(expectedDefaultConfiguration, result) - } - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/configuration/IConfigurationReaderTest.kt b/src/test/kotlin/com/github/rushyverse/api/configuration/IConfigurationReaderTest.kt deleted file mode 100644 index 063ea48a..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/configuration/IConfigurationReaderTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.github.rushyverse.api.configuration - -import com.github.rushyverse.api.AbstractTest -import com.github.rushyverse.api.configuration.IConfigurationReader.Companion.getOrCreateConfigurationFile -import com.github.rushyverse.api.utils.randomString -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.assertThrows -import java.io.File -import java.io.FileNotFoundException -import java.util.* -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class IConfigurationReaderTest : AbstractTest() { - - @Test - fun `name of default configuration file is correct`() = runTest { - assertEquals("server.conf", IConfigurationReader.DEFAULT_CONFIG_FILE_NAME) - } - - @Nested - inner class GetOrCreateConfigurationFile { - - @Nested - inner class GetExistingConfigurationFile { - - @Test - fun `should return the given configuration file`() = runTest { - createConfigFileAndCheckIfFound(randomString()) { - getOrCreateConfigurationFile(it.absolutePath) - } - } - - @Test - fun `should return the default config file without edit it`() = runTest { - createConfigFileAndCheckIfFound(IConfigurationReader.DEFAULT_CONFIG_FILE_NAME) { - getOrCreateConfigurationFile() - } - } - - private inline fun createConfigFileAndCheckIfFound(fileName: String, block: (File) -> File) { - val configurationFile = fileOfTmpDirectory(fileName) - assertTrue { configurationFile.createNewFile() } - - val content = UUID.randomUUID().toString() - configurationFile.writeText(content) - - val file = block(configurationFile) - assertEquals(configurationFile, file) - assertEquals(content, file.readText()) - } - } - - @Nested - inner class GetNonExistingConfigurationFile { - - @Test - fun `should throw exception if file not found`() = runTest { - assertThrows { - getOrCreateConfigurationFile(getRandomFileInTmpDirectory().absolutePath) - } - } - - @Test - fun `should throw exception if file is not a regular file`() = runTest { - assertThrows { - getOrCreateConfigurationFile(tmpDirectory.absolutePath) - } - } - } - - @Nested - inner class CreateDefaultConfiguration { - @Test - fun `should create the config file if it's not found in the current directory`() = runTest { - val configurationFile = getOrCreateConfigurationFile() - assertTrue { configurationFile.isFile } - - val expectedConfigurationFile = fileOfTmpDirectory(IConfigurationReader.DEFAULT_CONFIG_FILE_NAME) - assertEquals(expectedConfigurationFile, configurationFile) - - inputStreamOfDefaultConfiguration().bufferedReader().use { - assertEquals(it.readText(), configurationFile.readText()) - } - } - } - - private fun getRandomFileInTmpDirectory() = fileOfTmpDirectory(randomString()) - } - - private fun inputStreamOfDefaultConfiguration() = - IConfiguration::class.java.classLoader.getResourceAsStream(IConfigurationReader.DEFAULT_CONFIG_FILE_NAME) - ?: error("Unable to find default configuration file in server resources") -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/configuration/VelocityConfigurationTest.kt b/src/test/kotlin/com/github/rushyverse/api/configuration/VelocityConfigurationTest.kt deleted file mode 100644 index 60c1092c..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/configuration/VelocityConfigurationTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.github.rushyverse.api.configuration - -import com.github.rushyverse.api.utils.randomString -import kotlinx.serialization.json.Json -import org.junit.jupiter.api.Nested -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import kotlin.test.assertEquals - -class VelocityConfigurationTest { - - @Nested - inner class Serialize { - - @ValueSource(booleans = [true, false]) - @ParameterizedTest - fun `with empty secret`(enabled: Boolean) { - val configuration = VelocityConfiguration(enabled, "") - val serialize = Json.encodeToString(VelocityConfiguration.serializer(), configuration) - assertEquals("{\"enabled\":$enabled,\"secret\":\"\"}", serialize) - } - - @ValueSource(booleans = [true, false]) - @ParameterizedTest - fun `with non empty secret`(enabled: Boolean) { - val secret = randomString() - val configuration = VelocityConfiguration(enabled, secret) - val serialize = Json.encodeToString(VelocityConfiguration.serializer(), configuration) - assertEquals("{\"enabled\":$enabled,\"secret\":\"$secret\"}", serialize) - } - - } - - @Nested - inner class Deserialize { - - @ValueSource(booleans = [true, false]) - @ParameterizedTest - fun `with empty secret`(enabled: Boolean) { - val string = "{\"enabled\":$enabled,\"secret\":\"\"}" - val deserialize = Json.decodeFromString(VelocityConfiguration.serializer(), string) - assertEquals(VelocityConfiguration(enabled, ""), deserialize) - } - - @ValueSource(booleans = [true, false]) - @ParameterizedTest - fun `with non empty secret`(enabled: Boolean) { - val secret = randomString() - val string = "{\"enabled\":$enabled,\"secret\":\"$secret\"}" - val deserialize = Json.decodeFromString(VelocityConfiguration.serializer(), string) - assertEquals(VelocityConfiguration(enabled, secret), deserialize) - } - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/configuration/reader/YamlFileReaderTest.kt b/src/test/kotlin/com/github/rushyverse/api/configuration/reader/YamlFileReaderTest.kt new file mode 100644 index 00000000..7b12f3f8 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/configuration/reader/YamlFileReaderTest.kt @@ -0,0 +1,138 @@ +package com.github.rushyverse.api.configuration.reader + +import com.charleskorn.kaml.Yaml +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import org.bukkit.plugin.java.JavaPlugin +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import java.io.File +import kotlin.test.BeforeTest +import kotlin.test.Test + +class YamlFileReaderTest { + + @Serializable + data class TestValue(val test: String) + + private lateinit var plugin: JavaPlugin + private lateinit var reader: YamlFileReader + + @TempDir + lateinit var tempDir: File + + @BeforeTest + fun onBefore() { + plugin = mockk() + reader = YamlFileReader(plugin, Yaml.default) + } + + @Nested + inner class ReadConfigurationFileFromClass { + + @Test + fun `should throw if class is not serializable`() { + class Test + shouldThrow { + reader.readConfigurationFile(Test::class, "test.yml") + } + } + + @ParameterizedTest + @CsvSource( + "fake_config.yml, withoutParent", + "configuration/fake_config.yml, withParent" + ) + fun `should create file and decode it`(configFile: String, value: String) { + shouldCreateFileAndDecode(configFile, value) { reader.readConfigurationFile(TestValue::class, it) } + } + + @Test + fun `should not create file if exists and read it`() { + shouldNotCreateFileIfExistsAndDecode { reader.readConfigurationFile(TestValue::class, it) } + } + } + + @Nested + inner class ReadConfigurationFileFromSerializer { + + @Test + fun `should throw if plugin folder cannot be created`() { + every { plugin.dataFolder } returns mockk { + every { exists() } returns false + every { mkdirs() } returns false + every { absolutePath } returns "test" + } + + val exception = shouldThrow { + reader.readConfigurationFile(mockk>(), "test.yml") + } + + exception.message shouldBe "Unable to get or create the plugin data folder test." + } + + @ParameterizedTest + @CsvSource( + "fake_config.yml, withoutParent", + "configuration/fake_config.yml, withParent" + ) + fun `should create file and decode it`(configFile: String, value: String) { + shouldCreateFileAndDecode(configFile, value) { reader.readConfigurationFile(TestValue.serializer(), it) } + } + + @ParameterizedTest + @CsvSource( + "fake_config.yml, withoutParent", + "configuration/fake_config.yml, withParent" + ) + fun `should create file and decode it using reified type`(configFile: String, value: String) { + shouldCreateFileAndDecode(configFile, value) { reader.readConfigurationFile(it) } + } + + @Test + fun `should not create file if exists and read it`() { + shouldNotCreateFileIfExistsAndDecode { reader.readConfigurationFile(TestValue.serializer(), it) } + } + } + + fun shouldCreateFileAndDecode( + configFile: String, + value: String, + load: (String) -> TestValue + ) { + every { plugin.dataFolder } returns tempDir + + val expectedValue = TestValue(value) + load(configFile) shouldBe expectedValue + + val file = File(tempDir, configFile) + file.exists() shouldBe true + file.readText() shouldBe "test: $value${System.lineSeparator()}" + } + + fun shouldNotCreateFileIfExistsAndDecode( + load: (String) -> TestValue + ) { + every { plugin.dataFolder } returns tempDir + + val value = "This is a custom value" + + val configFile = "fake_config.yml" + val file = File(tempDir, configFile) + val content = "test: $value" + file.writeText(content) + + val expectedValue = TestValue(value) + load(configFile) shouldBe expectedValue + + file.readText() shouldBe content + } + +} diff --git a/src/test/kotlin/com/github/rushyverse/api/coroutine/MinestomAsyncCoroutineDispatcherTest.kt b/src/test/kotlin/com/github/rushyverse/api/coroutine/MinestomAsyncCoroutineDispatcherTest.kt deleted file mode 100644 index 336ada5b..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/coroutine/MinestomAsyncCoroutineDispatcherTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.rushyverse.api.coroutine - -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import net.minestom.server.ServerProcess -import net.minestom.server.timer.ExecutionType -import net.minestom.server.timer.SchedulerManager -import kotlin.test.Test - -class MinestomAsyncCoroutineDispatcherTest { - - @Test - fun `should not perform if process is not alive`() { - val process = mockk() - every { process.isAlive } returns false - val schedulerManager = mockk() - every { process.scheduler() } returns schedulerManager - - val dispatcher = MinestomAsyncCoroutineDispatcher(process) - dispatcher.dispatch(mockk()) {} - - verify(exactly = 0) { schedulerManager.scheduleNextProcess(any(), any()) } - verify(exactly = 0) { schedulerManager.scheduleNextTick(any()) } - } - - @Test - fun `should perform task in async context`() { - val process = mockk() - every { process.isAlive } returns true - val schedulerManager = mockk() - every { process.scheduler() } returns schedulerManager - every { schedulerManager.scheduleNextProcess(any(), any()) } returns mockk() - - val dispatcher = MinestomAsyncCoroutineDispatcher(process) - - val runnable = mockk() - dispatcher.dispatch(mockk(), runnable) - - verify(exactly = 1) { schedulerManager.scheduleNextProcess(runnable, ExecutionType.ASYNC) } - verify(exactly = 0) { schedulerManager.scheduleNextTick(any()) } - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/coroutine/MinestomCoroutineDispatcherTest.kt b/src/test/kotlin/com/github/rushyverse/api/coroutine/MinestomCoroutineDispatcherTest.kt deleted file mode 100644 index f12c9c70..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/coroutine/MinestomCoroutineDispatcherTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.rushyverse.api.coroutine - -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import net.minestom.server.ServerProcess -import net.minestom.server.timer.ExecutionType -import net.minestom.server.timer.SchedulerManager -import kotlin.test.Test - -class MinestomCoroutineDispatcherTest { - - @Test - fun `should not perform if process is not alive`() { - val process = mockk() - every { process.isAlive } returns false - val schedulerManager = mockk() - every { process.scheduler() } returns schedulerManager - - val dispatcher = MinestomCoroutineDispatcher(process, mockk()) - dispatcher.dispatch(mockk()) {} - - verify(exactly = 0) { schedulerManager.scheduleNextProcess(any(), any()) } - verify(exactly = 0) { schedulerManager.scheduleNextTick(any()) } - } - - @Test - fun `should perform task in sync context`() { - shouldPerformTask(ExecutionType.SYNC) - } - - @Test - fun `should perform task in async context`() { - shouldPerformTask(ExecutionType.ASYNC) - } - - private fun shouldPerformTask(type: ExecutionType) { - val process = mockk() - every { process.isAlive } returns true - val schedulerManager = mockk() - every { process.scheduler() } returns schedulerManager - every { schedulerManager.scheduleNextProcess(any(), any()) } returns mockk() - - val dispatcher = MinestomCoroutineDispatcher(process, type) - - val runnable = mockk() - dispatcher.dispatch(mockk(), runnable) - - verify(exactly = 1) { schedulerManager.scheduleNextProcess(runnable, type) } - verify(exactly = 0) { schedulerManager.scheduleNextTick(any()) } - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/coroutine/MinestomSyncCoroutineDispatcherTest.kt b/src/test/kotlin/com/github/rushyverse/api/coroutine/MinestomSyncCoroutineDispatcherTest.kt deleted file mode 100644 index fb69a153..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/coroutine/MinestomSyncCoroutineDispatcherTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.rushyverse.api.coroutine - -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import net.minestom.server.ServerProcess -import net.minestom.server.timer.ExecutionType -import net.minestom.server.timer.SchedulerManager -import kotlin.test.Test - -class MinestomSyncCoroutineDispatcherTest { - - @Test - fun `should not perform if process is not alive`() { - val process = mockk() - every { process.isAlive } returns false - val schedulerManager = mockk() - every { process.scheduler() } returns schedulerManager - - val dispatcher = MinestomSyncCoroutineDispatcher(process) - dispatcher.dispatch(mockk()) {} - - verify(exactly = 0) { schedulerManager.scheduleNextProcess(any(), any()) } - verify(exactly = 0) { schedulerManager.scheduleNextTick(any()) } - } - - @Test - fun `should perform task in sync context`() { - val process = mockk() - every { process.isAlive } returns true - val schedulerManager = mockk() - every { process.scheduler() } returns schedulerManager - every { schedulerManager.scheduleNextProcess(any(), any()) } returns mockk() - - val dispatcher = MinestomSyncCoroutineDispatcher(process) - - val runnable = mockk() - dispatcher.dispatch(mockk(), runnable) - - verify(exactly = 1) { schedulerManager.scheduleNextProcess(runnable, ExecutionType.SYNC) } - verify(exactly = 0) { schedulerManager.scheduleNextTick(any()) } - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/delegate/DelegateWorldTest.kt b/src/test/kotlin/com/github/rushyverse/api/delegate/DelegateWorldTest.kt new file mode 100644 index 00000000..9ce52636 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/delegate/DelegateWorldTest.kt @@ -0,0 +1,46 @@ +package com.github.rushyverse.api.delegate + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.WorldMock +import org.bukkit.World +import java.util.* +import kotlin.test.* + +class DelegateWorldTest { + + private lateinit var world: WorldMock + + @BeforeTest + fun onBefore() { + world = WorldMock() + MockBukkit.mock().addWorld(world) + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Test + fun `get world if server has it`() { + val delegate = DelegateWorld(world.uid) + + val obj = object { + val property: World? by delegate + } + + assertEquals(world, obj.property) + } + + @Test + fun `get world if server doesn't have it`() { + val uuid = UUID.randomUUID() + val delegate = DelegateWorld(uuid) + + val obj = object { + val property: World? by delegate + } + + assertNull(obj.property) + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/delegate/PlayerWorldTest.kt b/src/test/kotlin/com/github/rushyverse/api/delegate/PlayerWorldTest.kt new file mode 100644 index 00000000..3fd0f653 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/delegate/PlayerWorldTest.kt @@ -0,0 +1,50 @@ +package com.github.rushyverse.api.delegate + +import be.seeseemelk.mockbukkit.MockBukkit +import org.bukkit.entity.Player +import org.junit.jupiter.api.Test +import java.util.* +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class PlayerWorldTest { + + private lateinit var player: Player + + @BeforeTest + fun onBefore() { + MockBukkit.mock().apply { + player = addPlayer() + } + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Test + fun `get player if server has it`() { + val delegate = DelegatePlayer(player.uniqueId) + + val obj = object { + val property by delegate + } + + assertEquals(player, obj.property) + } + + @Test + fun `get player if server doesn't have it`() { + val uuid = UUID.randomUUID() + val delegate = DelegatePlayer(uuid) + + val obj = object { + val property by delegate + } + + assertNull(obj.property) + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/entity/CommonNPCEntityTest.kt b/src/test/kotlin/com/github/rushyverse/api/entity/CommonNPCEntityTest.kt deleted file mode 100644 index 63207f48..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/entity/CommonNPCEntityTest.kt +++ /dev/null @@ -1,307 +0,0 @@ -package com.github.rushyverse.api.entity - -import com.github.rushyverse.api.extension.acquirable -import com.github.rushyverse.api.position.IAreaLocatable -import com.github.rushyverse.api.utils.randomString -import io.mockk.* -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.Player -import net.minestom.server.event.player.PlayerEntityInteractEvent -import net.minestom.server.thread.Acquirable -import net.minestom.server.thread.Acquired -import net.minestom.testing.Env -import net.minestom.testing.EnvTest -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals - -abstract class CommonNPCEntityTest { - - @Nested - inner class LookNearbyPlayer { - - @Test - fun `should throw exception if area is null`() { - val npc = createEntity(null) - assertThrows { npc.lookNearestPlayer() } - } - - @Test - fun `should keep the position if no player is in area`() { - val area = mockk> { - every { entitiesInArea } returns emptySet() - } - val npc = createEntity(area) - val expectedPos = npc.position - - npc.lookNearestPlayer() - assertEquals(expectedPos, npc.position) - } - - @Test - fun `should look at the player if there is one`() { - val player = mockk() - - val area = mockk> { - every { entitiesInArea } returns setOf(player) - } - val npc = createEntity(area) - - val slotPlayer = slot() - val npcSpy = spyk(npc) { - justRun { lookAt(capture(slotPlayer)) } - } - - npcSpy.lookNearestPlayer() - - verify(exactly = 1) { npcSpy.lookAt(any()) } - assertEquals(player, slotPlayer.captured) - } - - @Test - fun `should look at the nearest player in the list`() { - val player1 = mockPlayerWithAcquirableMocks() - every { player1.position } returns Pos(100.0, 0.0, 0.0) - val player2 = mockPlayerWithAcquirableMocks() - every { player2.position } returns Pos(50.0, 0.0, 0.0) - - val area = mockk> { - every { entitiesInArea } returns setOf(player1, player2) - } - val npc = createEntity(area) - - val slotPlayer = slot() - val npcSpy = spyk(npc) { - justRun { lookAt(capture(slotPlayer)) } - every { position } returns Pos.ZERO - } - - npcSpy.lookNearestPlayer() - - verify(exactly = 1) { npcSpy.lookAt(any()) } - assertEquals(player2, slotPlayer.captured) - } - - } - - private fun mockPlayerWithAcquirableMocks(): Player { - return mockk().also { - val acquired = mockk>() { - every { get() } returns it - justRun { unlock() } - } - val acquirable = mockk>() { - every { lock() } returns acquired - } - every { it.acquirable } returns acquirable - } - } - - @Nested - inner class OnEnterArea { - - @Test - fun `should do nothing`() { - val npc = createEntity(null) - val player = mockk() - npc.onEnterArea(player) - } - - } - - @Nested - inner class OnLeaveArea { - - @Test - fun `should do nothing`() { - val npc = createEntity(null) - val player = mockk() - npc.onLeaveArea(player) - } - - } - - @Nested - inner class OnInteract { - - @Test - fun `should do nothing`() { - val npc = createEntity(null) - val event = mockk() - npc.onInteract(event) - } - - } - - @Nested - @EnvTest - inner class Update { - - @Nested - inner class OnEnterArea { - - @Test - fun `should trigger enter area if new player is in area`(env: Env) { - val pos = Pos(0.0, 0.0, 0.0) - val flatInstance = env.createFlatInstance() - val player = mockk() - val area = mockk> { - justRun { position = any() } - justRun { instance = any() } - every { updateEntitiesInArea() } returns Pair(setOf(player), emptySet()) - } - val npc = createEntity(area) - npc.setInstance(flatInstance, pos) - val npcSpy = spyk(npc) { - justRun { onEnterArea(any()) } - } - npcSpy.update(0) - - verify(exactly = 1) { npcSpy.onEnterArea(player) } - } - - @Test - fun `should not trigger enter area if the player is always in the area`(env: Env) { - val pos = Pos(0.0, 0.0, 0.0) - val flatInstance = env.createFlatInstance() - val player = mockk() - val area = mockk> { - justRun { position = any() } - justRun { instance = any() } - every { updateEntitiesInArea() } returns Pair(setOf(player), emptySet()) - } - val npc = createEntity(area) - npc.setInstance(flatInstance, pos) - val npcSpy = spyk(npc) { - justRun { onEnterArea(any()) } - } - npcSpy.update(0) - - verify(exactly = 1) { npcSpy.onEnterArea(player) } - - every { area.updateEntitiesInArea() } returns Pair(emptySet(), emptySet()) - - npcSpy.update(1) - - verify(exactly = 1) { npcSpy.onEnterArea(player) } - verify(exactly = 0) { npcSpy.onLeaveArea(any()) } - } - - } - - @Nested - inner class OnLeaveArea { - - @Test - fun `should trigger leave area if player is not in area anymore`(env: Env) { - val pos = Pos(0.0, 0.0, 0.0) - val flatInstance = env.createFlatInstance() - val player = mockk() - val area = mockk> { - justRun { position = any() } - justRun { instance = any() } - every { updateEntitiesInArea() } returns Pair(emptySet(), setOf(player)) - } - - val npc = createEntity(area) - npc.setInstance(flatInstance, pos) - val npcSpy = spyk(npc) { - justRun { onLeaveArea(any()) } - } - npcSpy.update(0) - - verify(exactly = 1) { npcSpy.onLeaveArea(player) } - } - - @Test - fun `should not leave enter area if the player is always out of the area`(env: Env) { - val pos = Pos(0.0, 0.0, 0.0) - val flatInstance = env.createFlatInstance() - val player = mockk() - val area = mockk> { - justRun { position = any() } - justRun { instance = any() } - every { updateEntitiesInArea() } returns Pair(emptySet(), setOf(player)) - } - val npc = createEntity(area) - npc.setInstance(flatInstance, pos) - val npcSpy = spyk(npc) { - justRun { onLeaveArea(any()) } - } - npcSpy.update(0) - - verify(exactly = 1) { npcSpy.onLeaveArea(player) } - - every { area.updateEntitiesInArea() } returns Pair(emptySet(), emptySet()) - - npcSpy.update(1) - - verify(exactly = 1) { npcSpy.onLeaveArea(player) } - verify(exactly = 0) { npcSpy.onEnterArea(any()) } - } - - } - - @Test - fun `should update the area entities`(env: Env) { - val pos = Pos(0.0, 0.0, 0.0) - val flatInstance = env.createFlatInstance() - val area = mockk> { - justRun { position = any() } - justRun { instance = any() } - every { updateEntitiesInArea() } returns Pair(emptySet(), emptySet()) - } - val npc = createEntity(area) - npc.setInstance(flatInstance, pos) - npc.update(0) - - verify(exactly = 1) { area.instance = flatInstance } - verify(exactly = 1) { area.position = pos } - verify(exactly = 1) { area.updateEntitiesInArea() } - } - - @Test - fun `should trigger enter and leave area if players is in and out of area`(env: Env) { - val pos = Pos(0.0, 0.0, 0.0) - val flatInstance = env.createFlatInstance() - val player = mockk(randomString()) - val player2 = mockk(randomString()) - val area = mockk> { - justRun { position = any() } - justRun { instance = any() } - every { updateEntitiesInArea() } returns Pair(setOf(player), setOf(player2)) - } - val npc = createEntity(area) - npc.setInstance(flatInstance, pos) - val npcSpy = spyk(npc) { - justRun { onEnterArea(any()) } - justRun { onLeaveArea(any()) } - } - npcSpy.update(0) - - verify(exactly = 1) { npcSpy.onEnterArea(player) } - verify(exactly = 1) { npcSpy.onLeaveArea(player2) } - - every { area.updateEntitiesInArea() } returns Pair(setOf(player2), setOf(player)) - - npcSpy.update(1) - - verify(exactly = 1) { npcSpy.onEnterArea(player) } - verify(exactly = 1) { npcSpy.onEnterArea(player2) } - verify(exactly = 1) { npcSpy.onLeaveArea(player) } - verify(exactly = 1) { npcSpy.onLeaveArea(player2) } - - every { area.updateEntitiesInArea() } returns Pair(emptySet(), emptySet()) - - npcSpy.update(0) - - verify(exactly = 1) { npcSpy.onEnterArea(player) } - verify(exactly = 1) { npcSpy.onEnterArea(player2) } - verify(exactly = 1) { npcSpy.onLeaveArea(player) } - verify(exactly = 1) { npcSpy.onLeaveArea(player2) } - } - } - - protected abstract fun createEntity(area: IAreaLocatable?): NPCEntity -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/entity/NPCEntityTest.kt b/src/test/kotlin/com/github/rushyverse/api/entity/NPCEntityTest.kt deleted file mode 100644 index 78e712f4..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/entity/NPCEntityTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.rushyverse.api.entity - -import com.github.rushyverse.api.position.IAreaLocatable -import net.minestom.server.entity.EntityType -import net.minestom.server.entity.Player -import org.junit.jupiter.api.Nested -import kotlin.test.Test -import kotlin.test.assertNull - -class NPCEntityTest : CommonNPCEntityTest() { - - @Nested - inner class Instantiation { - - @Test - fun `should set area trigger as null if not defined`() { - val npc = NPCEntity(EntityType.CREEPER) - assertNull(npc.areaTrigger) - } - } - - override fun createEntity(area: IAreaLocatable?): NPCEntity { - return NPCEntity(EntityType.CREEPER, area) - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/entity/PlayerNPCEntityTest.kt b/src/test/kotlin/com/github/rushyverse/api/entity/PlayerNPCEntityTest.kt deleted file mode 100644 index bd6cf8c2..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/entity/PlayerNPCEntityTest.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.github.rushyverse.api.entity - -import com.github.rushyverse.api.extension.AddPlayerTextureProperty -import com.github.rushyverse.api.position.IAreaLocatable -import com.github.rushyverse.api.utils.randomPos -import com.github.rushyverse.api.utils.randomString -import net.kyori.adventure.text.Component -import net.minestom.server.entity.GameMode -import net.minestom.server.entity.Player -import net.minestom.server.network.packet.server.play.PlayerInfoPacket -import net.minestom.server.network.packet.server.play.PlayerInfoPacket.AddPlayer -import net.minestom.testing.Env -import net.minestom.testing.EnvTest -import org.junit.jupiter.api.Nested -import java.util.* -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNull - - -class PlayerNPCEntityTest : CommonNPCEntityTest() { - - @Nested - inner class Instantiation { - - @Test - fun `should set properties as empty list if not defined`() { - val npc = PlayerNPCEntity(randomString()) - assertEquals(emptyList(), npc.properties) - } - - @Test - fun `should set area trigger as null if not defined`() { - val npc = PlayerNPCEntity(randomString()) - assertNull(npc.areaTrigger) - } - - @Test - fun `should set inTabList as false if not defined`() { - val npc = PlayerNPCEntity(randomString()) - assertFalse { npc.inTabList } - } - } - - @Nested - @EnvTest - inner class UpdateNewViewer { - - @Nested - inner class AddPlayerPacket { - - @Test - fun `should send add player packet to new viewer with default values`(env: Env) { - assertPacketSent( - env, - UUID.randomUUID(), - randomString(), - emptyList(), - null - ) - } - - @Test - fun `should send add player packet to new viewer with textures property`(env: Env) { - assertPacketSent( - env, - UUID.randomUUID(), - randomString(), - listOf( - AddPlayerTextureProperty(randomString(), randomString()) - ), - null - ) - } - - @Test - fun `should send add player packet to new viewer with custom name`(env: Env) { - assertPacketSent( - env, - UUID.randomUUID(), - randomString(), - emptyList(), - Component.text(randomString()) - ) - } - - private fun assertPacketSent( - env: Env, - npcUUID: UUID, - npcName: String, - npcProperties: List, - npcCustomName: Component? - ) { - val instance = env.createFlatInstance() - val connection = env.createConnection() - val player = connection.connect(instance, randomPos()).join() - - val npc = PlayerNPCEntity(npcName, npcProperties, null, npcUUID, false) - npc.customName = npcCustomName - - val packetTracker = connection.trackIncoming(PlayerInfoPacket::class.java) - npc.updateNewViewer(player) - - packetTracker.assertSingle { - assertEquals( - PlayerInfoPacket( - PlayerInfoPacket.Action.ADD_PLAYER, - listOf( - AddPlayer( - npcUUID, - npcName, - npcProperties, - GameMode.CREATIVE, - 0, - npcCustomName ?: Component.text(npcName), - null - ) - ) - ), it - ) - } - } - - } - - @Nested - inner class RemovePlayerPacket { - - @Test - fun `should send remove packet to new viewer`(env: Env) { - assertPacketSent( - env, - UUID.randomUUID(), - false - ) - } - - @Test - fun `should not send remove packet to new viewer`(env: Env) { - assertPacketSent( - env, - UUID.randomUUID(), - true - ) - } - - private fun assertPacketSent( - env: Env, - npcUUID: UUID, - inTabList: Boolean - ) { - val instance = env.createFlatInstance() - val connection = env.createConnection() - val player = connection.connect(instance, randomPos()).join() - - val npc = PlayerNPCEntity(randomString(), inTabList = inTabList, uuid = npcUUID) - - npc.updateNewViewer(player) - - val packetTracker = connection.trackIncoming(PlayerInfoPacket::class.java) - npc.scheduler().processTick() - - if (inTabList) { - packetTracker.assertEmpty() - } else { - packetTracker.assertSingle { - assertEquals( - PlayerInfoPacket( - PlayerInfoPacket.Action.REMOVE_PLAYER, - listOf(PlayerInfoPacket.RemovePlayer(npcUUID)) - ), it - ) - } - } - } - - } - - } - - override fun createEntity(area: IAreaLocatable?): NPCEntity { - return PlayerNPCEntity(randomString(), areaTrigger = area) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/AcquirableExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/AcquirableExtTest.kt deleted file mode 100644 index 9e550967..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/extension/AcquirableExtTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.github.rushyverse.api.extension - -import io.mockk.every -import io.mockk.mockk -import io.mockk.spyk -import net.minestom.server.entity.Entity -import net.minestom.server.entity.Player -import net.minestom.server.thread.Acquirable -import org.junit.jupiter.api.Nested -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertTrue - -class AcquirableExtTest { - - @Test - fun `should get the acquirable of the entity`() { - val entity = mockk() - val acquirable = mockk>() - every { entity.getAcquirable() } returns acquirable - assertTrue { entity.acquirable == acquirable } - } - - @Nested - inner class IterableToAcquirables { - - @Test - fun `empty to AcquirableCollection`() { - val iterable = emptyList() - val acquirableCollection = iterable.toAcquirables() - assertTrue(acquirableCollection.unwrap().toList().isEmpty()) - } - - - @ParameterizedTest - @ValueSource(ints = [1, 5, 10]) - fun `not empty to AcquirableCollection`(numberOfEntities: Int) { - val list = createListOfEntities(numberOfEntities) - val iterable = list.asIterable() - val acquirableCollection = iterable.toAcquirables() - assertContentEquals(iterable, acquirableCollection.unwrap().toList()) - } - } - - @Nested - inner class ArrayToAcquirables { - - @Test - fun `empty to AcquirableCollection`() { - val array = emptyArray() - val acquirableCollection = array.toAcquirables() - assertTrue(acquirableCollection.unwrap().toList().isEmpty()) - } - - - @ParameterizedTest - @ValueSource(ints = [1, 5, 10]) - fun `not empty to AcquirableCollection`(numberOfEntities: Int) { - val array = createListOfEntities(numberOfEntities).toTypedArray() - val acquirableCollection = array.toAcquirables() - assertContentEquals(array.toList(), acquirableCollection.unwrap().toList()) - } - } - - @Nested - inner class SequenceToAcquirables { - - @Test - fun `empty to AcquirableCollection`() { - val array = emptySequence() - val acquirableCollection = array.toAcquirables() - assertTrue(acquirableCollection.unwrap().toList().isEmpty()) - } - - - @ParameterizedTest - @ValueSource(ints = [1, 5, 10]) - fun `not empty to AcquirableCollection`(numberOfEntities: Int) { - val sequence = createListOfEntities(numberOfEntities).asSequence() - val acquirableCollection = sequence.toAcquirables() - assertContentEquals(sequence.toList(), acquirableCollection.unwrap().toList()) - } - } - - private fun createListOfEntities(numberOfEntities: Int) = List(numberOfEntities) { - val entity = mockk() - val acquirable = spyk>(Acquirable.of(entity)) { - every { unwrap() } returns entity - } - every { entity.acquirable } returns acquirable - entity - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/CommandExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/CommandExtTest.kt deleted file mode 100644 index 1a37efb7..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/extension/CommandExtTest.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.github.rushyverse.api.extension - -import com.github.rushyverse.api.utils.assertCoroutineContextFromScope -import com.github.rushyverse.api.utils.randomString -import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.yield -import net.minestom.server.command.CommandSender -import net.minestom.server.command.builder.Command -import net.minestom.server.command.builder.CommandContext -import net.minestom.server.command.builder.arguments.ArgumentType -import org.junit.jupiter.api.Test -import java.util.concurrent.CountDownLatch -import kotlin.coroutines.coroutineContext -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue - -class CommandExtTest { - - @Test - fun `should use dispatcher to process set default executor suspend`() { - val command = Command("test") - var executed = false - - val senderMock = mockk() - val contextMock = mockk() - - val scope = CoroutineScope(Dispatchers.Default) - - val currentThread = Thread.currentThread() - val latch = CountDownLatch(1) - - command.setDefaultExecutorSuspend(scope) { sender, context -> - assertCoroutineContextFromScope(scope, coroutineContext) - assertEquals(senderMock, sender) - assertEquals(contextMock, context) - executed = true - - assertEquals(currentThread, Thread.currentThread()) - yield() - assertNotEquals(currentThread, Thread.currentThread()) - latch.countDown() - } - command.defaultExecutor?.apply(senderMock, contextMock) - latch.await() - assertTrue(executed) - } - - @Test - fun `should use dispatcher to process add syntax suspend`() { - val command = Command("test") - val stringArg = ArgumentType.String("string") - val intArg = ArgumentType.Integer("int") - var executed = false - - val senderMock = mockk() - val contextMock = mockk() - val scope = CoroutineScope(Dispatchers.Default) - - val currentThread = Thread.currentThread() - val latch = CountDownLatch(1) - - command.addSyntaxSuspend({ sender, context -> - assertCoroutineContextFromScope(scope, coroutineContext) - assertEquals(senderMock, sender) - assertEquals(contextMock, context) - executed = true - - assertEquals(currentThread, Thread.currentThread()) - yield() - assertNotEquals(currentThread, Thread.currentThread()) - latch.countDown() - }, stringArg, intArg, coroutineScope = scope) - - command.syntaxes.first().executor.apply(senderMock, contextMock) - latch.await() - assertTrue(executed) - } - - @Test - fun `should use dispatcher to process add conditional syntax suspend`() { - val command = Command("test") - val stringArg = ArgumentType.String("string") - val intArg = ArgumentType.Integer("int") - var executed = false - var conditionalExecuted = false - - val senderMock = mockk() - val contextMock = mockk() - val scope = CoroutineScope(Dispatchers.Default) - - val currentThread = Thread.currentThread() - val latch = CountDownLatch(1) - - val expectedCommandString = randomString() - command.addConditionalSyntaxSuspend( - { sender, commandString -> - assertEquals(senderMock, sender) - assertEquals(expectedCommandString, commandString) - conditionalExecuted = true - true - }, - { sender, context -> - assertCoroutineContextFromScope(scope, coroutineContext) - assertEquals(senderMock, sender) - assertEquals(contextMock, context) - executed = true - - assertEquals(currentThread, Thread.currentThread()) - yield() - assertNotEquals(currentThread, Thread.currentThread()) - latch.countDown() - - }, stringArg, intArg, coroutineScope = scope - ) - - val syntax = command.syntaxes.first() - syntax.commandCondition!!.canUse(senderMock, expectedCommandString) - assertFalse(executed) - assertTrue(conditionalExecuted) - - syntax.executor.apply(senderMock, contextMock) - latch.await() - assertTrue(executed) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/CommandSenderExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/CommandSenderExtTest.kt new file mode 100644 index 00000000..2da98ad2 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/CommandSenderExtTest.kt @@ -0,0 +1,32 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.utils.randomString +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.slot +import net.kyori.adventure.text.TextComponent +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.command.CommandSender +import org.junit.jupiter.api.Nested +import kotlin.test.Test +import kotlin.test.assertEquals + +class CommandSenderExtTest { + + @Nested + inner class Message { + + @Test + fun `send error message`() { + val sender = mockk() + val slotComponent = slot() + justRun { sender.sendMessage(capture(slotComponent)) } + + val content = randomString() + sender.sendMessageError(content) + val component = slotComponent.captured + assertEquals(NamedTextColor.RED, component.color()) + assertEquals(content, component.content()) + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/MathExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/ComparableExtTest.kt similarity index 96% rename from src/test/kotlin/com/github/rushyverse/api/extension/MathExtTest.kt rename to src/test/kotlin/com/github/rushyverse/api/extension/ComparableExtTest.kt index 06f9aa3b..eec547eb 100644 --- a/src/test/kotlin/com/github/rushyverse/api/extension/MathExtTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/extension/ComparableExtTest.kt @@ -5,11 +5,11 @@ import org.junit.jupiter.api.Nested import kotlin.test.Test import kotlin.test.assertEquals -class MathExtTest { +class ComparableExtTest { @Nested @DisplayName("Get min and max") - inner class MinMaxOf { + inner class MinMax { @Test fun `a is inferior to b`() { @@ -50,4 +50,4 @@ class MathExtTest { assertEquals(expectedB, b2) } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/ComponentExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/ComponentExtTest.kt deleted file mode 100644 index 66c5bbf2..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/extension/ComponentExtTest.kt +++ /dev/null @@ -1,297 +0,0 @@ -package com.github.rushyverse.api.extension - -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.event.ClickEvent -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.format.TextColor -import net.kyori.adventure.text.format.TextDecoration -import org.junit.jupiter.api.Nested -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import kotlin.test.Test -import kotlin.test.assertEquals - -class ComponentExtTest { - - @Nested - inner class ChangeDecoration { - - @Nested - inner class Bold { - - @Test - fun `should add bold`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.BOLD, TextDecoration.State.TRUE), - Component.text("Hello").withBold() - ) - } - - @Test - fun `should remove bold`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.BOLD, TextDecoration.State.FALSE), - Component.text("Hello").withoutBold() - ) - } - - @Test - fun `should undefine bold`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.BOLD, TextDecoration.State.NOT_SET), - Component.text("Hello").undefineBold() - ) - } - - } - - @Nested - inner class Italic { - - @Test - fun `should add italic`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.ITALIC, TextDecoration.State.TRUE), - Component.text("Hello").withItalic() - ) - } - - @Test - fun `should remove italic`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.ITALIC, TextDecoration.State.FALSE), - Component.text("Hello").withoutItalic() - ) - } - - @Test - fun `should undefine italic`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.ITALIC, TextDecoration.State.NOT_SET), - Component.text("Hello").undefineItalic() - ) - } - - } - - @Nested - inner class Underlined { - - @Test - fun `should add underlined`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.UNDERLINED, TextDecoration.State.TRUE), - Component.text("Hello").withUnderlined() - ) - } - - @Test - fun `should remove underlined`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.UNDERLINED, TextDecoration.State.FALSE), - Component.text("Hello").withoutUnderlined() - ) - } - - @Test - fun `should undefine underlined`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.UNDERLINED, TextDecoration.State.NOT_SET), - Component.text("Hello").undefineUnderlined() - ) - } - - } - - @Nested - inner class Strikethrough { - - @Test - fun `should add strikethrough`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.STRIKETHROUGH, TextDecoration.State.TRUE), - Component.text("Hello").withStrikethrough() - ) - } - - @Test - fun `should remove strikethrough`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.STRIKETHROUGH, TextDecoration.State.FALSE), - Component.text("Hello").withoutStrikethrough() - ) - } - - @Test - fun `should undefine strikethrough`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.STRIKETHROUGH, TextDecoration.State.NOT_SET), - Component.text("Hello").undefineStrikethrough() - ) - } - - } - - @Nested - inner class Obfuscated { - - @Test - fun `should add obfuscated`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.OBFUSCATED, TextDecoration.State.TRUE), - Component.text("Hello").withObfuscated() - ) - } - - @Test - fun `should remove obfuscated`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.OBFUSCATED, TextDecoration.State.FALSE), - Component.text("Hello").withoutObfuscated() - ) - } - - @Test - fun `should undefine obfuscated`() { - assertEquals( - Component.text("Hello").decoration(TextDecoration.OBFUSCATED, TextDecoration.State.NOT_SET), - Component.text("Hello").undefineObfuscated() - ) - } - - } - - @Nested - inner class Decorations { - - @Test - fun `should add all decorations`() { - assertEquals( - Component.text("Hello") - .decoration(TextDecoration.BOLD, TextDecoration.State.TRUE) - .decoration(TextDecoration.ITALIC, TextDecoration.State.TRUE) - .decoration(TextDecoration.UNDERLINED, TextDecoration.State.TRUE) - .decoration(TextDecoration.STRIKETHROUGH, TextDecoration.State.TRUE) - .decoration(TextDecoration.OBFUSCATED, TextDecoration.State.TRUE), - Component.text("Hello").withDecorations() - ) - } - - @Test - fun `should remove all decorations`() { - assertEquals( - Component.text("Hello") - .decoration(TextDecoration.BOLD, TextDecoration.State.FALSE) - .decoration(TextDecoration.ITALIC, TextDecoration.State.FALSE) - .decoration(TextDecoration.UNDERLINED, TextDecoration.State.FALSE) - .decoration(TextDecoration.STRIKETHROUGH, TextDecoration.State.FALSE) - .decoration(TextDecoration.OBFUSCATED, TextDecoration.State.FALSE), - Component.text("Hello").withoutDecorations() - ) - } - - @Test - fun `should undefine all decorations`() { - assertEquals( - Component.text("Hello") - .decoration(TextDecoration.BOLD, TextDecoration.State.NOT_SET) - .decoration(TextDecoration.ITALIC, TextDecoration.State.NOT_SET) - .decoration(TextDecoration.UNDERLINED, TextDecoration.State.NOT_SET) - .decoration(TextDecoration.STRIKETHROUGH, TextDecoration.State.NOT_SET) - .decoration(TextDecoration.OBFUSCATED, TextDecoration.State.NOT_SET), - Component.text("Hello").undefineDecorations() - ) - } - - } - } - - @Nested - inner class StringToComponent { - - @Test - fun `should return empty component if empty string`() { - val component = "".toComponent() - assertEquals(Component.empty(), component) - } - - @ParameterizedTest - @ValueSource(strings = ["Hello", "b81f1Hello", "Hello https://www.youtube.com #3b81f1"]) - fun `should return component with text without extract color`(value: String) { - val component = value.toComponent(extractColors = false) - assertEquals(Component.text(value), component) - } - - @Test - fun `should separate text and url`() { - val component = - "Hello https://www.youtube.com b81f1".toComponent(extractUrls = true, extractColors = false) - val expected = Component.text("Hello ") - .append( - Component.text("https://www.youtube.com").clickEvent(ClickEvent.openUrl("https://www.youtube.com")) - ) - .append(Component.text(" b81f1")) - assertEquals(expected, component) - } - - @Test - fun `should return component with text with legacy color`() { - val component = "&cHello".toComponent() - assertEquals(Component.text("Hello").color(NamedTextColor.RED), component) - } - - @Test - fun `should return component with text with hex color`() { - val component = "b81f1Hello".toComponent() - assertEquals(Component.text("Hello").color(TextColor.fromHexString("#3b81f1")), component) - } - - @Test - fun `should return component with text if legacy colors is disabled`() { - val component = "&cHello".toComponent(extractColors = false) - assertEquals(Component.text("&cHello"), component) - } - - @Test - fun `should return component with text using another color character`() { - val component = "^#3b81f1Hello".toComponent(colorChar = '^') - assertEquals(Component.text("Hello").color(TextColor.fromHexString("#3b81f1")), component) - } - } - - @Nested - inner class ComponentToText { - - @Test - fun `should return empty string if empty component`() { - val text = Component.empty().toText() - assertEquals("", text) - } - - @Test - fun `should return text without legacy color`() { - val text = Component.text("Hello").toText() - assertEquals("Hello", text) - } - - @Test - fun `should return text with legacy color`() { - val text = Component.text("Hello").color(NamedTextColor.RED).toText() - assertEquals("§cHello", text) - } - - @Test - fun `should return text with hex color`() { - val text = Component.text("Hello").color(TextColor.fromHexString("#3b81f1")).toText() - assertEquals("§9Hello", text) - } - - @Test - fun `should return text with url`() { - val text = Component.text("Hello ").append( - Component.text("https://www.youtube.com").clickEvent(ClickEvent.openUrl("https://www.youtube.com")) - ).toText() - assertEquals("Hello https://www.youtube.com", text) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/CoroutineScopeExt.kt b/src/test/kotlin/com/github/rushyverse/api/extension/CoroutineScopeExt.kt new file mode 100644 index 00000000..f86ed772 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/CoroutineScopeExt.kt @@ -0,0 +1,46 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.schedule.SchedulerTask +import kotlinx.coroutines.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class CoroutineScopeExt { + + @Test + fun `create running scheduler with task`() { + val body: suspend SchedulerTask.Task.() -> Unit = {} + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val delay = 42.seconds + val scheduler = scope.scheduledTask(delay, body) + assertTrue { scheduler.running } + + assertEquals(delay, scheduler.delay) + assertEquals(1, scheduler.tasks.size) + + val task = scheduler.tasks.first() + assertEquals(body, task.body) + assertEquals(scheduler, task.parent) + + scope.coroutineContext.cancelChildren() + assertFalse { scheduler.running } + } + + @Test + fun `create scheduler`() { + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val delay = 10.seconds + val scheduler = scope.scheduler(delay) + assertFalse { scheduler.running } + assertEquals(delay, scheduler.delay) + assertEquals(0, scheduler.tasks.size) + + scheduler.start() + + scope.coroutineContext.cancelChildren() + assertFalse { scheduler.running } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/DurationExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/DurationExtTest.kt new file mode 100644 index 00000000..ea9bd37d --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/DurationExtTest.kt @@ -0,0 +1,847 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.APIPlugin +import com.github.rushyverse.api.time.FormatTime +import com.github.rushyverse.api.translation.ResourceBundleTranslator +import com.github.rushyverse.api.translation.SupportedLanguage +import com.github.rushyverse.api.translation.registerResourceBundleForSupportedLocales +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import java.util.* +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +class DurationExtTest { + + @Nested + @DisplayName("Int conversion") + inner class IntConversion { + + @Test + fun `positive returns the corresponding ticks`() { + assertEquals(Duration.ZERO, 0.ticks) + assertEquals(50.milliseconds, 1.ticks) + assertEquals(1.seconds, 20.ticks) + assertEquals(2.seconds, 40.ticks) + assertEquals(5.seconds, 100.ticks) + assertEquals(250.milliseconds, 5.ticks) + } + + @Test + fun `negative returns the corresponding ticks`() { + assertEquals((-50).milliseconds, (-1).ticks) + assertEquals((-1).seconds, (-20).ticks) + assertEquals((-2).seconds, (-40).ticks) + assertEquals((-5).seconds, (-100).ticks) + assertEquals((-250).milliseconds, (-5).ticks) + } + + @Test + fun `uInt returns the corresponding ticks`() { + assertEquals(Duration.ZERO, 0u.ticks) + assertEquals(50.milliseconds, 1u.ticks) + assertEquals(1.seconds, 20u.ticks) + assertEquals(2.seconds, 40u.ticks) + assertEquals(5.seconds, 100u.ticks) + assertEquals(250.milliseconds, 5u.ticks) + } + } + + @Nested + @DisplayName("Short conversion") + inner class ShortConversion { + + @Test + fun `positive returns the corresponding ticks`() { + assertEquals(Duration.ZERO, 0.toShort().ticks) + assertEquals(50.milliseconds, 1.toShort().ticks) + assertEquals(1.seconds, 20.toShort().ticks) + assertEquals(2.seconds, 40.toShort().ticks) + assertEquals(5.seconds, 100.toShort().ticks) + assertEquals(250.milliseconds, 5.toShort().ticks) + } + + @Test + fun `negative returns the corresponding ticks`() { + assertEquals((-50).milliseconds, (-1).toShort().ticks) + assertEquals((-1).seconds, (-20).toShort().ticks) + assertEquals((-2).seconds, (-40).toShort().ticks) + assertEquals((-5).seconds, (-100).toShort().ticks) + assertEquals((-250).milliseconds, (-5).toShort().ticks) + } + + @Test + fun `uShort returns the corresponding ticks`() { + assertEquals(Duration.ZERO, 0.toUShort().ticks) + assertEquals(50.milliseconds, 1.toUShort().ticks) + assertEquals(1.seconds, 20.toUShort().ticks) + assertEquals(2.seconds, 40.toUShort().ticks) + assertEquals(5.seconds, 100.toUShort().ticks) + assertEquals(250.milliseconds, 5.toUShort().ticks) + } + } + + @Nested + @DisplayName("Long conversion") + inner class LongConversion { + + @Test + fun `positive returns the corresponding ticks`() { + assertEquals(Duration.ZERO, 0.toLong().ticks) + assertEquals(50.milliseconds, 1.toLong().ticks) + assertEquals(1.seconds, 20.toLong().ticks) + assertEquals(2.seconds, 40.toLong().ticks) + assertEquals(5.seconds, 100.toLong().ticks) + assertEquals(250.milliseconds, 5.toLong().ticks) + } + + @Test + fun `negative returns the corresponding ticks`() { + assertEquals((-50).milliseconds, (-1).toLong().ticks) + assertEquals((-1).seconds, (-20).toLong().ticks) + assertEquals((-2).seconds, (-40).toLong().ticks) + assertEquals((-5).seconds, (-100).toLong().ticks) + assertEquals((-250).milliseconds, (-5).toLong().ticks) + } + + @Test + fun `uLong returns the corresponding ticks`() { + assertEquals(Duration.ZERO, 0.toULong().ticks) + assertEquals(50.milliseconds, 1.toULong().ticks) + assertEquals(1.seconds, 20.toULong().ticks) + assertEquals(2.seconds, 40.toULong().ticks) + assertEquals(5.seconds, 100.toULong().ticks) + assertEquals(250.milliseconds, 5.toULong().ticks) + } + } + + @Nested + inner class LongFormat { + + private lateinit var translator: ResourceBundleTranslator + + @BeforeTest + fun onBefore() { + translator = ResourceBundleTranslator(APIPlugin.BUNDLE_API) + translator.registerResourceBundleForSupportedLocales(APIPlugin.BUNDLE_API, ResourceBundle::getBundle) + } + + @Nested + inner class French { + + private val locale = SupportedLanguage.FRENCH.locale + + @ParameterizedTest + @ValueSource(ints = [0]) + fun `should return the correct format for 0 second`(time: Int) { + assertEquals( + "", time.seconds.format( + FormatTime.long(translator, locale) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [1]) + fun `should return the correct format for singular value second`(time: Int) { + assertEquals("0${time} seconde", time.seconds.format(FormatTime.long(translator, locale))) + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 7, 8, 9]) + fun `should return the correct format for plural value with single digit`(time: Int) { + assertEquals("0${time} secondes", time.seconds.format(FormatTime.long(translator, locale))) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should return the correct format for plural value with double digit`(time: Int) { + assertEquals("${time} secondes", time.seconds.format(FormatTime.long(translator, locale))) + } + + @ParameterizedTest + @ValueSource(ints = [1]) + fun `should return the correct format for singular value minute`(time: Int) { + assertEquals( + "0${time} minute 00 seconde", time.minutes.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 7, 8, 9]) + fun `should return the correct format for plural value with single digit minute`(time: Int) { + assertEquals( + "0${time} minutes 00 seconde", time.minutes.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should return the correct format for plural value with double digit minute`(time: Int) { + assertEquals( + "$time minutes 00 seconde", time.minutes.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [1]) + fun `should return the correct format for singular value hour`(time: Int) { + assertEquals( + "0${time} heure 00 minute 00 seconde", time.hours.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 7, 8, 9]) + fun `should return the correct format for plural value with single digit hour`(time: Int) { + assertEquals( + "0${time} heures 00 minute 00 seconde", + time.hours.format(FormatTime.long(translator, locale, acceptZero = true)) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should return the correct format for plural value with double digit hour`(time: Int) { + assertEquals( + "$time heures 00 minute 00 seconde", time.hours.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [1]) + fun `should return the correct format for singular value day`(time: Int) { + assertEquals( + "0${time} jour 00 heure 00 minute 00 seconde", + time.days.format(FormatTime.long(translator, locale, acceptZero = true)) + ) + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 7, 8, 9]) + fun `should return the correct format for plural value with single digit day`(time: Int) { + assertEquals( + "0${time} jours 00 heure 00 minute 00 seconde", + time.days.format(FormatTime.long(translator, locale, acceptZero = true)) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should return the correct format for plural value with double digit day`(time: Int) { + assertEquals( + "$time jours 00 heure 00 minute 00 seconde", + time.days.format(FormatTime.long(translator, locale, acceptZero = true)) + ) + } + + @Test + fun `should return the correct format for multiple values`() { + assertEquals( + "04 jours 01 heure 02 minutes 03 secondes", + (4.days + 1.hours + 2.minutes + 3.seconds).format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + } + + @Nested + inner class English { + + private val locale = SupportedLanguage.ENGLISH.locale + + @ParameterizedTest + @ValueSource(ints = [0]) + fun `should return the correct format for 0 second`(time: Int) { + assertEquals( + "", time.seconds.format( + FormatTime.long(translator, locale) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [1]) + fun `should return the correct format for singular value second`(time: Int) { + assertEquals("0${time} second", time.seconds.format(FormatTime.long(translator, locale))) + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 7, 8, 9]) + fun `should return the correct format for plural value with single digit`(time: Int) { + assertEquals( + "0${time} seconds", time.seconds.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should return the correct format for plural value with double digit`(time: Int) { + assertEquals( + "${time} seconds", time.seconds.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [1]) + fun `should return the correct format for singular value minute`(time: Int) { + assertEquals( + "0${time} minute 00 seconds", time.minutes.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 7, 8, 9]) + fun `should return the correct format for plural value with single digit minute`(time: Int) { + assertEquals( + "0${time} minutes 00 seconds", time.minutes.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should return the correct format for plural value with double digit minute`(time: Int) { + assertEquals( + "$time minutes 00 seconds", time.minutes.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [1]) + fun `should return the correct format for singular value hour`(time: Int) { + assertEquals( + "0${time} hour 00 minutes 00 seconds", time.hours.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 7, 8, 9]) + fun `should return the correct format for plural value with single digit hour`(time: Int) { + assertEquals( + "0${time} hours 00 minutes 00 seconds", time.hours.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should return the correct format for plural value with double digit hour`(time: Int) { + assertEquals( + "$time hours 00 minutes 00 seconds", time.hours.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [1]) + fun `should return the correct format for singular value day`(time: Int) { + assertEquals( + "0${time} day 00 hours 00 minutes 00 seconds", time.days.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 7, 8, 9]) + fun `should return the correct format for plural value with single digit day`(time: Int) { + assertEquals( + "0${time} days 00 hours 00 minutes 00 seconds", time.days.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should return the correct format for plural value with double digit day`(time: Int) { + assertEquals( + "$time days 00 hours 00 minutes 00 seconds", time.days.format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + + @Test + fun `should return the correct format for multiple values`() { + assertEquals( + "04 days 01 hour 02 minutes 03 seconds", + (4.days + 1.hours + 2.minutes + 3.seconds).format( + FormatTime.long(translator, locale, acceptZero = true) + ) + ) + } + } + } + + @Nested + inner class ShortFormat { + + private lateinit var translator: ResourceBundleTranslator + + @BeforeTest + fun onBefore() { + translator = ResourceBundleTranslator(APIPlugin.BUNDLE_API) + translator.registerResourceBundleForSupportedLocales(APIPlugin.BUNDLE_API, ResourceBundle::getBundle) + } + + @Test + fun `should throw an exception if the duration is negative`() { + assertThrows { + (-1).seconds.format( + FormatTime.short( + translator, + SupportedLanguage.CHINESE.locale + ) + ) + } + } + + @ParameterizedTest + @ValueSource(strings = ["", " ", "-"]) + fun `should use separator between the different parts`(separator: String) { + val time = (4.days + 1.hours + 2.minutes + 3.seconds).format( + FormatTime.short( + translator, + SupportedLanguage.ENGLISH.locale + ), + separator = separator + ) + assertEquals( + "04d${separator}01h${separator}02m${separator}03s", + time + ) + } + + @ParameterizedTest + @ValueSource(strings = ["∞", "inf"]) + fun `should return infinity if the duration is infinite`(infinity: String) { + assertEquals( + "${infinity}d ${infinity}h ${infinity}m ${infinity}s", + Duration.INFINITE.format( + FormatTime.short( + translator, + SupportedLanguage.ENGLISH.locale + ), + infiniteSymbol = infinity + ) + ) + } + + @Test + fun `should return the correct format for 0`() { + assertEquals( + "", Duration.ZERO.format( + FormatTime.short( + translator, + SupportedLanguage.ENGLISH.locale, + acceptZero = true + ) + ) + ) + } + + @Test + fun `should return the correct format for 1 minute`() { + assertEquals( + "01m 00s", 1.minutes.format( + FormatTime.short( + translator, + SupportedLanguage.ENGLISH.locale, + acceptZero = true + ) + ) + ) + } + + @Test + fun `should return the correct format for 1 hour`() { + assertEquals( + "01h 00m 00s", 1.hours.format( + FormatTime.short( + translator, + SupportedLanguage.ENGLISH.locale, + acceptZero = true + ) + ) + ) + } + + @Test + fun `should return the correct format for 1 hour 1 minute`() { + assertEquals( + "01h 01m 00s", (1.hours + 1.minutes).format( + FormatTime.short( + translator, + SupportedLanguage.ENGLISH.locale, + acceptZero = true + ) + ) + ) + } + + @Test + fun `should use selected language`() { + assertEquals( + "04天 12小时 38分 01秒", (4.days + 12.hours + 38.minutes + 1.seconds).format( + FormatTime.short( + translator, + SupportedLanguage.CHINESE.locale + ) + ) + ) + } + + } + + @Nested + inner class TimeFormat { + + private val format: FormatTime = FormatTime( + second = { it + "s" }, + minute = { it + "m" }, + hour = { it + "h" }, + day = { it + "d" }, + ) + + @Nested + inner class PrefixSingleDigitWithZero { + + private val localFormat = format.copy( + beginAtZero = true, + acceptZero = true, + prefixSingleDigitWithZero = true + ) + + @ParameterizedTest + @ValueSource(ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + fun `should prefix single digit for seconds`(time: Int) { + assertEquals( + "00d 00h 00m 0${time}s", time.seconds.format( + localFormat + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should not prefix double digit for seconds`(time: Int) { + assertEquals( + "00d 00h 00m ${time}s", time.seconds.format( + localFormat + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + fun `should prefix single digit for minutes`(time: Int) { + assertEquals( + "00d 00h 0${time}m 00s", time.minutes.format( + localFormat + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should not prefix double digit for minutes`(time: Int) { + assertEquals( + "00d 00h ${time}m 00s", time.minutes.format( + localFormat + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + fun `should prefix single digit for hours`(time: Int) { + assertEquals( + "00d 0${time}h 00m 00s", time.hours.format( + localFormat + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + fun `should not prefix double digit for hours`(time: Int) { + assertEquals( + "00d ${time}h 00m 00s", time.hours.format( + localFormat + ) + ) + } + + @ParameterizedTest + @ValueSource(ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + fun `should prefix single digit for days`(time: Int) { + assertEquals( + "0${time}d 00h 00m 00s", time.days.format( + localFormat + ) + ) + } + + @Test + fun `should prefix for all time`() { + assertEquals( + "01d 01h 01m 01s", (1.days + 1.hours + 1.minutes + 1.seconds).format( + localFormat + ) + ) + } + + } + + @Nested + inner class AcceptZero { + + private val localFormat = format.copy( + beginAtZero = false, + acceptZero = true, + prefixSingleDigitWithZero = false + ) + + @Test + fun `should display all time`() { + assertEquals( + "", Duration.ZERO.format( + localFormat + ) + ) + } + + @Test + fun `should not display day if there is only hour`() { + assertEquals( + "1h 0m 0s", 1.hours.format( + localFormat + ) + ) + } + + @Test + fun `should not display 0 value`() { + assertEquals( + "1h 1s", (1.hours + 1.seconds).format( + localFormat.copy( + beginAtZero = false, + acceptZero = false + ) + ) + ) + } + + @Test + fun `should not display day if day format is null`() { + assertEquals( + "24h 0m 0s", 1.days.format( + localFormat.copy( + day = null + ) + ) + ) + } + + } + + @Nested + inner class BeginAtZero { + + private val localFormat = format.copy( + beginAtZero = true, + acceptZero = true, + prefixSingleDigitWithZero = false + ) + + @Test + fun `should display all time`() { + assertEquals( + "0d 0h 0m 0s", Duration.ZERO.format( + localFormat + ) + ) + } + + @Test + fun `should not display 0 at begin if zero not accepted`() { + assertEquals( + "1h", 1.hours.format( + localFormat.copy( + acceptZero = false + ) + ) + ) + } + + @Test + fun `should display 0 day if there is only hour`() { + assertEquals( + "0d 1h 0m 0s", 1.hours.format( + localFormat + ) + ) + } + + @Test + fun `should not display day if format is null`() { + assertEquals( + "0h 0m 0s", Duration.ZERO.format( + localFormat.copy( + day = null + ) + ) + ) + } + + @Test + fun `should not display hour if format is null`() { + assertEquals( + "0d 0m 0s", Duration.ZERO.format( + localFormat.copy( + hour = null + ) + ) + ) + } + + @Test + fun `should not display minute if format is null`() { + assertEquals( + "0d 0h 0s", Duration.ZERO.format( + localFormat.copy( + minute = null + ) + ) + ) + } + + @Test + fun `should not display second if format is null`() { + assertEquals( + "0d 0h 0m", Duration.ZERO.format( + localFormat.copy( + second = null + ) + ) + ) + } + + } + + } + + @Nested + inner class FormatInfiniteTime { + + private val format: FormatTime = FormatTime( + second = { it + "s" }, + minute = { it + "m" }, + hour = { it + "h" }, + day = { it + "d" }, + ) + + @Test + fun `should display all time`() { + assertEquals( + "∞d ∞h ∞m ∞s", Duration.INFINITE.format( + format + ) + ) + } + + @Test + fun `should display seconds, minute and hour`() { + assertEquals( + "∞h ∞m ∞s", Duration.INFINITE.format( + format.copy(day = null) + ) + ) + } + + @Test + fun `should display seconds, minute`() { + assertEquals( + "∞m ∞s", Duration.INFINITE.format( + format.copy(day = null, hour = null) + ) + ) + } + + @Test + fun `should display seconds, hour`() { + assertEquals( + "∞h ∞s", Duration.INFINITE.format( + format.copy(day = null, minute = null) + ) + ) + } + + @Test + fun `should display seconds, day`() { + assertEquals( + "∞d ∞s", Duration.INFINITE.format( + format.copy(hour = null, minute = null) + ) + ) + } + + @Test + fun `should display seconds`() { + assertEquals( + "∞s", Duration.INFINITE.format( + format.copy(hour = null, minute = null, day = null) + ) + ) + } + + @ParameterizedTest + @ValueSource(strings = ["∞", "inf"]) + fun `should use separator`(infinity: String) { + assertEquals( + "${infinity}d ${infinity}h ${infinity}m ${infinity}s", + Duration.INFINITE.format( + format, + infiniteSymbol = infinity + ) + ) + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/EntityExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/EntityExtTest.kt deleted file mode 100644 index 5bd84b02..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/extension/EntityExtTest.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.github.rushyverse.api.extension - -import com.github.rushyverse.api.utils.assertCoroutineContextFromScope -import com.github.rushyverse.api.utils.randomString -import io.mockk.every -import io.mockk.justRun -import io.mockk.mockk -import io.mockk.verify -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.runTest -import net.minestom.server.entity.Entity -import net.minestom.server.entity.Player -import net.minestom.server.event.EventFilter -import net.minestom.server.event.EventListener -import net.minestom.server.event.EventNode -import net.minestom.server.event.entity.EntityAttackEvent -import net.minestom.server.event.entity.EntityDeathEvent -import net.minestom.server.event.item.ItemDropEvent -import net.minestom.server.event.trait.EntityEvent -import net.minestom.server.thread.Acquirable -import net.minestom.server.thread.Acquired -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.assertThrows -import java.util.concurrent.CountDownLatch -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class EntityExtTest { - - @Nested - inner class Sync { - - @Test - fun `should lock entity`() = runTest { - val player = mockk() - val acquired = mockk>() { - every { get() } returns player - justRun { unlock() } - } - val acquirable = mockk>() { - every { lock() } returns acquired - } - - every { player.acquirable } returns acquirable - - var executed = false - val expectedValue = randomString() - val returnedValue = player.sync { - verify(exactly = 1) { player.acquirable } - verify(exactly = 1) { acquired.get() } - verify(exactly = 1) { acquirable.lock() } - verify(exactly = 0) { acquired.unlock() } - executed = true - expectedValue - } - - assertTrue(executed) - assertEquals(expectedValue, returnedValue) - - verify(exactly = 1) { player.acquirable } - verify(exactly = 1) { acquired.get() } - verify(exactly = 1) { acquirable.lock() } - verify(exactly = 1) { acquired.unlock() } - } - - @Test - fun `should unlock entity despite exception`() = runTest { - val player = mockk() - val acquired = mockk>() { - every { get() } returns player - justRun { unlock() } - } - val acquirable = mockk>() { - every { lock() } returns acquired - } - - every { player.acquirable } returns acquirable - - val ex = assertThrows { - player.sync { - throw Exception("Test") - } - } - - assertTrue(ex.message == "Test") - - verify(exactly = 1) { player.acquirable } - verify(exactly = 1) { acquired.get() } - verify(exactly = 1) { acquirable.lock() } - verify(exactly = 1) { acquired.unlock() } - } - } - - @Nested - inner class Async { - - @Test - fun `should lock entity and execute in coroutine`() = runTest { - val player = mockk() - val acquired = mockk>() { - every { get() } returns player - justRun { unlock() } - } - val acquirable = mockk>() { - every { lock() } returns acquired - } - - every { player.acquirable } returns acquirable - val scope = CoroutineScope(Dispatchers.Default) - - val latch = CountDownLatch(1) - var executed = false - val expectedValue = randomString() - val deferred = player.async(scope) { - assertCoroutineContextFromScope(scope, coroutineContext) - verify(exactly = 1) { player.acquirable } - verify(exactly = 1) { acquired.get() } - verify(exactly = 1) { acquirable.lock() } - verify(exactly = 0) { acquired.unlock() } - - latch.await() - executed = true - expectedValue - } - - verify(exactly = 1) { player.acquirable } - verify(exactly = 1) { acquired.get() } - verify(exactly = 1) { acquirable.lock() } - verify(exactly = 0) { acquired.unlock() } - - assertFalse(executed) - latch.countDown() - assertEquals(expectedValue, deferred.await()) - assertTrue(executed) - - verify(exactly = 1) { player.acquirable } - verify(exactly = 1) { acquired.get() } - verify(exactly = 1) { acquirable.lock() } - verify(exactly = 1) { acquired.unlock() } - } - - @Test - fun `should unlock entity despite exception`() = runTest { - val player = mockk() - val acquired = mockk>() { - every { get() } returns player - justRun { unlock() } - } - val acquirable = mockk>() { - every { lock() } returns acquired - } - - every { player.acquirable } returns acquirable - val scope = CoroutineScope(Dispatchers.Default) - - val deferred = player.async(scope) { - throw Exception("Test") - } - - val ex = try { - deferred.await() - } catch (ex: Exception) { - ex - } - - assertTrue(ex is Exception) - assertTrue(ex.message == "Test") - - verify(exactly = 1) { player.acquirable } - verify(exactly = 1) { acquired.get() } - verify(exactly = 1) { acquirable.lock() } - verify(exactly = 1) { acquired.unlock() } - } - } - - @Nested - inner class OnEvent { - - @Test - fun `should register new event listener on the entity event node`() { - val node = mockk>() { - every { addListener(any()) } returns this - } - val entity = mockk() { - every { eventNode() } returns node - } - - val listener = entity.onEvent { - error("Should not be called") - } - - verify(exactly = 1) { entity.eventNode() } - verify(exactly = 1) { node.addListener(listener) } - } - - @Test - fun `should call body when event is fired`() { - val node = EventNode.type("test", EventFilter.ENTITY) - val entity = mockk() { - every { eventNode() } returns node - } - - val event = mockk() - var called = false - entity.onEvent { - called = true - assertEquals(event, it) - EventListener.Result.SUCCESS - } - - node.call(event) - assertTrue(called) - } - - @Test - fun `should not call body when another event type is fired`() { - val node = EventNode.type("test", EventFilter.ENTITY) - val entity = mockk() { - every { eventNode() } returns node - } - - val event = mockk() - var called = false - entity.onEvent { - called = true - EventListener.Result.SUCCESS - } - - node.call(event) - assertFalse(called) - } - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/EventExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/EventExtTest.kt new file mode 100644 index 00000000..2efdd377 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/EventExtTest.kt @@ -0,0 +1,51 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.extension.event.finalDamagedHealth +import io.mockk.every +import io.mockk.mockk +import org.bukkit.entity.Damageable +import org.bukkit.entity.Entity +import org.bukkit.event.entity.EntityDamageEvent +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class EventExtTest { + + @Nested + inner class EntityDamageEventTest { + + @Nested + @DisplayName("Get final health of damaged") + inner class FinalHealth { + + @Test + fun `null if the damaged is not damaged`() { + val entityNotDamaged = mockk() + val event = mockk() + every { event.entity } returns entityNotDamaged + assertNull(event.finalDamagedHealth()) + } + + @Test + fun `future health computed when entity is damaged`() { + val damaged = mockk() + val currentHealth = Random.nextDouble(Double.MIN_VALUE, Double.MAX_VALUE) + every { damaged.health } returns currentHealth + + val event = mockk() + every { event.entity } returns damaged + + val finalDamage = Random.nextDouble(Double.MIN_VALUE, Double.MAX_VALUE) + every { event.finalDamage } returns finalDamage + assertEquals(currentHealth - finalDamage, event.finalDamagedHealth()) + } + } + + } + + +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/InventoryExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/InventoryExtTest.kt deleted file mode 100644 index 38060667..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/extension/InventoryExtTest.kt +++ /dev/null @@ -1,1210 +0,0 @@ -package com.github.rushyverse.api.extension - -import com.github.rushyverse.api.item.ItemComparator -import com.github.rushyverse.api.utils.assertCoroutineContextFromScope -import com.github.rushyverse.api.utils.randomPos -import com.github.rushyverse.api.utils.randomString -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.yield -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.format.TextDecoration -import net.minestom.server.inventory.AbstractInventory -import net.minestom.server.inventory.Inventory -import net.minestom.server.inventory.InventoryType -import net.minestom.server.inventory.click.ClickType -import net.minestom.server.inventory.condition.InventoryCondition -import net.minestom.server.item.ItemStack -import net.minestom.server.item.Material -import net.minestom.testing.Env -import net.minestom.testing.EnvTest -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Timeout -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.coroutines.coroutineContext -import kotlin.test.* - -@Timeout(5, unit = TimeUnit.SECONDS) -class InventoryExtTest { - - @Test - fun `slots property should return the range of slots`() { - val inventory: AbstractInventory = Inventory(InventoryType.CHEST_1_ROW, "Test") - assertEquals(0..8, inventory.slots) - } - - @Nested - inner class RemoveCondition { - - @Test - fun `should remove the condition of interaction with the inventory`() { - val inventory: AbstractInventory = Inventory(InventoryType.CHEST_1_ROW, "Test") - val condition = InventoryCondition { _, _, _, _ -> } - inventory.addInventoryCondition(condition) - assertTrue(inventory.removeCondition(condition)) - } - - @Test - fun `should return false if the condition is not present`() { - val inventory: AbstractInventory = Inventory(InventoryType.CHEST_1_ROW, "Test") - val condition = InventoryCondition { _, _, _, _ -> } - assertFalse(inventory.removeCondition(condition)) - } - - } - - @EnvTest - @Nested - inner class LockItemPosition { - - @Test - fun `should lock the item position`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - val condition = inventory.lockItemPositions() - inventory.setItemStack(0, ItemStack.of(Material.DIAMOND)) - - inventory.slots.forEach { - assertFalse(inventory.leftClick(player, it)) - assertFalse(inventory.rightClick(player, it)) - } - - player.inventory.removeCondition(condition) - - inventory.slots.forEach { - assertTrue(inventory.leftClick(player, it)) - assertTrue(inventory.rightClick(player, it)) - } - } - } - - @EnvTest - @Nested - inner class AddInventoryConditionSuspend { - - @Test - fun `should register a suspendable condition`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - var count = 0 - inventory.addInventoryConditionSuspend { playerClicker, clickedSlot, type, _ -> - assertEquals(player, playerClicker) - assertEquals(0, clickedSlot) - if (count == 0) { - assertEquals(ClickType.LEFT_CLICK, type) - } else { - assertEquals(ClickType.RIGHT_CLICK, type) - } - count++ - } - - assertEquals(1, inventory.inventoryConditions.size) - - inventory.setItemStack(0, ItemStack.of(Material.DIAMOND)) - - val playerSlot = 36 - assertEquals(0, count) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - assertTrue(inventory.rightClick(player, playerSlot)) - assertEquals(2, count) - } - - @Test - fun `should stay in current thread before suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - var isCalled = false - val thread = Thread.currentThread().id - inventory.addInventoryConditionSuspend(CoroutineScope(Dispatchers.Default)) { _, _, _, _ -> - assertEquals(thread, Thread.currentThread().id) - isCalled = true - } - - assertTrue(inventory.leftClick(player, 0)) - assertTrue(isCalled) - } - - @Test - fun `should change thread context after suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val latch = CountDownLatch(1) - val coroutineScope = CoroutineScope(Dispatchers.Default) - - inventory.addInventoryConditionSuspend(coroutineScope) { _, _, _, _ -> - yield() - assertCoroutineContextFromScope(coroutineScope, coroutineContext) - latch.countDown() - } - - assertTrue(inventory.leftClick(player, 0)) - latch.await() - } - } - - @EnvTest - @Nested - inner class RegisterClickEventOnSlotSuspend { - - @Test - fun `should register a click event on a slot`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val slot = 0 - var count = 0 - inventory.registerClickEventOnSlotSuspend(slot) { _, _, _, _ -> - count++ - } - assertEquals(1, inventory.inventoryConditions.size) - - inventory.setItemStack(slot, ItemStack.of(Material.DIAMOND)) - - val playerSlot = 36 - assertEquals(0, count) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - assertTrue(inventory.rightClick(player, playerSlot)) - assertEquals(2, count) - - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - @Test - fun `should stay in current thread before suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - var isCalled = false - val thread = Thread.currentThread().id - inventory.registerClickEventOnSlotSuspend(0, CoroutineScope(Dispatchers.Default)) { _, _, _, _ -> - assertEquals(thread, Thread.currentThread().id) - isCalled = true - } - - assertTrue(inventory.leftClick(player, 36)) - assertTrue(isCalled) - } - - @Test - fun `should change thread context after suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val latch = CountDownLatch(1) - val coroutineScope = CoroutineScope(Dispatchers.Default) - - inventory.registerClickEventOnSlotSuspend(0, coroutineScope) { _, _, _, _ -> - yield() - assertCoroutineContextFromScope(coroutineScope, coroutineContext) - latch.countDown() - } - - assertTrue(inventory.leftClick(player, 36)) - latch.await() - } - - } - - @EnvTest - @Nested - inner class RegisterClickEventOnSlot { - - @Test - fun `should register a click event on a slot`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val slot = 0 - var count = 0 - inventory.registerClickEventOnSlot(slot) { _, _, _, _ -> - count++ - } - assertEquals(1, inventory.inventoryConditions.size) - - inventory.setItemStack(slot, ItemStack.of(Material.DIAMOND)) - - val playerSlot = 36 - assertEquals(0, count) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - assertTrue(inventory.rightClick(player, playerSlot)) - assertEquals(2, count) - - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - } - - @EnvTest - @Nested - inner class RegisterClickEventOnItemSuspend { - - @Test - fun `default identifier should be equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - var count = 0 - inventory.registerClickEventOnItemSuspend( - item, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> - count++ - } - - val playerSlot = 36 - inventory.setItemStack(0, item) - assertEquals(0, count) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(0, item.withAmount(2)) - assertTrue(inventory.rightClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(1, ItemStack.of(Material.DIAMOND)) - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - var count = 0 - inventory.registerClickEventOnItemSuspend( - item, - ItemComparator.EQUALS, - CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> - count++ - } - assertEquals(1, inventory.inventoryConditions.size) - - inventory.setItemStack(0, item) - inventory.setItemStack(1, item) - inventory.setItemStack(3, item) - - assertEquals(0, count) - - assertTrue(inventory.leftClick(player, 36)) // remove the item - assertEquals(1, count) - - assertTrue(inventory.rightClick(player, 37)) - assertEquals(2, count) - - assertTrue(inventory.leftClick(player, 38)) - assertEquals(2, count) - - assertTrue(inventory.leftClick(player, 39)) - assertEquals(3, count) - } - - @Test - fun `should trigger the click handler when the item is similar`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - var clicked = false - inventory.registerClickEventOnItemSuspend( - item, - ItemComparator.SIMILAR, - CoroutineScope(Dispatchers.Default) - ) { playerClicked, slot, type, _ -> - assertEquals(player, playerClicked) - assertEquals(0, slot) - assertEquals(ClickType.LEFT_CLICK, type) - clicked = true - } - - val item2 = item.withAmount(20) - inventory.setItemStack(0, item2) - assertTrue(inventory.leftClick(player, 36)) - assertTrue(clicked) - } - - @Test - fun `should stay in current thread before suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - var isCalled = false - val thread = Thread.currentThread().id - val item = ItemStack.of(Material.DIAMOND) - inventory.registerClickEventOnItemSuspend( - item, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> - assertEquals(thread, Thread.currentThread().id) - isCalled = true - } - - inventory.setItemStack(0, item) - assertTrue(inventory.leftClick(player, 36)) - assertTrue(isCalled) - } - - @Test - fun `should change thread context after suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val latch = CountDownLatch(1) - val coroutineScope = CoroutineScope(Dispatchers.Default) - - val item = ItemStack.of(Material.DIAMOND) - inventory.registerClickEventOnItemSuspend(item, coroutineScope = coroutineScope) { _, _, _, _ -> - yield() - assertCoroutineContextFromScope(coroutineScope, coroutineContext) - latch.countDown() - } - - inventory.setItemStack(0, item) - assertTrue(inventory.leftClick(player, 36)) - latch.await() - } - } - - @EnvTest - @Nested - inner class RegisterClickEventOnItem { - - @Test - fun `default identifier should be equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - var count = 0 - inventory.registerClickEventOnItem(item) { _, _, _, _ -> - count++ - } - - val playerSlot = 36 - inventory.setItemStack(0, item) - assertEquals(0, count) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(0, item.withAmount(2)) - assertTrue(inventory.rightClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(1, ItemStack.of(Material.DIAMOND)) - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - var count = 0 - inventory.registerClickEventOnItem(item, ItemComparator.EQUALS) { _, _, _, _ -> - count++ - } - assertEquals(1, inventory.inventoryConditions.size) - - inventory.setItemStack(0, item) - inventory.setItemStack(1, item) - inventory.setItemStack(3, item) - - assertEquals(0, count) - - assertTrue(inventory.leftClick(player, 36)) // remove the item - assertEquals(1, count) - - assertTrue(inventory.rightClick(player, 37)) - assertEquals(2, count) - - assertTrue(inventory.leftClick(player, 38)) - assertEquals(2, count) - - assertTrue(inventory.leftClick(player, 39)) - assertEquals(3, count) - } - - @Test - fun `should trigger the click handler when the item is similar`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - var clicked = false - inventory.registerClickEventOnItem(item, ItemComparator.SIMILAR) { playerClicked, slot, type, _ -> - assertEquals(player, playerClicked) - assertEquals(0, slot) - assertEquals(ClickType.LEFT_CLICK, type) - clicked = true - } - - val item2 = item.withAmount(20) - inventory.setItemStack(0, item2) - assertTrue(inventory.leftClick(player, 36)) - assertTrue(clicked) - } - } - - @EnvTest - @Nested - inner class SetItemStackSuspendWithClickHandler { - - @Test - fun `default identifier should be equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - val slot = 0 - var count = 0 - inventory.setItemStackSuspend( - slot, - item, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> - count++ - } - - val playerSlot = 36 - assertEquals(item, inventory.getItemStack(slot)) - assertEquals(1, inventory.itemStacks.filterNot { it.isAir }.size) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(slot, item.withAmount(2)) - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(slot + 1, ItemStack.of(Material.DIAMOND)) - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - val slot = 0 - var count = 0 - inventory.setItemStackSuspend( - slot, - item, - ItemComparator.EQUALS, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> - count++ - } - - val playerSlot = 36 - assertEquals(1, inventory.inventoryConditions.size) - assertEquals(item, inventory.getItemStack(slot)) - assertEquals(1, inventory.itemStacks.filterNot { it.isAir }.size) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(slot + 1, item) - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is similar`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - val slot = 0 - var clicked = false - inventory.setItemStackSuspend( - slot, - item, - ItemComparator.SIMILAR, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { playerClicked, clickedSlot, type, _ -> - assertEquals(player, playerClicked) - assertEquals(slot, clickedSlot) - assertEquals(ClickType.LEFT_CLICK, type) - clicked = true - } - - val item2 = item.withAmount(10) - inventory.setItemStack(slot, item2) - - assertTrue(inventory.leftClick(player, 36)) - assertTrue(clicked) - } - - @Test - fun `should stay in current thread before suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - val slot = 0 - var clicked = false - val thread = Thread.currentThread().id - inventory.setItemStackSuspend( - slot, - item, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> - assertEquals(thread, Thread.currentThread().id) - clicked = true - } - - assertTrue(inventory.leftClick(player, 36)) - assertTrue(clicked) - } - - @Test - fun `should change thread context after suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - val slot = 0 - - val latch = CountDownLatch(1) - val coroutineScope = CoroutineScope(Dispatchers.Default) - - inventory.setItemStackSuspend(slot, item, coroutineScope = coroutineScope) { _, _, _, _ -> - yield() - assertCoroutineContextFromScope(coroutineScope, coroutineContext) - latch.countDown() - } - - assertTrue(inventory.leftClick(player, 36)) - latch.await() - } - } - - @EnvTest - @Nested - inner class SetItemStackWithClickHandler { - - @Test - fun `default identifier should be equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - val slot = 0 - var count = 0 - inventory.setItemStack(slot, item) { _, _, _, _ -> - count++ - } - - val playerSlot = 36 - assertEquals(item, inventory.getItemStack(slot)) - assertEquals(1, inventory.itemStacks.filterNot { it.isAir }.size) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(slot, item.withAmount(2)) - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(slot + 1, ItemStack.of(Material.DIAMOND)) - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - val slot = 0 - var count = 0 - inventory.setItemStack(slot, item, ItemComparator.EQUALS) { _, _, _, _ -> - count++ - } - - val playerSlot = 36 - assertEquals(1, inventory.inventoryConditions.size) - assertEquals(item, inventory.getItemStack(slot)) - assertEquals(1, inventory.itemStacks.filterNot { it.isAir }.size) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(slot + 1, item) - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is similar`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - val slot = 0 - var clicked = false - inventory.setItemStack(slot, item, ItemComparator.SIMILAR) { playerClicked, clickedSlot, type, _ -> - assertEquals(player, playerClicked) - assertEquals(slot, clickedSlot) - assertEquals(ClickType.LEFT_CLICK, type) - clicked = true - } - - val item2 = item.withAmount(10) - inventory.setItemStack(slot, item2) - - assertTrue(inventory.leftClick(player, 36)) - assertTrue(clicked) - } - } - - @EnvTest - @Nested - inner class SlotIsEmpty { - - @Test - fun `should return true if the slot is empty`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - assertTrue(inventory.slotIsEmpty(0)) - } - - @Test - fun `should return false if the slot is not empty`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - inventory.setItemStack(0, ItemStack.of(Material.DIAMOND)) - assertFalse(inventory.slotIsEmpty(0)) - } - } - - @EnvTest - @Nested - inner class FirstAvailableSlot { - - @Test - fun `should return the first slot if all slots are empty`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - assertEquals(0, inventory.firstAvailableSlot()) - } - - @Test - fun `should return the first available slot`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - inventory.setItemStack(0, ItemStack.of(Material.DIAMOND)) - assertEquals(1, inventory.firstAvailableSlot()) - } - - @Test - fun `should return -1 if all slots are full`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - inventory.slots.forEach { inventory.setItemStack(it, ItemStack.of(Material.DIAMOND)) } - assertEquals(-1, inventory.firstAvailableSlot()) - } - } - - @EnvTest - @Nested - inner class AddItemStackSuspendWithClickHandler { - - @Test - fun `default identifier should be equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - var count = 0 - val condition = inventory.addItemStackSuspend( - item, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> - count++ - } - - assertNotNull(condition) - assertEquals(1, inventory.inventoryConditions.size) - assertEquals(1, inventory.itemStacks.filterNot { it.isAir }.size) - - val playerSlot = 36 - assertEquals(item, inventory.getItemStack(0)) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(0, ItemStack.AIR) - inventory.addItemStack(item.withAmount(2)) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(0, ItemStack.AIR) - inventory.addItemStack(ItemStack.of(Material.DIAMOND)) - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - val slot = 0 - var count = 0 - val condition = inventory.addItemStackSuspend( - item, - ItemComparator.EQUALS, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> - count++ - } - - assertNotNull(condition) - assertEquals(1, inventory.inventoryConditions.size) - assertEquals(1, inventory.itemStacks.filterNot { it.isAir }.size) - - val playerSlot = 36 - assertEquals(item, inventory.getItemStack(slot)) - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(slot + 1, item) - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is similar`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - var clicked = false - val condition = inventory.addItemStackSuspend( - item, - ItemComparator.SIMILAR, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> - clicked = true - } - - assertNotNull(condition) - - val item2 = item.withAmount(10) - inventory.setItemStack(1, item2) - - assertTrue(inventory.leftClick(player, 37)) - assertTrue(clicked) - } - - @Test - fun `should not add item and create condition if item can't be added`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - inventory.slots.forEach { inventory.setItemStack(it, item) } - - val diamondItem = ItemStack.of(Material.DIAMOND) - val condition = inventory.addItemStackSuspend( - diamondItem, - coroutineScope = CoroutineScope(Dispatchers.Default) - ) { _, _, _, _ -> } - assertNull(condition) - inventory.itemStacks.forEach { assertNotEquals(diamondItem, it) } - } - - @Test - fun `should stay in current thread before suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - var clicked = false - val thread = Thread.currentThread().id - inventory.addItemStackSuspend(item, coroutineScope = CoroutineScope(Dispatchers.Default)) { _, _, _, _ -> - assertEquals(thread, Thread.currentThread().id) - clicked = true - } - - assertTrue(inventory.leftClick(player, 36)) - assertTrue(clicked) - } - - @Test - fun `should change thread context after suspend point`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - - val latch = CountDownLatch(1) - val coroutineScope = CoroutineScope(Dispatchers.Default) - - inventory.addItemStackSuspend(item, coroutineScope = coroutineScope) { _, _, _, _ -> - yield() - assertCoroutineContextFromScope(coroutineScope, coroutineContext) - latch.countDown() - } - - assertTrue(inventory.leftClick(player, 36)) - latch.await() - } - } - - @EnvTest - @Nested - inner class AddItemStackWithClickHandler { - - @Test - fun `default identifier should be equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - var count = 0 - val condition = inventory.addItemStack(item) { _, _, _, _ -> - count++ - } - - assertNotNull(condition) - assertEquals(1, inventory.inventoryConditions.size) - assertEquals(1, inventory.itemStacks.filterNot { it.isAir }.size) - - val playerSlot = 36 - assertEquals(item, inventory.getItemStack(0)) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(0, ItemStack.AIR) - inventory.addItemStack(item.withAmount(2)) - - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(0, ItemStack.AIR) - inventory.addItemStack(ItemStack.of(Material.DIAMOND)) - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is equals`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.DIAMOND) - val slot = 0 - var count = 0 - val condition = inventory.addItemStack(item, ItemComparator.EQUALS) { _, _, _, _ -> - count++ - } - - assertNotNull(condition) - assertEquals(1, inventory.inventoryConditions.size) - assertEquals(1, inventory.itemStacks.filterNot { it.isAir }.size) - - val playerSlot = 36 - assertEquals(item, inventory.getItemStack(slot)) - assertTrue(inventory.leftClick(player, playerSlot)) - assertEquals(1, count) - - inventory.setItemStack(slot + 1, item) - assertTrue(inventory.leftClick(player, playerSlot + 1)) - assertEquals(2, count) - } - - @Test - fun `should trigger the click handler when the item is similar`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - var clicked = false - val condition = inventory.addItemStack(item, ItemComparator.SIMILAR) { _, _, _, _ -> - clicked = true - } - - assertNotNull(condition) - - val item2 = item.withAmount(10) - inventory.setItemStack(1, item2) - - assertTrue(inventory.leftClick(player, 37)) - assertTrue(clicked) - } - - @Test - fun `should not add item and create condition if item can't be added`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - val inventory: AbstractInventory = player.inventory - - val item = ItemStack.of(Material.ARROW) - inventory.slots.forEach { inventory.setItemStack(it, item) } - - val diamondItem = ItemStack.of(Material.DIAMOND) - val condition = inventory.addItemStack(diamondItem) { _, _, _, _ -> } - assertNull(condition) - inventory.itemStacks.forEach { assertNotEquals(diamondItem, it) } - } - } - - @EnvTest - @Nested - inner class SetCloseButton { - - @Test - fun `should set the close button`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - - val inventory = Inventory(InventoryType.CHEST_1_ROW, "Test") - val condition = inventory.setCloseButton(0) - assertNotNull(condition) - assertEquals(1, inventory.inventoryConditions.size) - assertEquals( - ItemStack.of(Material.BARRIER).withDisplayName(Component.text("❌").color(NamedTextColor.RED)), - inventory.getItemStack(0) - ) - - player.openInventory(inventory) - assertFalse(inventory.leftClick(player, 0)) - assertFalse(inventory.isViewer(player)) - assertNull(player.openInventory) - } - - @Test - fun `should set the close button who override item at the slot`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - - val inventory = Inventory(InventoryType.CHEST_1_ROW, "Test") - inventory.setItemStack(0, ItemStack.of(Material.DIAMOND)) - inventory.setCloseButton(0) - assertTrue { inventory.getItemStack(0).material() == Material.BARRIER } - - inventory.setItemStack(0, ItemStack.of(Material.DIAMOND)) - player.openInventory(inventory) - assertTrue(inventory.leftClick(player, 0)) - assertTrue(inventory.isViewer(player)) - assertEquals(inventory, player.openInventory) - } - } - - @EnvTest - @Nested - inner class SetPreviousButton { - - private fun getItem(backInventory: Inventory): ItemStack { - return getChangeItem("< ", backInventory) - } - - @Test - fun `should set the previous button`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - - val inventory = Inventory(InventoryType.CHEST_1_ROW, randomString()) - val backInventory = Inventory(InventoryType.BEACON, randomString()) - val condition = inventory.setPreviousButton(0, backInventory) - val expectedNavItem = getItem(backInventory) - assertNotNull(condition) - assertEquals(1, inventory.inventoryConditions.size) - assertEquals(expectedNavItem, inventory.getItemStack(0)) - - player.openInventory(inventory) - assertTrue(inventory.isViewer(player)) - assertEquals(inventory, player.openInventory) - assertFalse(inventory.leftClick(player, 0)) - - assertFalse(inventory.isViewer(player)) - assertTrue(backInventory.isViewer(player)) - assertEquals(backInventory, player.openInventory) - } - - @Test - fun `should set the previous button who override item at the slot`(env: Env) { - val inventory = Inventory(InventoryType.CHEST_1_ROW, randomString()) - val backInventory = Inventory(InventoryType.BEACON, randomString()) - inventory.setItemStack(0, ItemStack.of(Material.DIAMOND)) - inventory.setPreviousButton(0, backInventory) - val expectedNavItem = getItem(backInventory) - assertEquals(expectedNavItem, inventory.getItemStack(0)) - } - } - - @EnvTest - @Nested - inner class SetNextButton { - - private fun getItem(inventory: Inventory): ItemStack { - return getChangeItem("> ", inventory) - } - - @Test - fun `should set the next button`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - - val inventory = Inventory(InventoryType.CHEST_1_ROW, randomString()) - val nextInventory = Inventory(InventoryType.BLAST_FURNACE, randomString()) - val condition = inventory.setNextButton(0, nextInventory) - val expectedNavItem = getItem(nextInventory) - assertNotNull(condition) - assertEquals(1, inventory.inventoryConditions.size) - assertEquals(expectedNavItem, inventory.getItemStack(0)) - - player.openInventory(inventory) - assertTrue(inventory.isViewer(player)) - assertEquals(inventory, player.openInventory) - assertFalse(inventory.leftClick(player, 0)) - - assertFalse(inventory.isViewer(player)) - assertTrue(nextInventory.isViewer(player)) - assertEquals(nextInventory, player.openInventory) - } - - @Test - fun `should set the next button who override item at the slot`(env: Env) { - val inventory = Inventory(InventoryType.CHEST_1_ROW, randomString()) - val nextInventory = Inventory(InventoryType.ENCHANTMENT, randomString()) - inventory.setItemStack(0, ItemStack.of(Material.DIAMOND)) - inventory.setNextButton(0, nextInventory) - val expectedNavItem = getItem(nextInventory) - assertEquals(expectedNavItem, inventory.getItemStack(0)) - } - } - - @EnvTest - @Nested - inner class SetItemChangeInventory { - - private val textItem = "test" - - private fun getItem(inventory: Inventory): ItemStack { - return getChangeItem(textItem, inventory) - } - - @Test - fun `should set the change button`(env: Env) { - val instance = env.createFlatInstance() - val player = env.createPlayer(instance, randomPos()) - - val inventory = Inventory(InventoryType.CHEST_1_ROW, randomString()) - val changeInventory = Inventory(InventoryType.BLAST_FURNACE, randomString()) - val condition = inventory.setItemChangeInventory(0, changeInventory, textItem) - val expectedNavItem = getItem(changeInventory) - assertNotNull(condition) - assertEquals(1, inventory.inventoryConditions.size) - assertEquals(expectedNavItem, inventory.getItemStack(0)) - - player.openInventory(inventory) - assertTrue(inventory.isViewer(player)) - assertEquals(inventory, player.openInventory) - assertFalse(inventory.leftClick(player, 0)) - - assertFalse(inventory.isViewer(player)) - assertTrue(changeInventory.isViewer(player)) - assertEquals(changeInventory, player.openInventory) - } - - @Test - fun `should set the change button who override item at the slot`(env: Env) { - val inventory = Inventory(InventoryType.CHEST_1_ROW, randomString()) - val nextInventory = Inventory(InventoryType.ENCHANTMENT, randomString()) - inventory.setItemStack(0, ItemStack.of(Material.DIAMOND)) - inventory.setItemChangeInventory(0, nextInventory, textItem) - val expectedNavItem = getItem(nextInventory) - assertEquals(expectedNavItem, inventory.getItemStack(0)) - } - } - - private fun getChangeItem(text: String, inventory: Inventory): ItemStack { - return ItemStack.of(Material.ARROW) - .withDisplayName( - Component.text(text) - .color(NamedTextColor.GOLD) - .decoration(TextDecoration.BOLD, true) - .append( - inventory.title - .color(NamedTextColor.GRAY) - .decoration(TextDecoration.ITALIC, true) - .decoration(TextDecoration.BOLD, false) - ) - ) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/ItemStackExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/ItemStackExtTest.kt index 219d671b..feb309c4 100644 --- a/src/test/kotlin/com/github/rushyverse/api/extension/ItemStackExtTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/extension/ItemStackExtTest.kt @@ -1,361 +1,326 @@ package com.github.rushyverse.api.extension -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.minestom.server.item.ItemStack -import net.minestom.server.item.Material +import com.github.rushyverse.api.utils.randomString +import io.mockk.every +import io.mockk.mockk +import org.bukkit.Material +import org.bukkit.inventory.ItemStack import org.junit.jupiter.api.Nested -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals class ItemStackExtTest { + @Test + fun `get material property returns the value of type property`() { + val item = item {} + assertEquals(item.type, item.material) + item.type = Material.IRON_SHOVEL + assertEquals(item.type, item.material) + } + + @Test + fun `set material property define the value of type property`() { + val item = item {} + assertEquals(item.type, item.material) + item.material = Material.IRON_SHOVEL + assertEquals(item.type, item.material) + } + + @Test + fun `item function builder create init item stack with air material`() { + val item = item {} + assertEquals(Material.AIR, item.type) + } + + @Test + fun `item function builder set the properties of item stack`() { + val expectedType = Material.STICK + val expectedAmount = 42 + + val item = item { + type = expectedType + amount = expectedAmount + } + + assertEquals(expectedType, item.type) + assertEquals(expectedAmount, item.amount) + } + + @Test + fun `ItemStack function builder create init item stack with air material`() { + val item = ItemStack {} + assertEquals(Material.AIR, item.type) + } + + @Test + fun `ItemStack function builder set the properties of item stack`() { + val expectedType = Material.STICK + val expectedAmount = 42 + + val item = ItemStack { + type = expectedType + amount = expectedAmount + } + + assertEquals(expectedType, item.type) + assertEquals(expectedAmount, item.amount) + } + + @Test + fun `ItemStack function builder set the material by the parameter`() { + val expectedType = Material.STICK + val item = ItemStack(expectedType) { } + assertEquals(expectedType, item.type) + } + + @Test + fun `ItemStack function builder set the properties by lambda`() { + val expectedAmount = 42 + val item = ItemStack(Material.AIR) { + amount = expectedAmount + } + assertEquals(expectedAmount, item.amount) + } + @Nested - inner class Builder { + inner class FilterNotAir { - @Nested - inner class FormattedLore { + @Test + fun `array of items`() { + val item1 = mockItem(Material.DIAMOND) + val item2 = mockItem(Material.AMETHYST_BLOCK) + assertEquals( + listOf(item1, item2), arrayOf( + item1, + mockItem(Material.AIR), + item2, + mockItem(Material.AIR) + ).filterNotAir() + ) + } - private lateinit var builder: ItemStack.Builder + @Test + fun `list of items`() { + val item1 = mockItem(Material.DIAMOND_HELMET) + val item2 = mockItem(Material.ATTACHED_PUMPKIN_STEM) + assertEquals( + listOf(item1, item2), listOf( + item1, + mockItem(Material.AIR), + item2, + mockItem(Material.AIR) + ).filterNotAir() + ) + } - @BeforeTest - fun onBefore() { - builder = ItemStack.builder(Material.DIAMOND_SWORD) - } + @Test + fun `sequence of items`() { + val item1 = mockItem(Material.BEDROCK) + val item2 = mockItem(Material.ACACIA_LEAVES) + assertEquals( + listOf(item1, item2), sequenceOf( + item1, + mockItem(Material.AIR), + item2, + mockItem(Material.AIR) + ).filterNotAir().toList() + ) + } + } + + @Nested + inner class ItemIndexed { + + @Nested + inner class ArrayNotNullItems { @Test - fun `should return an empty sequence if the string is empty`() { - val expected = ItemStack.builder(Material.DIAMOND_SWORD).lore().build() - assertEquals(expected, builder.formattedLore("").build()) + fun `empty array returns empty map`() { + assertEquals(emptyMap(), emptyArray().itemsIndexed()) } @Test - fun `should cut sentence without space`() { - val expected = ItemStack.builder(Material.DIAMOND_SWORD).lore( - Component.text("0123-").color(NamedTextColor.GRAY), - Component.text("4567-").color(NamedTextColor.GRAY), - Component.text("89ab-").color(NamedTextColor.GRAY), - Component.text("cdef").color(NamedTextColor.GRAY) - ).build() - assertEquals( - expected, - builder.formattedLore("0123456789abcdef", 5).build() + fun `items not air is linked to the index`() { + val item1 = mockItem(Material.ACACIA_DOOR) + val item2 = mockItem(Material.ACTIVATOR_RAIL) + val array = arrayOf( + mockItem(Material.AIR), + item1, + mockItem(Material.AIR), + mockItem(Material.AIR), + item2 ) + val map = array.itemsIndexed() + val expectedMap = mapOf( + 1 to item1, + 4 to item2 + ) + + assertEquals(expectedMap, map) } + } + + @Nested + inner class ArrayNullableItems { @Test - fun `should create only one component if line length is equals to the string size`() { - val sentence = "Hello World" - val expected = ItemStack.builder(Material.DIAMOND_SWORD) - .lore(Component.text(sentence).color(NamedTextColor.GRAY)) - .build() - assertEquals(expected, builder.formattedLore(sentence).build()) + fun `empty array returns empty map`() { + assertEquals(emptyMap(), emptyArray().itemsIndexed()) } @Test - fun `should create multiple components by cut on the line length char adding a '-'`() { - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Hel-").color(NamedTextColor.GRAY), - Component.text("lo").color(NamedTextColor.GRAY), - Component.text("Wor-").color(NamedTextColor.GRAY), - Component.text("ld").color(NamedTextColor.GRAY) - ) - .build(), - builder.formattedLore("Hello World", 4).build() + fun `items not air is linked to the index`() { + val item1 = mockItem(Material.ACACIA_DOOR) + val item2 = mockItem(Material.ACTIVATOR_RAIL) + val array = arrayOfNulls(4) + array[0] = null + array[1] = item1 + array[2] = item2 + array[3] = mockItem(Material.AIR) + + val map = array.itemsIndexed() + val expectedMap = mapOf( + 1 to item1, + 2 to item2 ) - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("He-").color(NamedTextColor.GRAY), - Component.text("llo").color(NamedTextColor.GRAY), - Component.text("Wo-").color(NamedTextColor.GRAY), - Component.text("rld").color(NamedTextColor.GRAY) - ) - .build(), - builder.formattedLore("Hello World", 3).build() - ) + assertEquals(expectedMap, map) } + } + + @Nested + inner class SequenceNotNullItems { @Test - fun `should create multiple element by cut on the previous space char`() { - val sentence = "Hello World" - // Indexes of "W" to "d" chars - for (i in 6..10) { - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Hello").color(NamedTextColor.GRAY), - Component.text("World").color(NamedTextColor.GRAY) - ) - .build(), - builder.formattedLore(sentence, i).build() - ) - } + fun `empty array returns empty map`() { + assertEquals(emptyMap(), emptySequence().itemsIndexed()) } @Test - fun `should create multiple components with long sentence`() { - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("This is a tool").color(NamedTextColor.GRAY), - Component.text("to create a").color(NamedTextColor.GRAY), - Component.text("game").color(NamedTextColor.GRAY) - ) - .build(), - builder.formattedLore("This is a tool to create a game", 15).build() - ) - - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("This is a tool").color(NamedTextColor.GRAY), - Component.text("to create a").color(NamedTextColor.GRAY), - Component.text("game0123456789-").color(NamedTextColor.GRAY), - Component.text("0123456789").color(NamedTextColor.GRAY) - ) - .build(), - builder.formattedLore("This is a tool to create a game01234567890123456789", 15).build() + fun `items not air is linked to the index`() { + val item1 = mockItem(Material.STICK) + val item2 = mockItem(Material.AMETHYST_BLOCK) + + val sequence = sequenceOf( + item1, + mockItem(Material.AIR), + mockItem(Material.AIR), + item2, + mockItem(Material.AIR) ) - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Ajoutez, chattez et rejoignez").color(NamedTextColor.GRAY), - Component.text("vos amis à travers le serveur").color(NamedTextColor.GRAY) - ) - .build(), - builder.formattedLore("Ajoutez, chattez et rejoignez vos amis à travers le serveur", 30).build() + val map = sequence.itemsIndexed() + val expectedMap = mapOf( + 0 to item1, + 3 to item2 ) - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Ajoutez, chattez et rejoignez").color(NamedTextColor.GRAY), - Component.text("v").color(NamedTextColor.GRAY) - ) - .build(), - builder.formattedLore("Ajoutez, chattez et rejoignez v", 30).build() - ) + assertEquals(expectedMap, map) + } + } - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Ajoutez, chattez et rejoignez").color(NamedTextColor.GRAY), - Component.text(" ").color(NamedTextColor.GRAY) - ) - .build(), - builder.formattedLore("Ajoutez, chattez et rejoignez ", 30).build() - ) + @Nested + inner class SequenceNullableItems { - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Add, chat and join your friends through").color(NamedTextColor.GRAY), - Component.text("the server").color(NamedTextColor.GRAY) - ) - .build(), - builder.formattedLore("Add, chat and join your friends through the server", 40).build() - ) + @Test + fun `empty array returns empty map`() { + assertEquals(emptyMap(), emptySequence().itemsIndexed()) } @Test - fun `should apply transformation for created components`() { - val expected = ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Hello").color(NamedTextColor.RED), - Component.text("World").color(NamedTextColor.RED) - ) - .build() - assertEquals( - expected, - builder.formattedLore("Hello World", 5) { color(NamedTextColor.RED) }.build() + fun `items not air is linked to the index`() { + val item1 = mockItem(Material.STICK) + val item2 = mockItem(Material.AMETHYST_BLOCK) + + val sequence = sequenceOf( + item1, + null, + mockItem(Material.AIR), + item2, + null ) - } - } - } - - @Nested - inner class FormattedLore { - - private lateinit var item: ItemStack - - @BeforeTest - fun onBefore() { - item = ItemStack.of(Material.DIAMOND_SWORD) - } - - @Test - fun `should return an empty sequence if the string is empty`() { - val expected = ItemStack.builder(Material.DIAMOND_SWORD).lore().build() - assertEquals(expected, item.withFormattedLore("")) - } + val map = sequence.itemsIndexed() + val expectedMap = mapOf( + 0 to item1, + 3 to item2 + ) - @Test - fun `should cut sentence without space`() { - val expected = ItemStack.builder(Material.DIAMOND_SWORD).lore( - Component.text("0123-").color(NamedTextColor.GRAY), - Component.text("4567-").color(NamedTextColor.GRAY), - Component.text("89ab-").color(NamedTextColor.GRAY), - Component.text("cdef").color(NamedTextColor.GRAY) - ).build() - assertEquals( - expected, - item.withFormattedLore("0123456789abcdef", 5) - ) + assertEquals(expectedMap, map) + } } - @Test - fun `should create only one component if line length is equals to the string size`() { - val sentence = "Hello World" - val expected = ItemStack.builder(Material.DIAMOND_SWORD) - .lore(Component.text(sentence).color(NamedTextColor.GRAY)) - .build() - assertEquals(expected, item.withFormattedLore(sentence)) - } + @Nested + inner class IterableNotNullItems { - @Test - fun `should create multiple components by cut on the line length char adding a '-'`() { - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Hel-").color(NamedTextColor.GRAY), - Component.text("lo").color(NamedTextColor.GRAY), - Component.text("Wor-").color(NamedTextColor.GRAY), - Component.text("ld").color(NamedTextColor.GRAY) - ) - .build(), - item.withFormattedLore("Hello World", 4) - ) + @Test + fun `empty array returns empty map`() { + assertEquals(emptyMap(), emptyList().itemsIndexed()) + } - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("He-").color(NamedTextColor.GRAY), - Component.text("llo").color(NamedTextColor.GRAY), - Component.text("Wo-").color(NamedTextColor.GRAY), - Component.text("rld").color(NamedTextColor.GRAY) - ) - .build(), - item.withFormattedLore("Hello World", 3) - ) - } + @Test + fun `items not air is linked to the index`() { + val item1 = mockItem(Material.BONE) + val item2 = mockItem(Material.IRON_SWORD) + + val list = listOf( + item1, + mockItem(Material.AIR), + mockItem(Material.AIR), + item1, + item2, + mockItem(Material.AIR) + ) - @Test - fun `should create multiple element by cut on the previous space char`() { - val sentence = "Hello World" - // Indexes of "W" to "d" chars - for (i in 6..10) { - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Hello").color(NamedTextColor.GRAY), - Component.text("World").color(NamedTextColor.GRAY) - ) - .build(), - item.withFormattedLore(sentence, i) + val map = list.itemsIndexed() + val expectedMap = mapOf( + 0 to item1, + 3 to item1, + 4 to item2 ) + + assertEquals(expectedMap, map) } } - @Test - fun `should create multiple components with long sentence`() { - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("This is a tool").color(NamedTextColor.GRAY), - Component.text("to create a").color(NamedTextColor.GRAY), - Component.text("game").color(NamedTextColor.GRAY) - ) - .build(), - item.withFormattedLore("This is a tool to create a game", 15) - ) - - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("This is a tool").color(NamedTextColor.GRAY), - Component.text("to create a").color(NamedTextColor.GRAY), - Component.text("game0123456789-").color(NamedTextColor.GRAY), - Component.text("0123456789").color(NamedTextColor.GRAY) - ) - .build(), - item.withFormattedLore("This is a tool to create a game01234567890123456789", 15) - ) - - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Ajoutez, chattez et rejoignez").color(NamedTextColor.GRAY), - Component.text("vos amis à travers le serveur").color(NamedTextColor.GRAY) - ) - .build(), - item.withFormattedLore("Ajoutez, chattez et rejoignez vos amis à travers le serveur", 30) - ) - - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Ajoutez, chattez et rejoignez").color(NamedTextColor.GRAY), - Component.text("v").color(NamedTextColor.GRAY) - ) - .build(), - item.withFormattedLore("Ajoutez, chattez et rejoignez v", 30) - ) + @Nested + inner class IterableNullableItems { - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Ajoutez, chattez et rejoignez").color(NamedTextColor.GRAY), - Component.text(" ").color(NamedTextColor.GRAY) - ) - .build(), - item.withFormattedLore("Ajoutez, chattez et rejoignez ", 30) - ) + @Test + fun `empty array returns empty map`() { + assertEquals(emptyMap(), emptyList().itemsIndexed()) + } - assertEquals( - ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Add, chat and join your friends through").color(NamedTextColor.GRAY), - Component.text("the server").color(NamedTextColor.GRAY) - ) - .build(), - item.withFormattedLore("Add, chat and join your friends through the server", 40) - ) - } + @Test + fun `items not air is linked to the index`() { + val item1 = mockItem(Material.BONE) + val item2 = mockItem(Material.IRON_SWORD) + + val list = listOf( + item1, + null, + mockItem(Material.AIR), + item1, + item2, + mockItem(Material.AIR) + ) - @Test - fun `should apply transformation for created components`() { - val expected = ItemStack.builder(Material.DIAMOND_SWORD) - .lore( - Component.text("Hello").color(NamedTextColor.RED), - Component.text("World").color(NamedTextColor.RED) + val map = list.itemsIndexed() + val expectedMap = mapOf( + 0 to item1, + 3 to item1, + 4 to item2 ) - .build() - assertEquals( - expected, - item.withFormattedLore("Hello World", 5) { color(NamedTextColor.RED) } - ) - } + assertEquals(expectedMap, map) + } + } } - @Nested - inner class WithLore { - - @Test - fun `should set a single lore`() { - val item = ItemStack.of(Material.STONE).withLore(Component.text("Hello")) - assertEquals( - ItemStack.builder(Material.STONE).lore(Component.text("Hello")).build(), - item - ) + private fun mockItem(material: Material): ItemStack { + return mockk(randomString()).apply { + every { type } returns material } - } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/JavaPluginExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/JavaPluginExtTest.kt new file mode 100644 index 00000000..f3813639 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/JavaPluginExtTest.kt @@ -0,0 +1,168 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.item.CraftSlot +import com.github.rushyverse.api.item.exception.CraftResultMissingException +import com.github.rushyverse.api.utils.randomString +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.bukkit.Material +import org.bukkit.Server +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.Recipe +import org.bukkit.inventory.ShapedRecipe +import org.bukkit.plugin.java.JavaPlugin +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import java.util.* +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class JavaPluginExtTest { + + private lateinit var plugin: JavaPlugin + private lateinit var server: Server + + @BeforeTest + fun onBefore() { + plugin = mockk() + server = mockk() + + every { plugin.server } returns server + every { plugin.name } returns randomString() + every { server.pluginManager } returns mockk() + } + + @Nested + @DisplayName("Register craft") + inner class RegisterCraft { + + @Test + fun `no add a final item throws error`() { + assertThrows { + plugin.registerCraft {} + } + } + + @Test + fun `add recipe created into the server`() { + val slotRecipe = slot() + every { server.addRecipe(capture(slotRecipe)) } returns true + + val expectedResult: ItemStack + val expectedKey = "test" + plugin.registerCraft(key = "test") { + result = mockk().also { + every { it.type } returns Material.STICK + every { it.amount } returns 1 + every { it.hasItemMeta() } returns false + } + expectedResult = result!! + } + + val recipe = slotRecipe.captured as ShapedRecipe + assertEquals(expectedKey, recipe.key.key) + + val resultRecipe = recipe.result + assertEquals(expectedResult.amount, resultRecipe.amount) + assertEquals(expectedResult.type, resultRecipe.type) + assertContentEquals(arrayOf(" ", " ", " "), recipe.shape) + } + + @Test + fun `add the defined recipe from the builder`() { + val slotRecipe = slot() + every { server.addRecipe(capture(slotRecipe)) } returns true + + plugin.registerCraft { + val item1 = mockk() + val item2 = mockk() + + set(CraftSlot.TopLeft, item = item1) + set(CraftSlot.Top, item = item2) + set(CraftSlot.TopRight, item = mockk()) + + set(CraftSlot.CenterLeft, item = mockk()) + set(CraftSlot.Center, item = item1) + set(CraftSlot.CenterRight, item = mockk()) + + set(CraftSlot.BottomLeft, item = item2) + set(CraftSlot.Bottom, item = mockk()) + set(CraftSlot.BottomRight, item = mockk()) + result = mockk().also { + every { it.type } returns Material.ACACIA_BUTTON + every { it.amount } returns 1 + every { it.hasItemMeta() } returns false + } + } + + val recipe = slotRecipe.captured as ShapedRecipe + assertContentEquals(arrayOf("ABC", "DAE", "BFG"), recipe.shape) + } + + @Test + fun `add all different item will create different designation`() { + val slotRecipe = slot() + every { server.addRecipe(capture(slotRecipe)) } returns true + + plugin.registerCraft { + for (index in CraftSlot.entries) { + set(index, item = mockk()) + } + + result = mockk().also { + every { it.type } returns Material.ACACIA_BUTTON + every { it.amount } returns 1 + every { it.hasItemMeta() } returns false + } + } + + val recipe = slotRecipe.captured as ShapedRecipe + assertContentEquals(arrayOf("ABC", "DEF", "GHI"), recipe.shape) + } + + @Test + fun `add several times the same item will create once designation`() { + val slotRecipe = slot() + every { server.addRecipe(capture(slotRecipe)) } returns true + + plugin.registerCraft { + val item = mockk() + for (index in CraftSlot.entries) { + set(index, item = item) + } + result = mockk().also { + every { it.type } returns Material.ACACIA_BUTTON + every { it.amount } returns 1 + every { it.hasItemMeta() } returns false + } + } + + val recipe = slotRecipe.captured as ShapedRecipe + assertContentEquals(arrayOf("AAA", "AAA", "AAA"), recipe.shape) + } + + @Test + fun `no key add a default UUID name`() { + val slotRecipe = slot() + every { server.addRecipe(capture(slotRecipe)) } returns true + + plugin.registerCraft { + result = mockk().also { + every { it.type } returns Material.STICK + every { it.amount } returns 1 + every { it.hasItemMeta() } returns false + } + } + + val recipe = slotRecipe.captured as ShapedRecipe + val key = recipe.key.key + UUID.fromString(key) + } + + } + +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/ListenerExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/ListenerExtTest.kt deleted file mode 100644 index a9602c51..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/extension/ListenerExtTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.rushyverse.api.extension - -import com.github.rushyverse.api.utils.assertCoroutineContextFromScope -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.spyk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.yield -import net.minestom.server.event.EventListener -import net.minestom.server.event.EventNode -import net.minestom.server.event.player.PlayerSkinInitEvent -import java.util.concurrent.CountDownLatch -import kotlin.coroutines.coroutineContext -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue - -class ListenerExtTest { - - @Test - fun `should handle event in coroutine context by adding listener suspend`() { - val node = spyk(EventNode.all("test")) - val slot = slot>() - every { node.addListener(capture(slot)) } returns mockk() - - val currentThread = Thread.currentThread() - val latch = CountDownLatch(1) - val event = mockk() - - var executed = false - val scope = CoroutineScope(Dispatchers.Default) - node.addListenerSuspend(scope) { - assertCoroutineContextFromScope(scope, coroutineContext) - assertEquals(event, it) - executed = true - - assertEquals(currentThread, Thread.currentThread()) - yield() - assertNotEquals(currentThread, Thread.currentThread()) - latch.countDown() - } - - slot.captured.run(event) - latch.await() - assertTrue(executed) - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/LocationExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/LocationExtTest.kt new file mode 100644 index 00000000..770d850e --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/LocationExtTest.kt @@ -0,0 +1,124 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.utils.randomString +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.bukkit.Location +import org.bukkit.World +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class LocationExtTest { + + private lateinit var loc: Location + + @BeforeTest + fun onBefore() { + loc = Location(mockk(randomString()), 0.0, 1.0, 2.0, 3.0f, 4.0f) + } + + @Nested + @DisplayName("Copy properties") + inner class Copy { + + @Test + fun `copyFrom will copy properties into the instance`() { + val dest = Location(mockk("World 2"), -1.0, -1.0, -1.0, -1.0f, -1.0f) + + dest.copyFrom(loc) + assertEquals(loc.world, dest.world) + assertEquals(loc.x, dest.x) + assertEquals(loc.y, dest.y) + assertEquals(loc.z, dest.z) + assertEquals(loc.yaw, dest.yaw) + assertEquals(loc.pitch, dest.pitch) + } + + @Test + fun `copy without arg will clone the location`() { + assertEquals(loc.clone(), loc.copy()) + } + + @Test + fun `copy create a new instance`() { + assertTrue { loc !== loc.copy() } + } + + @Test + fun `copy with only world property will change only the property`() { + val world = mockk(randomString()) + assertEquals(Location(world, loc.x, loc.y, loc.z, loc.yaw, loc.pitch), loc.copy(world = world)) + } + + @Test + fun `copy with only x property will change only the property`() { + val x = loc.x + 10 + assertEquals(Location(loc.world, x, loc.y, loc.z, loc.yaw, loc.pitch), loc.copy(x = x)) + } + + @Test + fun `copy with only y property will change only the property`() { + val y = loc.y + 10 + assertEquals(Location(loc.world, loc.x, y, loc.z, loc.yaw, loc.pitch), loc.copy(y = y)) + } + + @Test + fun `copy with only z property will change only the property`() { + val z = loc.z + 10 + assertEquals(Location(loc.world, loc.x, loc.y, z, loc.yaw, loc.pitch), loc.copy(z = z)) + } + + @Test + fun `copy with only yaw property will change only the property`() { + val yaw = loc.yaw + 10 + assertEquals(Location(loc.world, loc.x, loc.y, loc.z, yaw, loc.pitch), loc.copy(yaw = yaw)) + } + + @Test + fun `copy with only pitch property will change only the property`() { + val pitch = loc.pitch + 10 + assertEquals(Location(loc.world, loc.x, loc.y, loc.z, loc.yaw, pitch), loc.copy(pitch = pitch)) + } + + @Test + fun `copy with all args will change all properties`() { + val world = mockk(randomString()) + val x = loc.x + 10 + val y = loc.y + 20 + val z = loc.z + 30 + val yaw = loc.yaw + 40 + val pitch = loc.pitch + 50 + assertEquals(Location(world, x, y, z, yaw, pitch), loc.copy(world, x, y, z, yaw, pitch)) + } + } + + @Nested + inner class CenterRelative { + + @Test + fun `should return the center position between two points at 0 0 0`() { + loc = Location(loc.world, 0.0, 0.0, 0.0) + val other = Location(loc.world, 0.0, 0.0, 0.0) + val expected = Location(loc.world, 0.0, 0.0, 0.0) + loc.centerRelative(other) shouldBe expected + + other shouldBe Location(loc.world, 0.0, 0.0, 0.0) + loc shouldBe Location(loc.world, 0.0, 0.0, 0.0) + } + + @Test + fun `should return the center position between two points`() { + val previousLoc = loc.copy() + val other = Location(loc.world, 10.0, 10.0, 10.0) + val expected = Location(loc.world, 5.0, 5.5, 6.0) + loc.centerRelative(other) shouldBe expected + + other shouldBe Location(loc.world, 10.0, 10.0, 10.0) + loc shouldBe previousLoc + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/MaterialExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/MaterialExtTest.kt new file mode 100644 index 00000000..f85b8959 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/MaterialExtTest.kt @@ -0,0 +1,88 @@ +package com.github.rushyverse.api.extension + +import be.seeseemelk.mockbukkit.MockBukkit +import io.kotest.matchers.shouldBe +import org.bukkit.Material +import org.junit.jupiter.api.Nested +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class MaterialExtTest { + + @BeforeTest + fun onBefore() { + MockBukkit.mock() + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class IsWool { + + @Test + fun `should be wool`() { + Material.BLACK_WOOL.isWool() shouldBe true + Material.BLUE_WOOL.isWool() shouldBe true + Material.BROWN_WOOL.isWool() shouldBe true + Material.CYAN_WOOL.isWool() shouldBe true + Material.GRAY_WOOL.isWool() shouldBe true + Material.GREEN_WOOL.isWool() shouldBe true + Material.LIGHT_BLUE_WOOL.isWool() shouldBe true + Material.LIGHT_GRAY_WOOL.isWool() shouldBe true + Material.LIME_WOOL.isWool() shouldBe true + Material.MAGENTA_WOOL.isWool() shouldBe true + Material.ORANGE_WOOL.isWool() shouldBe true + Material.PINK_WOOL.isWool() shouldBe true + Material.PURPLE_WOOL.isWool() shouldBe true + Material.RED_WOOL.isWool() shouldBe true + Material.WHITE_WOOL.isWool() shouldBe true + Material.YELLOW_WOOL.isWool() shouldBe true + } + + @Test + fun `should not be wool`() { + Material.AIR.isWool() shouldBe false + Material.BLACK_CARPET.isWool() shouldBe false + Material.BLUE_CARPET.isWool() shouldBe false + Material.ACACIA_FENCE.isWool() shouldBe false + } + + } + + @Nested + inner class IsWoolCarpet { + + @Test + fun `should be wool carpet`() { + Material.BLACK_CARPET.isWoolCarpet() shouldBe true + Material.BLUE_CARPET.isWoolCarpet() shouldBe true + Material.BROWN_CARPET.isWoolCarpet() shouldBe true + Material.CYAN_CARPET.isWoolCarpet() shouldBe true + Material.GRAY_CARPET.isWoolCarpet() shouldBe true + Material.GREEN_CARPET.isWoolCarpet() shouldBe true + Material.LIGHT_BLUE_CARPET.isWoolCarpet() shouldBe true + Material.LIGHT_GRAY_CARPET.isWoolCarpet() shouldBe true + Material.LIME_CARPET.isWoolCarpet() shouldBe true + Material.MAGENTA_CARPET.isWoolCarpet() shouldBe true + Material.ORANGE_CARPET.isWoolCarpet() shouldBe true + Material.PINK_CARPET.isWoolCarpet() shouldBe true + Material.PURPLE_CARPET.isWoolCarpet() shouldBe true + Material.RED_CARPET.isWoolCarpet() shouldBe true + Material.WHITE_CARPET.isWoolCarpet() shouldBe true + Material.YELLOW_CARPET.isWoolCarpet() shouldBe true + } + + @Test + fun `should not be wool carpet`() { + Material.AIR.isWoolCarpet() shouldBe false + Material.BLACK_WOOL.isWoolCarpet() shouldBe false + Material.BLUE_WOOL.isWoolCarpet() shouldBe false + Material.ACACIA_FENCE.isWoolCarpet() shouldBe false + } + + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/MerchantRecipeExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/MerchantRecipeExtTest.kt new file mode 100644 index 00000000..ce953f01 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/MerchantRecipeExtTest.kt @@ -0,0 +1,86 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.utils.randomString +import io.mockk.mockk +import org.bukkit.Material +import org.bukkit.inventory.ItemStack +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class MerchantRecipeExtTest { + + @Test + fun `constructor utils function`() { + val result = mockk(randomString()) + val maxUses = 1 + val uses = 2 + val experienceReward = false + val villagerExperience = 3 + val priceMultiplier = 1.1F + val demand = 4 + val specialPrice = 5 + val ignoreDiscounts = true + + val item1 = ItemStack(Material.COOKED_BEEF) + val item2 = ItemStack(Material.ACACIA_DOOR) + val ingredients = listOf(item1, item2) + + var recipe = MerchantRecipe( + result = result, + maxUses = maxUses, + uses = uses, + experienceReward = experienceReward, + villagerExperience = villagerExperience, + priceMultiplier = priceMultiplier, + demand = demand, + specialPrice = specialPrice, + ignoreDiscounts = ignoreDiscounts, + ingredients = ingredients + ) + + assertEquals(result, recipe.result) + assertEquals(maxUses, recipe.maxUses) + assertEquals(uses, recipe.uses) + assertEquals(experienceReward, recipe.hasExperienceReward()) + assertEquals(villagerExperience, recipe.villagerExperience) + assertEquals(priceMultiplier, recipe.priceMultiplier) + assertEquals(demand, recipe.demand) + assertEquals(specialPrice, recipe.specialPrice) + assertEquals(ignoreDiscounts, recipe.shouldIgnoreDiscounts()) + // Problem with Bukkit server + // assertEquals(ingredients, recipe.ingredients) + + recipe = MerchantRecipe( + result = result, + maxUses = 0, + experienceReward = true, + ignoreDiscounts = false, + ) + + assertTrue { recipe.hasExperienceReward() } + assertFalse { recipe.shouldIgnoreDiscounts() } + } + + @Test + fun `default constructor utils function`() { + val result = mockk(randomString()) + val maxUses = 10 + val recipe = MerchantRecipe( + result = result, + maxUses = maxUses + ) + + assertEquals(result, recipe.result) + assertEquals(maxUses, recipe.maxUses) + assertEquals(0, recipe.uses) + assertEquals(false, recipe.hasExperienceReward()) + assertEquals(0, recipe.villagerExperience) + assertEquals(0.0f, recipe.priceMultiplier) + assertEquals(0, recipe.demand) + assertEquals(0, recipe.specialPrice) + assertEquals(false, recipe.shouldIgnoreDiscounts()) + assertEquals(emptyList(), recipe.ingredients) + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/NumberExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/NumberExtTest.kt index 2a6a89b2..617d4d08 100644 --- a/src/test/kotlin/com/github/rushyverse/api/extension/NumberExtTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/extension/NumberExtTest.kt @@ -1,11 +1,12 @@ package com.github.rushyverse.api.extension +import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Nested import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvFileSource import org.junit.jupiter.params.provider.ValueSource import kotlin.test.Test -import kotlin.test.assertEquals class NumberExtTest { @@ -26,19 +27,19 @@ class NumberExtTest { 4 to "IV", 1 to "I" ) - assertEquals(expected, ROMAN_NUMERALS_VALUES) + ROMAN_NUMERALS_VALUES shouldBe expected } @Test fun `roman numerals array should be ordered from the largest to the smallest`() { val expected = listOf("M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I") - assertEquals(expected, ROMAN_NUMERALS.toList()) + ROMAN_NUMERALS.toList() shouldBe expected } @Test fun `roman values array should be ordered from the largest to the smallest`() { val expected = listOf(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1) - assertEquals(expected, ROMAN_VALUES.toList()) + ROMAN_VALUES.toList() shouldBe expected } @Nested @@ -51,47 +52,21 @@ class NumberExtTest { @ValueSource(ints = [0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10]) fun `should throw exception when number is negative or zero`(number: Int) { val ex = assertThrows { number.toRomanNumerals() } - assertEquals("Number must be positive", ex.message) + ex.message shouldBe "Number must be positive" } @ParameterizedTest @ValueSource(ints = [4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010]) fun `should throw exception when number is greater than 3999`(number: Int) { val ex = assertThrows { number.toRomanNumerals() } - assertEquals("Number must be less than 4000", ex.message) + ex.message shouldBe "Number must be less than 4000" } @ParameterizedTest - @ValueSource(ints = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000]) - fun `should return pure roman numerals`(number: Int) { - val expected = when (number) { - 1 -> "I" - 4 -> "IV" - 5 -> "V" - 9 -> "IX" - 10 -> "X" - 40 -> "XL" - 50 -> "L" - 90 -> "XC" - 100 -> "C" - 400 -> "CD" - 500 -> "D" - 900 -> "CM" - 1000 -> "M" - else -> throw IllegalArgumentException("Invalid number") - } - assertEquals(expected, number.toRomanNumerals()) - } - - @Test - fun `should return complex roman numerals`() { - NumberExtTest::class.java.getResourceAsStream("/cases/roman/numerals.txt")!!.bufferedReader().useLines { lines -> - lines.forEach { line -> - val (number, expected) = line.split(" ") - assertEquals(expected, number.toInt().toRomanNumerals()) - } - } + @CsvFileSource(resources = ["/cases/roman/numerals.csv"]) + fun `should return complex roman numerals`(number: Int, expectedRoman: String) { + number.toRomanNumerals() shouldBe expectedRoman } } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/PersistentDataHolderTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/PersistentDataHolderTest.kt new file mode 100644 index 00000000..9c948bee --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/PersistentDataHolderTest.kt @@ -0,0 +1,23 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.utils.randomString +import io.mockk.mockk +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataHolder +import kotlin.test.Test +import kotlin.test.assertTrue + +class PersistentDataHolderTest { + + @Test + fun `open data container and manage it`() { + val container = mockk(randomString()) + val holder = PersistentDataHolder { container } + + val isEquals = holder.dataContainer { + this == container + } + assertTrue { isEquals } + } + +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/PlayerExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/PlayerExtTest.kt new file mode 100644 index 00000000..2cd81712 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/PlayerExtTest.kt @@ -0,0 +1,104 @@ +package com.github.rushyverse.api.extension + +import com.destroystokyo.paper.profile.PlayerProfile +import com.github.rushyverse.api.utils.randomBoolean +import com.github.rushyverse.api.utils.randomString +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.slot +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.PlayerInventory +import org.junit.jupiter.api.Nested +import kotlin.test.* + +class PlayerExtTest { + + @Nested + inner class Profile { + + @Test + fun `edit profile use the current profile and redefine it with the same modified instance`() { + val player = mockk(randomString()) + val profile = mockk(randomString()) + every { player.playerProfile } returns profile + + val slot = slot() + every { profile.complete(capture(slot)) } returns randomBoolean() + + val slotProfile = slot() + justRun { player.playerProfile = capture(slotProfile) } + + val expectedValue = randomBoolean() + player.editProfile { + complete(expectedValue) + } + + assertEquals(profile, slotProfile.captured) + assertEquals(expectedValue, slot.captured) + } + } + + @Nested + inner class ItemInHand { + + private lateinit var player: Player + private val inventory get() = player.inventory + + @BeforeTest + fun onBefore() { + player = mockk(randomString()) + val inventory = mockk() + every { player.inventory } returns inventory + } + + @Test + fun `compare with equals and item found`() { + val expectedItem = mockk(randomString()) + every { inventory.itemInMainHand } returns expectedItem + every { inventory.itemInOffHand } returns mockk(randomString()) + + assertTrue { player.itemInHand(expectedItem) } + + every { inventory.itemInMainHand } returns mockk(randomString()) + every { inventory.itemInOffHand } returns expectedItem + + assertTrue { player.itemInHand(expectedItem) } + } + + @Test + fun `compare with equals and item not found`() { + val expectedItem = mockk(randomString()) + every { inventory.itemInMainHand } returns mockk(randomString()) + every { inventory.itemInOffHand } returns mockk(randomString()) + + assertFalse { player.itemInHand(expectedItem) } + } + + @Test + fun `compare with lambda and item found`() { + val expectedItem = ItemStack(Material.ACACIA_DOOR) + every { inventory.itemInMainHand } returns expectedItem + every { inventory.itemInOffHand } returns ItemStack(Material.AIR) + + assertTrue { player.itemInHand { it.type == expectedItem.type } } + + every { inventory.itemInMainHand } returns ItemStack(Material.COOKED_BEEF) + every { inventory.itemInOffHand } returns expectedItem + + assertTrue { player.itemInHand { it.type == expectedItem.type } } + } + + @Test + fun `compare with lambda and item not found`() { + val expectedItem = ItemStack(Material.ACACIA_DOOR) + every { inventory.itemInMainHand } returns ItemStack(Material.SWEET_BERRIES) + every { inventory.itemInOffHand } returns ItemStack(Material.BLUE_WOOL) + + assertFalse { player.itemInHand { expectedItem.isSimilar(it) } } + } + } + +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/PlayerProfileExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/PlayerProfileExtTest.kt new file mode 100644 index 00000000..fc4fe957 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/PlayerProfileExtTest.kt @@ -0,0 +1,49 @@ +package com.github.rushyverse.api.extension + +import be.seeseemelk.mockbukkit.MockBukkit +import com.github.rushyverse.api.utils.randomString +import io.kotest.matchers.shouldBe +import org.bukkit.Bukkit +import java.util.* +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class PlayerProfileExtTest { + + @BeforeTest + fun onBefore() { + MockBukkit.mock() + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Test + fun `should define textures property`() { + val profile = Bukkit.createProfile(UUID.randomUUID()) + val skin = randomString() + val signature = randomString() + profile.setTextures(skin, signature) + + profile.properties.find { it.name == "textures" }!!.let { + it.value shouldBe skin + it.signature shouldBe signature + } + } + + @Test + fun `should get textures property`() { + val profile = Bukkit.createProfile(UUID.randomUUID()) + val skin = randomString() + val signature = randomString() + profile.setTextures(skin, signature) + + profile.getTexturesProperty()!!.let { + it.value shouldBe skin + it.signature shouldBe signature + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/PosExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/PosExtTest.kt deleted file mode 100644 index d3ec841d..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/extension/PosExtTest.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.github.rushyverse.api.extension - -import net.minestom.server.coordinate.Pos -import org.junit.jupiter.api.Nested -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class PosExtTest { - - @Nested - inner class CenterRelative { - - @Test - fun `should return the center relative to the other position with positive`() { - val pos = Pos(1.0, 2.0, 3.0) - val other = Pos(2.0, 3.0, 4.0) - val expected = Pos(1.5, 2.5, 3.5) - val actual = pos.centerRelative(other) - assertEquals(expected, actual) - } - - @Test - fun `should return the center relative to the other position with negative`() { - val pos = Pos(-1.0, -2.0, -3.0) - val other = Pos(-2.0, -3.0, -4.0) - val expected = Pos(-1.5, -2.5, -3.5) - val actual = pos.centerRelative(other) - assertEquals(expected, actual) - } - - @Test - fun `should return the center relative to the other position with mixed`() { - val pos = Pos(1.0, -2.0, 3.0) - val other = Pos(-2.0, 3.0, -4.0) - val expected = Pos(-0.5, 0.5, -0.5) - val actual = pos.centerRelative(other) - assertEquals(expected, actual) - } - } - - @Nested - inner class IsInCube { - - @Test - fun `should return true if the position is in the cube`() { - val min = Pos(0.0, 0.0, 0.0) - val max = Pos(10.0, 10.0, 10.0) - for (x in 0..10) { - for (y in 0..10) { - for (z in 0..10) { - val pos = Pos(x.toDouble(), y.toDouble(), z.toDouble()) - assertTrue { pos.isInCube(min, max) } - } - } - } - } - - @Test - fun `should return false if the position is not in the cube`() { - val min = Pos(0.0, 0.0, 0.0) - val max = Pos(10.0, 10.0, 10.0) - assertFalse { Pos(-0.1, 0.0, 0.0).isInCube(min, max) } - assertFalse { Pos(10.1, 0.0, 0.0).isInCube(min, max) } - assertFalse { Pos(0.0, -0.1, 0.0).isInCube(min, max) } - assertFalse { Pos(0.0, 10.1, 0.0).isInCube(min, max) } - assertFalse { Pos(0.0, 0.0, -0.1).isInCube(min, max) } - assertFalse { Pos(0.0, 0.0, 10.1).isInCube(min, max) } - - for (x in -10..-1) { - for (y in -10..-1) { - for (z in -10..-1) { - val pos = Pos(x.toDouble(), y.toDouble(), z.toDouble()) - assertFalse { pos.isInCube(min, max) } - } - } - } - - for (x in 11..20) { - for (y in 11..20) { - for (z in 11..20) { - val pos = Pos(x.toDouble(), y.toDouble(), z.toDouble()) - assertFalse { pos.isInCube(min, max) } - } - } - } - } - } - - @Nested - inner class IsInCylinder { - - @Test - fun `should return true if the position is in the cylinder`() { - val positionCylinder = Pos(0.0, 0.0, 0.0) - val radius = 10.0 - val limitY = 0.0..10.0 - for (x in -10..10) { - for (y in 0..10) { - val posX = Pos(x.toDouble(), y.toDouble(), 0.0) - assertTrue { posX.isInCylinder(positionCylinder, radius, limitY) } - - val posZ = Pos(0.0, y.toDouble(), x.toDouble()) - assertTrue { posZ.isInCylinder(positionCylinder, radius, limitY) } - } - } - } - - @Test - fun `should return false if the position is not in the cylinder`() { - val positionCylinder = Pos(0.0, 0.0, 0.0) - val radius = 10.0 - val limitY = 0.0..10.0 - assertFalse { Pos(-10.1, 0.0, 0.0).isInCylinder(positionCylinder, radius, limitY) } - assertFalse { Pos(10.1, 0.0, 0.0).isInCylinder(positionCylinder, radius, limitY) } - assertFalse { Pos(0.0, -0.1, 0.0).isInCylinder(positionCylinder, radius, limitY) } - assertFalse { Pos(0.0, 10.1, 0.0).isInCylinder(positionCylinder, radius, limitY) } - assertFalse { Pos(0.0, 0.0, -10.1).isInCylinder(positionCylinder, radius, limitY) } - assertFalse { Pos(0.0, 0.0, 10.1).isInCylinder(positionCylinder, radius, limitY) } - - for (x in -20..-11) { - for (y in -10..-1) { - val posX = Pos(x.toDouble(), y.toDouble(), 0.0) - assertFalse { posX.isInCylinder(positionCylinder, radius, limitY) } - - val posZ = Pos(0.0, y.toDouble(), x.toDouble()) - assertFalse { posZ.isInCylinder(positionCylinder, radius, limitY) } - } - } - - for (x in 11..20) { - for (y in 11..20) { - val posX = Pos(x.toDouble(), y.toDouble(), 0.0) - assertFalse { posX.isInCylinder(positionCylinder, radius, limitY) } - - val posZ = Pos(0.0, y.toDouble(), x.toDouble()) - assertFalse { posZ.isInCylinder(positionCylinder, radius, limitY) } - } - } - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/PropertyExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/PropertyExtTest.kt deleted file mode 100644 index c2767f5a..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/extension/PropertyExtTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.rushyverse.api.extension - -import com.github.rushyverse.api.utils.randomString -import net.minestom.server.network.packet.server.play.PlayerInfoPacket -import net.minestom.server.network.player.GameProfile -import kotlin.test.Test -import kotlin.test.assertEquals - -class PropertyExtTest { - - @Test - fun `should create a GameProfile property for textures`() { - val textures = randomString() - val signature = randomString() - val expected = GameProfile.Property("textures", textures, signature) - val actual = GameProfileTextureProperty(textures, signature) - assertEquals(expected, actual) - } - - @Test - fun `should create an AddPlayer property for textures`() { - val textures = randomString() - val signature = randomString() - val expected = PlayerInfoPacket.AddPlayer.Property("textures", textures, signature) - val actual = AddPlayerTextureProperty(textures, signature) - assertEquals(expected, actual) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/RunnableExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/RunnableExtTest.kt new file mode 100644 index 00000000..1bfc9ae0 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/RunnableExtTest.kt @@ -0,0 +1,17 @@ +package com.github.rushyverse.api.extension + +import kotlin.test.Test +import kotlin.test.assertTrue + +class RunnableExtTest { + + @Test + fun `create bukkit runnable instance`() { + var isCalled = false + val runnable = BukkitRunnable { + isCalled = true + } + runnable.run() + assertTrue { isCalled } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/StringExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/StringExtTest.kt index 16ee7ff7..89502ada 100644 --- a/src/test/kotlin/com/github/rushyverse/api/extension/StringExtTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/extension/StringExtTest.kt @@ -1,341 +1,260 @@ package com.github.rushyverse.api.extension +import com.github.rushyverse.api.utils.randomString +import io.kotest.matchers.shouldBe import net.kyori.adventure.text.Component -import net.kyori.adventure.text.TextComponent -import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.text.minimessage.MiniMessage import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import java.util.* import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotNull +import kotlin.test.assertNull class StringExtTest { - @Test - fun `default lore line length`() { - assertEquals(30, DEFAULT_LORE_LINE_LENGTH) - } - @Nested - inner class SequenceToLore { - - @Test - fun `should return empty component if sequence is empty`() { - assertEquals(emptyList(), emptySequence().toLore()) - } - - @Test - fun `should set color gray by default`() { - val components = sequenceOf("a", "b", "c").toLore() - assertEquals( - listOf( - Component.text().content("a").color(NamedTextColor.GRAY).build(), - Component.text().content("b").color(NamedTextColor.GRAY).build(), - Component.text().content("c").color(NamedTextColor.GRAY).build() - ), - components - ) - } + @DisplayName("Base64") + inner class Base64Test { @Test - fun `should return component with all strings`() { - val components = sequenceOf("Hello", "World").toLore() {} - assertEquals( - listOf( - Component.text().content("Hello").build(), - Component.text().content("World").build() - ), - components - ) + fun `encode string`() { + assertEquals("SGVsbG8gd29ybGQ=", "Hello world".encodeBase64ToString()) + assertEquals("w6LDr8O5LSo=", "âïù-*".encodeBase64ToString()) + assertEquals("2LXYqNin2K0g2KfZhNiu2YrYsQ==", "صباح الخير".encodeBase64ToString()) + assertEquals("44GT44KT44Gr44Gh44Gv", "こんにちは".encodeBase64ToString()) } @Test - fun `should return component with all strings and transform`() { - val components = sequenceOf("Hello", "World").toLore { color(NamedTextColor.RED) } - assertEquals( - listOf( - Component.text().content("Hello").color(NamedTextColor.RED).build(), - Component.text().content("World").color(NamedTextColor.RED).build() - ), - components - ) + fun `decode string`() { + assertEquals("Hello world", "SGVsbG8gd29ybGQ=".decodeBase64ToString()) + assertEquals("âïù-*", "w6LDr8O5LSo=".decodeBase64ToString()) + assertEquals("صباح الخير", "2LXYqNin2K0g2KfZhNiu2YrYsQ==".decodeBase64ToString()) + assertEquals("こんにちは", "44GT44KT44Gr44Gh44Gv".decodeBase64ToString()) } - } @Nested - inner class CollectionToLore { + @DisplayName("Conversion UUID") + inner class ConversionUUID { - @Test - fun `should return empty component if sequence is empty`() { - assertEquals(emptyList(), emptyList().toLore()) - } - - @Test - fun `should set color gray by default`() { - val components = listOf("a", "b", "c").toLore() - assertEquals( - listOf( - Component.text().content("a").color(NamedTextColor.GRAY).build(), - Component.text().content("b").color(NamedTextColor.GRAY).build(), - Component.text().content("c").color(NamedTextColor.GRAY).build() - ), - components - ) - } + @Nested + inner class Strict { - @Test - fun `should return component with all strings`() { - val components = listOf("Hello", "World").toLore() {} - assertEquals( - listOf( - Component.text().content("Hello").build(), - Component.text().content("World").build() - ), - components - ) - } + @Test + fun `can convert if the string is valid`() { + val uuid = UUID.randomUUID() + val string = uuid.toString() + assertEquals(uuid, string.toUUIDStrict()) + } - @Test - fun `should return component with all strings and transform`() { - val components = listOf("Hello", "World").toLore { color(NamedTextColor.RED) } - assertEquals( - listOf( - Component.text().content("Hello").color(NamedTextColor.RED).build(), - Component.text().content("World").color(NamedTextColor.RED).build() - ), - components + @ParameterizedTest + @ValueSource( + strings = [ + "", + "a", + "c7e4ca3236d942408e53de44bef8eeeb", + "c7e4ca32-36d9-4240-8e53-de44bef8eeeba" + ] ) - } - - } - - @Nested - inner class ToFormattedLore { + fun `throws exception if invalid`(value: String) { + assertThrows { + value.toUUIDStrict() + } + } - @Test - fun `should return an empty sequence if the string is empty`() { - assertEquals(emptyList(), "".toFormattedLore(10)) - } + @Test + fun `non null value when value can be converted`() { + val uuid = UUID.randomUUID() + val string = uuid.toString() + assertEquals(uuid, string.toUUIDStrictOrNull()) + } - @Test - fun `should cut sentence without space`() { - val sentence = "0123456789abcdef" - assertEquals( - listOf("0123-", "4567-", "89ab-", "cdef"), - sentence.toFormattedLore(5) + @ParameterizedTest + @ValueSource( + strings = [ + "", + "a", + "c7e4ca3236d942408e53de44bef8eeeb", + "c7e4ca32-36d9-4240-8e53-de44bef8eeeba" + ] ) - } - - @Test - fun `should create only one element if line length is greater than string size`() { - for (i in 1..100) { - val string = "a".repeat(i) - val sequence = string.toFormattedLore(i + 1) - assert(sequence.count() == 1) + fun `nulls if invalid`(value: String) { + assertNull(value.toUUIDStrictOrNull()) } - } - - @Test - fun `should create only one element if line length is equals to the string size`() { - val sentence = "Hello World" - assertEquals(listOf(sentence), sentence.toFormattedLore(sentence.length)) - } - - @Test - fun `should create multiple elements by cut on the space char`() { - val sequence = "Hello World".toFormattedLore(5) - assertEquals(listOf("Hello", "World"), sequence) - } - - @Test - fun `should create multiple elements by cut on the line length char adding a '-'`() { - val sequence1 = "Hello World".toFormattedLore(4) - assertEquals(listOf("Hel-", "lo", "Wor-", "ld"), sequence1) - val sequence2 = "Hello World".toFormattedLore(3) - assertEquals(listOf("He-", "llo", "Wo-", "rld"), sequence2) } - @Test - fun `should create multiple element by cut on the previous space char`() { - // Indexes of "W" to "d" chars - for (i in 6..10) { - assertEquals(listOf("Hello", "World"), "Hello World".toFormattedLore(i)) - } - } - - @Test - fun `should create multiple element with long sentence`() { - assertEquals( - listOf("This is a tool", "to create a", "game"), - "This is a tool to create a game".toFormattedLore(15) - ) + @Nested + inner class NoStrict { - assertEquals( - listOf("This is a tool", "to create a", "game0123456789-", "0123456789"), - "This is a tool to create a game01234567890123456789".toFormattedLore(15) + @ParameterizedTest + @ValueSource( + strings = [ + "c7e4ca3236d942408e53de44bef8eeeb", + "c7e4ca32-36d9-4240-8e53-de44bef8eeeb" + ] ) + fun `can convert if the string is valid`(value: String) { + val uuid = UUID.fromString("c7e4ca32-36d9-4240-8e53-de44bef8eeeb") + assertEquals(uuid, value.toUUID()) + } - assertEquals( - listOf("Ajoutez, chattez et rejoignez", "vos amis à travers le serveur"), - "Ajoutez, chattez et rejoignez vos amis à travers le serveur".toFormattedLore(30), + @ParameterizedTest + @ValueSource( + strings = [ + "", + "a", + "c7e4ca3236d942408e53de44bef8eeeba", + "c7e4ca32-36d9-4240-8e53-de44bef8eeeba" + ] ) + fun `throws exception if invalid`(value: String) { + assertThrows { + value.toUUID() + } + } - assertEquals( - listOf("Ajoutez, chattez et rejoignez", "v"), - "Ajoutez, chattez et rejoignez v".toFormattedLore(30), - ) + @ParameterizedTest + @ValueSource(strings = ["c7e4ca3236d942408e53de44bef8eeeb", "c7e4ca32-36d9-4240-8e53-de44bef8eeeb"]) + fun `non null value when value can be converted`(value: String) { + val uuid = UUID.fromString("c7e4ca32-36d9-4240-8e53-de44bef8eeeb") + assertEquals(uuid, value.toUUIDOrNull()) + } - assertEquals( - listOf("Ajoutez, chattez et rejoignez", " "), - "Ajoutez, chattez et rejoignez ".toFormattedLore(30), + @ParameterizedTest + @ValueSource( + strings = [ + "", + "a", + "c7e4ca3236d942408e53de44bef8eeeba", + "c7e4ca32-36d9-4240-8e53-de44bef8eeeba" + ] ) + fun `nulls if invalid`(value: String) { + assertNull(value.toUUIDStrictOrNull()) + } - assertEquals( - listOf("Add, chat and join your friends through", "the server"), - "Add, chat and join your friends through the server".toFormattedLore(40), - ) } + } @Nested - inner class ToFormattedLoreSequence { - - @Test - fun `should return an empty sequence if the string is empty`() { - assertEquals(emptySequence(), "".toFormattedLoreSequence(10)) + inner class WithColor { + + @ParameterizedTest + @ValueSource( + strings = [ + "", + " ", + "red" + ] + ) + fun `should wrap non empty string`(value: String) { + val string = randomString() + string withColor value shouldBe "<$value>$string" } - @Test - fun `should cut sentence without space`() { - val sentence = "0123456789abcdef" - assertEquals( - listOf("0123-", "4567-", "89ab-", "cdef"), - sentence.toFormattedLoreSequence(5).toList() - ) + @ParameterizedTest + @ValueSource( + strings = [ + "", + " ", + "red" + ] + ) + fun `should wrap empty string`(value: String) { + "" withColor value shouldBe "<$value>" } - @Test - fun `should create only one element if line length is greater than string size`() { - for (i in 1..100) { - val string = "a".repeat(i) - val sequence = string.toFormattedLoreSequence(i + 1) - assert(sequence.count() == 1) - } - } + } + + @Nested + inner class AsComponent { @Test - fun `should create only one element if line length is equals to the string size`() { - val sentence = "Hello World" - assertEquals(listOf(sentence), sentence.toFormattedLoreSequence(sentence.length).toList()) + fun `should transform non empty string`() { + val string = randomString() + string.asComponent() shouldBe Component.text(string) } @Test - fun `should create multiple elements by cut on the space char`() { - val sequence = "Hello World".toFormattedLoreSequence(5).toList() - assertEquals(listOf("Hello", "World"), sequence) + fun `should transform empty string`() { + "".asComponent() shouldBe Component.empty() } @Test - fun `should create multiple elements by cut on the line length char adding a '-'`() { - val sequence1 = "Hello World".toFormattedLoreSequence(4).toList() - assertEquals(listOf("Hel-", "lo", "Wor-", "ld"), sequence1) - - val sequence2 = "Hello World".toFormattedLoreSequence(3).toList() - assertEquals(listOf("He-", "llo", "Wo-", "rld"), sequence2) + fun `should read mini message tag`() { + val string = "hello" + string.asComponent() shouldBe Component.text("hello").color(NamedTextColor.RED) + .decorate(TextDecoration.BOLD) } @Test - fun `should create multiple element by cut on the previous space char`() { - // Indexes of "W" to "d" chars - for (i in 6..10) { - assertEquals(listOf("Hello", "World"), "Hello World".toFormattedLoreSequence(i).toList()) - } + fun `should use tag if defined`() { + val string = "hello" + string.asComponent( + Placeholder.parsed("test", "myvalue") + ) shouldBe Component.text("myvaluehello").color(NamedTextColor.RED) } @Test - fun `should create multiple element with long sentence`() { - assertEquals( - listOf("This is a tool", "to create a", "game"), - "This is a tool to create a game".toFormattedLoreSequence(15).toList() - ) - - assertEquals( - listOf("This is a tool", "to create a", "game0123456789-", "0123456789"), - "This is a tool to create a game01234567890123456789".toFormattedLoreSequence(15).toList() - ) - - assertEquals( - listOf("Ajoutez, chattez et rejoignez", "vos amis à travers le serveur"), - "Ajoutez, chattez et rejoignez vos amis à travers le serveur".toFormattedLoreSequence(30).toList(), - ) - - assertEquals( - listOf("Ajoutez, chattez et rejoignez", "v"), - "Ajoutez, chattez et rejoignez v".toFormattedLoreSequence(30).toList(), - ) - - assertEquals( - listOf("Ajoutez, chattez et rejoignez", " "), - "Ajoutez, chattez et rejoignez ".toFormattedLoreSequence(30).toList(), - ) - - assertEquals( - listOf("Add, chat and join your friends through", "the server"), - "Add, chat and join your friends through the server".toFormattedLoreSequence(40).toList(), - ) + fun `should use custom instance of mini message`() { + val string = "hello" + + val miniMessage = MiniMessage.builder() + .tags( + TagResolver.resolver( + Placeholder.parsed("test", "myvalue") + ) + ) + .build() + + string.asComponent(miniMessage = miniMessage) shouldBe Component.text("myvaluehello") } + } @Nested - inner class AsMiniComponent { + inner class StringBuilderDeleteLast { @Test - fun `should return simple component text with simple string`() { - val string = "Hello World!" - val component = string.asMiniComponent() - component as TextComponent - assertEquals("Hello World!", component.content()) + fun `should throw if size is under 0`() { + assertThrows { + StringBuilder().deleteLast(-1) + } } @Test - fun `should return component text with color`() { - val string = "Hello World!" - val component = string.asMiniComponent() - component as TextComponent - - assertEquals("Hello World!", component.content()) - - val color = component.color() - assertNotNull(color) - assertEquals("#ff5555", color.asHexString()) + fun `should return the same string builder if size is 0`() { + val builder = StringBuilder().append(randomString()) + builder.deleteLast(0) shouldBe builder } - @Test - fun `should return component with click event`() { - val string = "Click me!" - val component = string.asMiniComponent() - component as TextComponent - - assertEquals("Click me!", component.content()) - val clickEvent = component.clickEvent() - assertNotNull(clickEvent) - assertEquals(ClickEvent.Action.RUN_COMMAND, clickEvent.action()) - assertEquals("/test", clickEvent.value()) + @Test + fun `should return the same string builder if size is bigger than length`() { + val string = randomString() + val builder = StringBuilder().append(string) + builder.deleteLast(string.length) + builder.length shouldBe 0 } - @Test - fun `should return component with custom tag`() { - val string = "Hello !" - val component = string.asMiniComponent( - Placeholder.unparsed("test", "my test") - ) - component as TextComponent - assertEquals("Hello my test!", component.content()) + @ParameterizedTest + @ValueSource(ints = [1, 2, 3, 4, 5, 6, 7, 8, 9]) + fun `should delete last char`(size: Int) { + val string = randomString() + val builder = StringBuilder().append(string) + builder.deleteLast(size) + builder.toString() shouldBe string.dropLast(size) } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/VillagerExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/VillagerExtTest.kt new file mode 100644 index 00000000..3d014a46 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/VillagerExtTest.kt @@ -0,0 +1,87 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.utils.randomString +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.slot +import org.bukkit.NamespacedKey +import org.bukkit.entity.Villager +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataType +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import kotlin.test.* + +class VillagerExtTest { + + private lateinit var plugin: com.github.rushyverse.api.Plugin + + @BeforeTest + fun onBefore() { + plugin = mockk() + every { plugin.name } returns "test" + } + + @Nested + @DisplayName("Keep profession") + inner class KeepProfession { + + @Test + fun `returns true when data is present`() { + val villager = mockk(randomString()) + val container = mockk() + + val slotNamespaced = slot() + every { container.get(capture(slotNamespaced), PersistentDataType.BYTE) } returns 0.toByte() + every { villager.persistentDataContainer } returns container + assertTrue { villager.keepProfession(plugin) } + assertEquals(namespacedKeyKeepJob(plugin), slotNamespaced.captured) + } + + @Test + fun `returns false when data is not present`() { + val villager = mockk(randomString()) + val container = mockk() + + val slotNamespaced = slot() + every { container.get(capture(slotNamespaced), PersistentDataType.BYTE) } returns null + every { villager.persistentDataContainer } returns container + assertFalse { villager.keepProfession(plugin) } + assertEquals(namespacedKeyKeepJob(plugin), slotNamespaced.captured) + } + + } + + @Nested + @DisplayName("Set keep profession") + inner class SetKeepProfession { + + @Test + fun `when true, set key into the data container`() { + val villager = mockk(randomString()) + val container = mockk() + + val slotNamespaced = slot() + justRun { container.set(capture(slotNamespaced), PersistentDataType.BYTE, 0) } + every { villager.persistentDataContainer } returns container + + villager.keepProfession(plugin, true) + assertEquals(namespacedKeyKeepJob(plugin), slotNamespaced.captured) + } + + @Test + fun `when false, remove key into the data container`() { + val villager = mockk(randomString()) + val container = mockk() + + val slotNamespaced = slot() + justRun { container.remove(capture(slotNamespaced)) } + every { villager.persistentDataContainer } returns container + + villager.keepProfession(plugin, false) + assertEquals(namespacedKeyKeepJob(plugin), slotNamespaced.captured) + } + + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/WorldExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/WorldExtTest.kt new file mode 100644 index 00000000..98d738c1 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/WorldExtTest.kt @@ -0,0 +1,95 @@ +package com.github.rushyverse.api.extension + +import com.github.rushyverse.api.utils.randomLocation +import com.github.rushyverse.api.utils.randomString +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.runBlocking +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.Block +import java.util.concurrent.CompletableFuture +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class WorldExtTest { + + @Test + fun `await chunk at with block`() = runBlocking { + val world = mockk(randomString()) + val slotBlock = slot() + val slotGen = slot() + + val chunk = mockk(randomString()) + every { world.getChunkAtAsync(capture(slotBlock), capture(slotGen)) } returns CompletableFuture.completedFuture( + chunk + ) + + val block = mockk(randomString()) + assertEquals(chunk, world.awaitChunkAt(block, true)) + assertEquals(block, slotBlock.captured) + assertTrue { slotGen.captured } + assertEquals(chunk, world.awaitChunkAt(block, false)) + assertFalse { slotGen.captured } + } + + @Test + fun `await chunk at with location`() = runBlocking { + val world = mockk(randomString()) + val slotLoc = slot() + val slotGen = slot() + + val chunk = mockk(randomString()) + every { world.getChunkAtAsync(capture(slotLoc), capture(slotGen)) } returns CompletableFuture.completedFuture( + chunk + ) + + val location = randomLocation() + assertEquals(chunk, world.awaitChunkAt(location, true)) + assertEquals(location, slotLoc.captured) + assertTrue { slotGen.captured } + assertEquals(chunk, world.awaitChunkAt(location, false)) + assertFalse { slotGen.captured } + } + + @Test + fun `await chunk at with coord`() = runBlocking { + val world = mockk(randomString()) + val slotX = slot() + val slotZ = slot() + val slotGen = slot() + val slotUrgent = slot() + + val chunk = mockk(randomString()) + every { + world.getChunkAtAsync( + capture(slotX), + capture(slotZ), + capture(slotGen), + capture(slotUrgent) + ) + } returns CompletableFuture.completedFuture( + chunk + ) + + var x = 10 + var z = 20 + assertEquals(chunk, world.awaitChunkAt(x, z, gen = true, urgent = true)) + assertEquals(x, slotX.captured) + assertEquals(z, slotZ.captured) + assertTrue { slotGen.captured } + assertTrue { slotUrgent.captured } + + x = 42 + z = 64 + assertEquals(chunk, world.awaitChunkAt(x, z, gen = false, urgent = false)) + assertEquals(x, slotX.captured) + assertEquals(z, slotZ.captured) + assertFalse { slotGen.captured } + assertFalse { slotUrgent.captured } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/extension/event/CancellableExtTest.kt b/src/test/kotlin/com/github/rushyverse/api/extension/event/CancellableExtTest.kt new file mode 100644 index 00000000..76a45d62 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/extension/event/CancellableExtTest.kt @@ -0,0 +1,25 @@ +package com.github.rushyverse.api.extension.event + +import io.kotest.matchers.shouldBe +import org.bukkit.event.Cancellable +import kotlin.test.Test + +class CancellableExtTest { + + @Test + fun `cancel() sets isCancelled to true`() { + var setCancel: Boolean? = null + val cancellable = object : Cancellable { + override fun isCancelled(): Boolean { + error("Should not be called") + } + + override fun setCancelled(cancel: Boolean) { + setCancel = cancel + } + + } + cancellable.cancel() + setCancel shouldBe true + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt deleted file mode 100644 index abd40b28..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/image/MapImageMathTest.kt +++ /dev/null @@ -1,806 +0,0 @@ -package com.github.rushyverse.api.image - -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class MapImageMathTest { - - @Nested - inner class Up { - - private lateinit var instance: MapImageMath - - @BeforeTest - fun onBefore() { - instance = MapImageMath.Up - } - - @Test - fun `should have the correct yaw`() { - assertEquals(0f, instance.yaw) - } - - @Test - fun `should have the correct pitch`() { - assertEquals(270f, instance.pitch) - } - - @Nested - inner class ComputeX { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct x with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeX(begin, 0, itemFramesPerLine)) - assertEquals(1, instance.computeX(begin, 1, itemFramesPerLine)) - assertEquals(2, instance.computeX(begin, 2, itemFramesPerLine)) - - assertEquals(0, instance.computeX(begin, 3, itemFramesPerLine)) - assertEquals(1, instance.computeX(begin, 4, itemFramesPerLine)) - assertEquals(2, instance.computeX(begin, 5, itemFramesPerLine)) - } - - @Test - fun `should compute the correct x with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeX(begin, 0, itemFramesPerLine)) - assertEquals(6, instance.computeX(begin, 1, itemFramesPerLine)) - assertEquals(7, instance.computeX(begin, 2, itemFramesPerLine)) - - assertEquals(5, instance.computeX(begin, 3, itemFramesPerLine)) - assertEquals(6, instance.computeX(begin, 4, itemFramesPerLine)) - assertEquals(7, instance.computeX(begin, 5, itemFramesPerLine)) - } - - } - - @Nested - inner class ComputeY { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) - } - } - - @ParameterizedTest - @ValueSource(ints = [-1, 0, 1]) - fun `should stay on the same y`(beginY: Int) { - repeat(10) { - assertEquals(0, instance.computeY(0, it, 5)) - } - } - - } - - @Nested - inner class ComputeZ { - - @Test - fun `should throws exception with no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct z with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeZ(begin, 0, itemFramesPerLine)) - assertEquals(0, instance.computeZ(begin, 1, itemFramesPerLine)) - assertEquals(0, instance.computeZ(begin, 2, itemFramesPerLine)) - - assertEquals(1, instance.computeZ(begin, 3, itemFramesPerLine)) - assertEquals(1, instance.computeZ(begin, 4, itemFramesPerLine)) - assertEquals(1, instance.computeZ(begin, 5, itemFramesPerLine)) - - assertEquals(2, instance.computeZ(begin, 6, itemFramesPerLine)) - assertEquals(2, instance.computeZ(begin, 7, itemFramesPerLine)) - assertEquals(2, instance.computeZ(begin, 8, itemFramesPerLine)) - } - - @Test - fun `should compute the correct z with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeZ(begin, 0, itemFramesPerLine)) - assertEquals(5, instance.computeZ(begin, 1, itemFramesPerLine)) - assertEquals(5, instance.computeZ(begin, 2, itemFramesPerLine)) - - assertEquals(6, instance.computeZ(begin, 3, itemFramesPerLine)) - assertEquals(6, instance.computeZ(begin, 4, itemFramesPerLine)) - assertEquals(6, instance.computeZ(begin, 5, itemFramesPerLine)) - - assertEquals(7, instance.computeZ(begin, 6, itemFramesPerLine)) - assertEquals(7, instance.computeZ(begin, 7, itemFramesPerLine)) - assertEquals(7, instance.computeZ(begin, 8, itemFramesPerLine)) - } - - } - } - - @Nested - inner class Down { - - private lateinit var instance: MapImageMath - - @BeforeTest - fun onBefore() { - instance = MapImageMath.Down - } - - @Test - fun `should have the correct yaw`() { - assertEquals(0f, instance.yaw) - } - - @Test - fun `should have the correct pitch`() { - assertEquals(90f, instance.pitch) - } - - @Nested - inner class ComputeX { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct x with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeX(begin, 0, itemFramesPerLine)) - assertEquals(1, instance.computeX(begin, 1, itemFramesPerLine)) - assertEquals(2, instance.computeX(begin, 2, itemFramesPerLine)) - - assertEquals(0, instance.computeX(begin, 3, itemFramesPerLine)) - assertEquals(1, instance.computeX(begin, 4, itemFramesPerLine)) - assertEquals(2, instance.computeX(begin, 5, itemFramesPerLine)) - } - - @Test - fun `should compute the correct x with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeX(begin, 0, itemFramesPerLine)) - assertEquals(6, instance.computeX(begin, 1, itemFramesPerLine)) - assertEquals(7, instance.computeX(begin, 2, itemFramesPerLine)) - - assertEquals(5, instance.computeX(begin, 3, itemFramesPerLine)) - assertEquals(6, instance.computeX(begin, 4, itemFramesPerLine)) - assertEquals(7, instance.computeX(begin, 5, itemFramesPerLine)) - } - - } - - @Nested - inner class ComputeY { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) - } - } - - @ParameterizedTest - @ValueSource(ints = [-1, 0, 1]) - fun `should stay on the same y`(beginY: Int) { - repeat(10) { - assertEquals(0, instance.computeY(0, it, 5)) - } - } - - } - - @Nested - inner class ComputeZ { - - @Test - fun `should throws exception with no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct z with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeZ(begin, 0, itemFramesPerLine)) - assertEquals(0, instance.computeZ(begin, 1, itemFramesPerLine)) - assertEquals(0, instance.computeZ(begin, 2, itemFramesPerLine)) - - assertEquals(-1, instance.computeZ(begin, 3, itemFramesPerLine)) - assertEquals(-1, instance.computeZ(begin, 4, itemFramesPerLine)) - assertEquals(-1, instance.computeZ(begin, 5, itemFramesPerLine)) - - assertEquals(-2, instance.computeZ(begin, 6, itemFramesPerLine)) - assertEquals(-2, instance.computeZ(begin, 7, itemFramesPerLine)) - assertEquals(-2, instance.computeZ(begin, 8, itemFramesPerLine)) - } - - @Test - fun `should compute the correct z with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeZ(begin, 0, itemFramesPerLine)) - assertEquals(5, instance.computeZ(begin, 1, itemFramesPerLine)) - assertEquals(5, instance.computeZ(begin, 2, itemFramesPerLine)) - - assertEquals(4, instance.computeZ(begin, 3, itemFramesPerLine)) - assertEquals(4, instance.computeZ(begin, 4, itemFramesPerLine)) - assertEquals(4, instance.computeZ(begin, 5, itemFramesPerLine)) - - assertEquals(3, instance.computeZ(begin, 6, itemFramesPerLine)) - assertEquals(3, instance.computeZ(begin, 7, itemFramesPerLine)) - assertEquals(3, instance.computeZ(begin, 8, itemFramesPerLine)) - } - - } - - } - - @Nested - inner class North { - - private lateinit var instance: MapImageMath - - @BeforeTest - fun onBefore() { - instance = MapImageMath.North - } - - @Test - fun `should have the correct yaw`() { - assertEquals(180f, instance.yaw) - } - - @Test - fun `should have the correct pitch`() { - assertEquals(0f, instance.pitch) - } - - @Nested - inner class ComputeX { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct x with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeX(begin, 0, itemFramesPerLine)) - assertEquals(-1, instance.computeX(begin, 1, itemFramesPerLine)) - assertEquals(-2, instance.computeX(begin, 2, itemFramesPerLine)) - - assertEquals(0, instance.computeX(begin, 3, itemFramesPerLine)) - assertEquals(-1, instance.computeX(begin, 4, itemFramesPerLine)) - assertEquals(-2, instance.computeX(begin, 5, itemFramesPerLine)) - } - - @Test - fun `should compute the correct x with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeX(begin, 0, itemFramesPerLine)) - assertEquals(4, instance.computeX(begin, 1, itemFramesPerLine)) - assertEquals(3, instance.computeX(begin, 2, itemFramesPerLine)) - - assertEquals(5, instance.computeX(begin, 3, itemFramesPerLine)) - assertEquals(4, instance.computeX(begin, 4, itemFramesPerLine)) - assertEquals(3, instance.computeX(begin, 5, itemFramesPerLine)) - } - - } - - @Nested - inner class ComputeY { - - @Test - fun `should throws exception with no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct y with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeY(begin, 0, itemFramesPerLine)) - assertEquals(0, instance.computeY(begin, 1, itemFramesPerLine)) - assertEquals(0, instance.computeY(begin, 2, itemFramesPerLine)) - - assertEquals(-1, instance.computeY(begin, 3, itemFramesPerLine)) - assertEquals(-1, instance.computeY(begin, 4, itemFramesPerLine)) - assertEquals(-1, instance.computeY(begin, 5, itemFramesPerLine)) - - assertEquals(-2, instance.computeY(begin, 6, itemFramesPerLine)) - assertEquals(-2, instance.computeY(begin, 7, itemFramesPerLine)) - assertEquals(-2, instance.computeY(begin, 8, itemFramesPerLine)) - } - - @Test - fun `should compute the correct y with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeY(begin, 0, itemFramesPerLine)) - assertEquals(5, instance.computeY(begin, 1, itemFramesPerLine)) - assertEquals(5, instance.computeY(begin, 2, itemFramesPerLine)) - - assertEquals(4, instance.computeY(begin, 3, itemFramesPerLine)) - assertEquals(4, instance.computeY(begin, 4, itemFramesPerLine)) - assertEquals(4, instance.computeY(begin, 5, itemFramesPerLine)) - - assertEquals(3, instance.computeY(begin, 6, itemFramesPerLine)) - assertEquals(3, instance.computeY(begin, 7, itemFramesPerLine)) - assertEquals(3, instance.computeY(begin, 8, itemFramesPerLine)) - } - } - - @Nested - inner class ComputeZ { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) - } - } - - @ParameterizedTest - @ValueSource(ints = [-1, 0, 1]) - fun `should stay on the same z`(beginZ: Int) { - repeat(10) { - assertEquals(0, instance.computeZ(0, it, 5)) - } - } - - } - - } - - @Nested - inner class South { - - private lateinit var instance: MapImageMath - - @BeforeTest - fun onBefore() { - instance = MapImageMath.South - } - - @Test - fun `should have the correct yaw`() { - assertEquals(0f, instance.yaw) - } - - @Test - fun `should have the correct pitch`() { - assertEquals(0f, instance.pitch) - } - - @Nested - inner class ComputeX { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct x with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeX(begin, 0, itemFramesPerLine)) - assertEquals(1, instance.computeX(begin, 1, itemFramesPerLine)) - assertEquals(2, instance.computeX(begin, 2, itemFramesPerLine)) - - assertEquals(0, instance.computeX(begin, 3, itemFramesPerLine)) - assertEquals(1, instance.computeX(begin, 4, itemFramesPerLine)) - assertEquals(2, instance.computeX(begin, 5, itemFramesPerLine)) - } - - @Test - fun `should compute the correct x with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeX(begin, 0, itemFramesPerLine)) - assertEquals(6, instance.computeX(begin, 1, itemFramesPerLine)) - assertEquals(7, instance.computeX(begin, 2, itemFramesPerLine)) - - assertEquals(5, instance.computeX(begin, 3, itemFramesPerLine)) - assertEquals(6, instance.computeX(begin, 4, itemFramesPerLine)) - assertEquals(7, instance.computeX(begin, 5, itemFramesPerLine)) - } - - } - - @Nested - inner class ComputeY { - - @Test - fun `should throws exception with no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct y with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeY(begin, 0, itemFramesPerLine)) - assertEquals(0, instance.computeY(begin, 1, itemFramesPerLine)) - assertEquals(0, instance.computeY(begin, 2, itemFramesPerLine)) - - assertEquals(-1, instance.computeY(begin, 3, itemFramesPerLine)) - assertEquals(-1, instance.computeY(begin, 4, itemFramesPerLine)) - assertEquals(-1, instance.computeY(begin, 5, itemFramesPerLine)) - - assertEquals(-2, instance.computeY(begin, 6, itemFramesPerLine)) - assertEquals(-2, instance.computeY(begin, 7, itemFramesPerLine)) - assertEquals(-2, instance.computeY(begin, 8, itemFramesPerLine)) - } - - @Test - fun `should compute the correct y with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeY(begin, 0, itemFramesPerLine)) - assertEquals(5, instance.computeY(begin, 1, itemFramesPerLine)) - assertEquals(5, instance.computeY(begin, 2, itemFramesPerLine)) - - assertEquals(4, instance.computeY(begin, 3, itemFramesPerLine)) - assertEquals(4, instance.computeY(begin, 4, itemFramesPerLine)) - assertEquals(4, instance.computeY(begin, 5, itemFramesPerLine)) - - assertEquals(3, instance.computeY(begin, 6, itemFramesPerLine)) - assertEquals(3, instance.computeY(begin, 7, itemFramesPerLine)) - assertEquals(3, instance.computeY(begin, 8, itemFramesPerLine)) - } - } - - @Nested - inner class ComputeZ { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) - } - } - - @ParameterizedTest - @ValueSource(ints = [-1, 0, 1]) - fun `should stay on the same z`(beginZ: Int) { - repeat(10) { - assertEquals(0, instance.computeZ(0, it, 5)) - } - } - } - } - - @Nested - inner class West { - - private lateinit var instance: MapImageMath - - @BeforeTest - fun onBefore() { - instance = MapImageMath.West - } - - @Test - fun `should have the correct yaw`() { - assertEquals(90f, instance.yaw) - } - - @Test - fun `should have the correct pitch`() { - assertEquals(0f, instance.pitch) - } - - @Nested - inner class ComputeX { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) - } - } - - @ParameterizedTest - @ValueSource(ints = [-1, 0, 1]) - fun `should stay on the same x`(beginZ: Int) { - repeat(10) { - assertEquals(0, instance.computeX(0, it, 5)) - } - } - - } - - @Nested - inner class ComputeY { - - @Test - fun `should throws exception with no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct y with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeY(begin, 0, itemFramesPerLine)) - assertEquals(0, instance.computeY(begin, 1, itemFramesPerLine)) - assertEquals(0, instance.computeY(begin, 2, itemFramesPerLine)) - - assertEquals(-1, instance.computeY(begin, 3, itemFramesPerLine)) - assertEquals(-1, instance.computeY(begin, 4, itemFramesPerLine)) - assertEquals(-1, instance.computeY(begin, 5, itemFramesPerLine)) - - assertEquals(-2, instance.computeY(begin, 6, itemFramesPerLine)) - assertEquals(-2, instance.computeY(begin, 7, itemFramesPerLine)) - assertEquals(-2, instance.computeY(begin, 8, itemFramesPerLine)) - } - - @Test - fun `should compute the correct y with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeY(begin, 0, itemFramesPerLine)) - assertEquals(5, instance.computeY(begin, 1, itemFramesPerLine)) - assertEquals(5, instance.computeY(begin, 2, itemFramesPerLine)) - - assertEquals(4, instance.computeY(begin, 3, itemFramesPerLine)) - assertEquals(4, instance.computeY(begin, 4, itemFramesPerLine)) - assertEquals(4, instance.computeY(begin, 5, itemFramesPerLine)) - - assertEquals(3, instance.computeY(begin, 6, itemFramesPerLine)) - assertEquals(3, instance.computeY(begin, 7, itemFramesPerLine)) - assertEquals(3, instance.computeY(begin, 8, itemFramesPerLine)) - } - } - - @Nested - inner class ComputeZ { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct z with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeZ(begin, 0, itemFramesPerLine)) - assertEquals(1, instance.computeZ(begin, 1, itemFramesPerLine)) - assertEquals(2, instance.computeZ(begin, 2, itemFramesPerLine)) - - assertEquals(0, instance.computeZ(begin, 3, itemFramesPerLine)) - assertEquals(1, instance.computeZ(begin, 4, itemFramesPerLine)) - assertEquals(2, instance.computeZ(begin, 5, itemFramesPerLine)) - } - - @Test - fun `should compute the correct z with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeZ(begin, 0, itemFramesPerLine)) - assertEquals(6, instance.computeZ(begin, 1, itemFramesPerLine)) - assertEquals(7, instance.computeZ(begin, 2, itemFramesPerLine)) - - assertEquals(5, instance.computeZ(begin, 3, itemFramesPerLine)) - assertEquals(6, instance.computeZ(begin, 4, itemFramesPerLine)) - assertEquals(7, instance.computeZ(begin, 5, itemFramesPerLine)) - } - } - } - - @Nested - inner class East { - - private lateinit var instance: MapImageMath - - @BeforeTest - fun onBefore() { - instance = MapImageMath.East - } - - @Test - fun `should have the correct yaw`() { - assertEquals(270f, instance.yaw) - } - - @Test - fun `should have the correct pitch`() { - assertEquals(0f, instance.pitch) - } - - @Nested - inner class ComputeX { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertEquals(0, instance.computeX(0, it, itemFramesPerLine = 0)) - } - } - - @ParameterizedTest - @ValueSource(ints = [-1, 0, 1]) - fun `should stay on the same x`(beginZ: Int) { - repeat(10) { - assertEquals(0, instance.computeX(0, it, 5)) - } - } - - } - - @Nested - inner class ComputeY { - - @Test - fun `should throws exception with no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeY(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct y with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeY(begin, 0, itemFramesPerLine)) - assertEquals(0, instance.computeY(begin, 1, itemFramesPerLine)) - assertEquals(0, instance.computeY(begin, 2, itemFramesPerLine)) - - assertEquals(-1, instance.computeY(begin, 3, itemFramesPerLine)) - assertEquals(-1, instance.computeY(begin, 4, itemFramesPerLine)) - assertEquals(-1, instance.computeY(begin, 5, itemFramesPerLine)) - - assertEquals(-2, instance.computeY(begin, 6, itemFramesPerLine)) - assertEquals(-2, instance.computeY(begin, 7, itemFramesPerLine)) - assertEquals(-2, instance.computeY(begin, 8, itemFramesPerLine)) - } - - @Test - fun `should compute the correct y with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeY(begin, 0, itemFramesPerLine)) - assertEquals(5, instance.computeY(begin, 1, itemFramesPerLine)) - assertEquals(5, instance.computeY(begin, 2, itemFramesPerLine)) - - assertEquals(4, instance.computeY(begin, 3, itemFramesPerLine)) - assertEquals(4, instance.computeY(begin, 4, itemFramesPerLine)) - assertEquals(4, instance.computeY(begin, 5, itemFramesPerLine)) - - assertEquals(3, instance.computeY(begin, 6, itemFramesPerLine)) - assertEquals(3, instance.computeY(begin, 7, itemFramesPerLine)) - assertEquals(3, instance.computeY(begin, 8, itemFramesPerLine)) - } - - } - - @Nested - inner class ComputeZ { - - @Test - fun `should always return 0 if no element per line`() { - repeat(10) { - assertThrows { - assertEquals(0, instance.computeZ(0, it, itemFramesPerLine = 0)) - } - } - } - - @Test - fun `should compute the correct z with begin equals to 0`() { - val begin = 0 - val itemFramesPerLine = 3 - - assertEquals(0, instance.computeZ(begin, 0, itemFramesPerLine)) - assertEquals(-1, instance.computeZ(begin, 1, itemFramesPerLine)) - assertEquals(-2, instance.computeZ(begin, 2, itemFramesPerLine)) - - assertEquals(0, instance.computeZ(begin, 3, itemFramesPerLine)) - assertEquals(-1, instance.computeZ(begin, 4, itemFramesPerLine)) - assertEquals(-2, instance.computeZ(begin, 5, itemFramesPerLine)) - } - - @Test - fun `should compute the correct z with begin greater than 0`() { - val begin = 5 - val itemFramesPerLine = 3 - - assertEquals(5, instance.computeZ(begin, 0, itemFramesPerLine)) - assertEquals(4, instance.computeZ(begin, 1, itemFramesPerLine)) - assertEquals(3, instance.computeZ(begin, 2, itemFramesPerLine)) - - assertEquals(5, instance.computeZ(begin, 3, itemFramesPerLine)) - assertEquals(4, instance.computeZ(begin, 4, itemFramesPerLine)) - assertEquals(3, instance.computeZ(begin, 5, itemFramesPerLine)) - } - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt b/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt deleted file mode 100644 index 33c541b5..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/image/MapImageTest.kt +++ /dev/null @@ -1,722 +0,0 @@ -package com.github.rushyverse.api.image - -import com.github.rushyverse.api.image.exception.ImageAlreadyLoadedException -import com.github.rushyverse.api.image.exception.ImageNotLoadedException -import com.github.rushyverse.api.image.exception.ItemFramesAlreadyExistException -import io.mockk.every -import io.mockk.spyk -import kotlinx.coroutines.test.runTest -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.metadata.other.ItemFrameMeta -import net.minestom.server.item.Material -import net.minestom.server.item.metadata.MapMeta -import net.minestom.server.network.packet.server.SendablePacket -import net.minestom.server.network.packet.server.play.MapDataPacket -import net.minestom.server.utils.Rotation -import net.minestom.testing.Env -import net.minestom.testing.EnvTest -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.assertThrows -import java.awt.Color -import java.awt.image.BufferedImage -import java.awt.image.BufferedImage.TYPE_INT_ARGB -import kotlin.test.* - -class MapImageTest { - - companion object { - private const val BLACK_COLOR_PACKET = 119.toByte() - } - - @Nested - @EnvTest - inner class CreateItemFrames { - - @Nested - inner class Position { - - @Test - fun `should spawn item frame at the target position`(env: Env) = runTest { - val instance = env.createFlatInstance() - val orientation = ItemFrameMeta.Orientation.NORTH - val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(128, 128, TYPE_INT_ARGB) - - repeat(10) { x -> - repeat(10) { y -> - repeat(10) { z -> - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames( - instance, - Pos(x.toDouble(), y.toDouble(), z.toDouble()), - orientation - ) - assertEquals( - Pos(x.toDouble(), y.toDouble(), z.toDouble(), math.yaw, math.pitch), - mapImage.itemFrames!!.first().position - ) - } - } - } - } - - @Test - fun `should spawn item by following the north orientation`(env: Env) = runTest { - val instance = env.createFlatInstance() - val orientation = ItemFrameMeta.Orientation.NORTH - val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) - - val itemFrames = mapImage.itemFrames!! - assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) - assertEquals(Pos(-1.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[1].position) - assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[2].position) - assertEquals(Pos(-1.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[3].position) - } - - @Test - fun `should spawn item by following the east orientation`(env: Env) = runTest { - val instance = env.createFlatInstance() - val orientation = ItemFrameMeta.Orientation.EAST - val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) - - val itemFrames = mapImage.itemFrames!! - assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) - assertEquals(Pos(0.0, 0.0, -1.0, math.yaw, math.pitch), itemFrames[1].position) - assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[2].position) - assertEquals(Pos(0.0, -1.0, -1.0, math.yaw, math.pitch), itemFrames[3].position) - } - - @Test - fun `should spawn item by following the south orientation`(env: Env) = runTest { - val instance = env.createFlatInstance() - val orientation = ItemFrameMeta.Orientation.SOUTH - val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) - - val itemFrames = mapImage.itemFrames!! - assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) - assertEquals(Pos(1.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[1].position) - assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[2].position) - assertEquals(Pos(1.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[3].position) - } - - @Test - fun `should spawn item by following the west orientation`(env: Env) = runTest { - val instance = env.createFlatInstance() - val orientation = ItemFrameMeta.Orientation.WEST - val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) - - val itemFrames = mapImage.itemFrames!! - assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) - assertEquals(Pos(0.0, 0.0, 1.0, math.yaw, math.pitch), itemFrames[1].position) - assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), itemFrames[2].position) - assertEquals(Pos(0.0, -1.0, 1.0, math.yaw, math.pitch), itemFrames[3].position) - } - - @Test - fun `should spawn item by following the up orientation`(env: Env) = runTest { - val instance = env.createFlatInstance() - val orientation = ItemFrameMeta.Orientation.UP - val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) - - val itemFrames = mapImage.itemFrames!! - assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) - assertEquals(Pos(1.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[1].position) - assertEquals(Pos(0.0, 0.0, 1.0, math.yaw, math.pitch), itemFrames[2].position) - assertEquals(Pos(1.0, 0.0, 1.0, math.yaw, math.pitch), itemFrames[3].position) - } - - @Test - fun `should spawn item by following the down orientation`(env: Env) = runTest { - val instance = env.createFlatInstance() - val orientation = ItemFrameMeta.Orientation.DOWN - val math = MapImageMath.getFromOrientation(orientation) - val image = BufferedImage(256, 256, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) - - val itemFrames = mapImage.itemFrames!! - assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[0].position) - assertEquals(Pos(1.0, 0.0, 0.0, math.yaw, math.pitch), itemFrames[1].position) - assertEquals(Pos(0.0, 0.0, -1.0, math.yaw, math.pitch), itemFrames[2].position) - assertEquals(Pos(1.0, 0.0, -1.0, math.yaw, math.pitch), itemFrames[3].position) - } - } - - @Nested - inner class MetaInformation { - - @Test - fun `should custom meta of item frame if needed`(env: Env) = runTest { - val instance = env.createFlatInstance() - val image = BufferedImage(128, 128, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - - val invisible = false - val rotation = Rotation.values().random() - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) { - this.isInvisible = false - this.rotation = rotation - } - - val itemFrame = mapImage.itemFrames!!.first() - val itemFrameMeta = itemFrame.entityMeta as ItemFrameMeta - assertEquals(rotation, itemFrameMeta.rotation) - assertEquals(invisible, itemFrameMeta.isInvisible) - } - - @Test - fun `should spawn item frame with the target orientation`(env: Env) = runTest { - val instance = env.createFlatInstance() - val image = BufferedImage(128, 128, TYPE_INT_ARGB) - - ItemFrameMeta.Orientation.values().forEach { orientation -> - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) - val itemFrame = mapImage.itemFrames!!.first() - assertEquals(orientation, (itemFrame.entityMeta as ItemFrameMeta).orientation) - } - } - - @Test - fun `should spawn item frame with invisibility by default`(env: Env) = runTest { - val instance = env.createFlatInstance() - val image = BufferedImage(128, 128, TYPE_INT_ARGB) - - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) - val itemFrame = mapImage.itemFrames!!.first() - assertEquals(true, itemFrame.isInvisible) - } - - @Test - fun `should spawn item frame with map item in meta`(env: Env) = runTest { - val instance = env.createFlatInstance() - val image = BufferedImage(128, 128, TYPE_INT_ARGB) - - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) - val itemFrame = mapImage.itemFrames!!.first() - val meta = itemFrame.entityMeta as ItemFrameMeta - - val metaItem = meta.item - assertNotNull(metaItem) - assertEquals(Material.FILLED_MAP, metaItem.material()) - } - - @Test - fun `should spawn item frame with map id`(env: Env) = runTest { - val instance = env.createFlatInstance() - val image = BufferedImage(256, 256, TYPE_INT_ARGB) - - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) - - val itemFrames = mapImage.itemFrames - assertNotNull(itemFrames) - assertEquals(4, itemFrames.size) - - itemFrames.forEachIndexed { index, entity -> - val meta = entity.entityMeta as ItemFrameMeta - val metaItem = meta.item - val metaOfMetaItem = metaItem.meta(MapMeta::class.java) - assertEquals(index, metaOfMetaItem.mapId) - } - } - - } - - @Nested - inner class WithImageNotLoaded { - - @Test - fun `should throw exception if image is not loaded`(env: Env) = runTest { - val instance = env.createFlatInstance() - val mapImage = MapImage() - val ex = assertThrows { - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) - } - assertEquals("An image must be loaded before creating the item frames.", ex.message) - } - } - - @Nested - inner class ItemFramesAlreadyExist { - - @Test - fun `should throw exception if all item frames already exist`(env: Env) = runTest { - val instance = env.createFlatInstance() - val image = BufferedImage(128, 128, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) - - val ex = assertThrows { - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) - } - - assertEquals("The item frames are already present in the instance.", ex.message) - } - - @Test - fun `should throw exception if at least one item frames already exist`(env: Env) = runTest { - val instance = env.createFlatInstance() - val image = BufferedImage(1024, 1024, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) - - val itemFrames = mapImage.itemFrames - assertNotNull(itemFrames) - itemFrames.drop(1).forEach { it.remove() } - assertTrue { itemFrames.drop(1).all { it.isRemoved } } - assertFalse { itemFrames.first().isRemoved } - - val ex = assertThrows { - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) - } - - assertEquals("The item frames are already present in the instance.", ex.message) - } - - } - - @Test - fun `should throw exception when image size is 0x0`(env: Env) = runTest { - val instance = env.createFlatInstance() - val mapImage = MapImage() - val image = BufferedImage(1, 1, TYPE_INT_ARGB) - mapImage.loadImageAsPackets(image) - - val spyMapImage = spyk(mapImage) { - every { itemFramesPerLine } returns 0 - every { itemFramesPerColumn } returns 0 - } - val returnedFrame = - spyMapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) - assertTrue { returnedFrame.isEmpty() } - assertTrue { spyMapImage.itemFrames!!.isEmpty() } - } - - @Test - fun `should return and set the property of item frames`(env: Env) = runTest { - val instance = env.createFlatInstance() - val mapImage = MapImage() - val image = BufferedImage(512, 1024, TYPE_INT_ARGB) - mapImage.loadImageAsPackets(image) - - val returnedFrame = mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) - assertEquals(returnedFrame, mapImage.itemFrames) - } - - @Test - fun `should create one item frame if image is between 1x1 and 128x128`(env: Env) = runTest { - val instance = env.createFlatInstance() - (1..128).forEach { width -> - val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(width, width, TYPE_INT_ARGB)) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) - - val itemFrames = mapImage.itemFrames - assertNotNull(itemFrames) - assertEquals(1, itemFrames.size) - } - } - - @Test - fun `should create two item frame if image is 129x128`(env: Env) = runTest { - val instance = env.createFlatInstance() - val mapImage = MapImage() - val orientation = ItemFrameMeta.Orientation.NORTH - mapImage.loadImageAsPackets(BufferedImage(129, 128, TYPE_INT_ARGB)) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) - val math = MapImageMath.getFromOrientation(orientation) - - val itemFrames = mapImage.itemFrames - assertNotNull(itemFrames) - assertEquals(2, itemFrames.size) - - val (first, second) = itemFrames - assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), first.position) - assertEquals(Pos(-1.0, 0.0, 0.0, math.yaw, math.pitch), second.position) - } - - @Test - fun `should create two item frame if image is 128x129`(env: Env) = runTest { - val instance = env.createFlatInstance() - val mapImage = MapImage() - val orientation = ItemFrameMeta.Orientation.NORTH - mapImage.loadImageAsPackets(BufferedImage(128, 129, TYPE_INT_ARGB)) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation) - val math = MapImageMath.getFromOrientation(orientation) - - val itemFrames = mapImage.itemFrames - assertNotNull(itemFrames) - assertEquals(2, itemFrames.size) - - val (first, second) = itemFrames - assertEquals(Pos(0.0, 0.0, 0.0, math.yaw, math.pitch), first.position) - assertEquals(Pos(0.0, -1.0, 0.0, math.yaw, math.pitch), second.position) - } - - @Test - fun `should spawn item frame at the target instance`(env: Env) = runTest { - val instance = env.createFlatInstance() - val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(128, 128, TYPE_INT_ARGB)) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), orientation = ItemFrameMeta.Orientation.NORTH) - assertEquals(instance, mapImage.itemFrames!!.first().instance) - } - } - - @Nested - inner class LoadImageAsPackets { - - @Nested - inner class ItemFramesPerLine { - - @Test - fun `should set property according to the width`() { - fun assertWidthPixelWithItemFramesPerLine(widths: IntRange, expectedFramesPerLine: Int) { - widths.forEach { - val image = BufferedImage(it, 128, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - assertEquals(expectedFramesPerLine, mapImage.itemFramesPerLine) - } - } - assertWidthPixelWithItemFramesPerLine(1..128, 1) - assertWidthPixelWithItemFramesPerLine(129..256, 2) - assertWidthPixelWithItemFramesPerLine(257..384, 3) - assertWidthPixelWithItemFramesPerLine(385..512, 4) - assertWidthPixelWithItemFramesPerLine(513..640, 5) - assertWidthPixelWithItemFramesPerLine(641..768, 6) - assertWidthPixelWithItemFramesPerLine(769..896, 7) - assertWidthPixelWithItemFramesPerLine(897..1024, 8) - } - - } - - @Nested - inner class ItemFramesPerColumn { - - @Test - fun `should set property according to the height`() { - fun assertHeightPixelWithItemFramesPerColumn(heights: IntRange, expectedFramesPerColumn: Int) { - heights.forEach { - val image = BufferedImage(128, it, TYPE_INT_ARGB) - val mapImage = MapImage() - mapImage.loadImageAsPackets(image) - assertEquals(expectedFramesPerColumn, mapImage.itemFramesPerColumn) - } - } - assertHeightPixelWithItemFramesPerColumn(1..128, 1) - assertHeightPixelWithItemFramesPerColumn(129..256, 2) - assertHeightPixelWithItemFramesPerColumn(257..384, 3) - assertHeightPixelWithItemFramesPerColumn(385..512, 4) - assertHeightPixelWithItemFramesPerColumn(513..640, 5) - assertHeightPixelWithItemFramesPerColumn(641..768, 6) - assertHeightPixelWithItemFramesPerColumn(769..896, 7) - assertHeightPixelWithItemFramesPerColumn(897..1024, 8) - } - - } - - @Test - fun `should throw exception if an image is already loaded`() { - val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(128, 128, TYPE_INT_ARGB)) - assertThrows { - mapImage.loadImageAsPackets(BufferedImage(128, 128, TYPE_INT_ARGB)) - } - } - - @Test - fun `should load map data packets`() { - val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(1000, 1000, TYPE_INT_ARGB)) - - val packets = assertNotNull(mapImage.packets) - packets.forEachIndexed { index, packet -> - assertTrue(packet is MapDataPacket) - assertEquals(index, packet.mapId) - } - } - - @Test - fun `should create one packet if the image is between 1x1 and 128x128`() { - (1..128).forEach { width -> - (1..128).forEach { height -> - val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(width, height, TYPE_INT_ARGB)) - val packets = assertNotNull(mapImage.packets) - assertEquals(1, packets.size) - } - } - } - - @Test - fun `should create two packets if the image width is between 129 and 256`() { - (129..256).forEach { width -> - val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(width, 1, TYPE_INT_ARGB)) - val packets = assertNotNull(mapImage.packets) - assertEquals(2, packets.size) - } - } - - @Test - fun `should create two packets if the image height is between 129 and 256`() { - (129..256).forEach { height -> - val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(1, height, TYPE_INT_ARGB)) - val packets = assertNotNull(mapImage.packets) - assertEquals(2, packets.size) - } - } - - @Test - fun `should create four packets if the image is between 129x129 and 256x256`() { - fun assertNumberPackets(width: Int, height: Int, expectedNumberPackets: Int) { - val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(width, height, TYPE_INT_ARGB)) - val packets = assertNotNull(mapImage.packets) - assertEquals(expectedNumberPackets, packets.size) - } - assertNumberPackets(129, 129, 4) - assertNumberPackets(256, 129, 4) - assertNumberPackets(129, 256, 4) - assertNumberPackets(200, 200, 4) - assertNumberPackets(256, 256, 4) - } - - @Test - fun `should have same content than the image for one packet`() { - val mapImage = MapImage() - val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_RGB) - - val colors = listOf( - Color.PINK.rgb to (-110).toByte(), - Color.BLUE.rgb to (49).toByte(), - Color.GREEN.rgb to (-122).toByte() - ) - - for (y in 0 until image.height) { - for (x in 0 until image.width) { - val colorIndex = x % colors.size - image.setRGB(x, y, colors[colorIndex].first) - } - } - - mapImage.loadImageAsPackets(image) - val packets = assertNotNull(mapImage.packets) - assertEquals(1, packets.size) - val packet = packets[0] as MapDataPacket - - val data = packet.colorContent!!.data - assertEquals(128 * 128, data.size) - - for (y in 0 until 128) { - for (x in 0 until 128) { - val colorIndex = x % colors.size - assertEquals(colors[colorIndex].second, data[y * 128 + x]) - } - } - } - - @Test - fun `should have same content than the image for two packets`() { - val mapImage = MapImage() - val image = BufferedImage(256, 128, BufferedImage.TYPE_INT_RGB) - - val colorsFirstFrame = listOf( - Color.PINK.rgb to (-110).toByte(), - Color.BLUE.rgb to (49).toByte(), - Color.GREEN.rgb to (-122).toByte() - ) - - val colorsSecondFrame = listOf( - Color.DARK_GRAY.rgb to (85).toByte(), - Color.YELLOW.rgb to (74).toByte(), - Color.CYAN.rgb to (126).toByte() - ) - - // horizontal stripes - for (y in 0 until image.height) { - for (x in 0 until 128) { - val colorIndex = x % colorsFirstFrame.size - image.setRGB(x, y, colorsFirstFrame[colorIndex].first) - } - } - - // vertical stripes - for (x in 128 until 256) { - for (y in 0 until image.height) { - val colorIndex = x % colorsSecondFrame.size - image.setRGB(x, y, colorsSecondFrame[colorIndex].first) - } - } - - mapImage.loadImageAsPackets(image) - val packets = assertNotNull(mapImage.packets) - assertEquals(2, packets.size) - - val packet1 = packets[0] as MapDataPacket - val data1 = packet1.colorContent!!.data - assertTrue { data1.all { it != BLACK_COLOR_PACKET } } - assertEquals(128 * 128, data1.size) - - for (y in 0 until 128) { - for (x in 0 until image.height) { - val colorIndex = x % colorsFirstFrame.size - assertEquals(colorsFirstFrame[colorIndex].second, data1[y * 128 + x]) - } - } - - val packet2 = packets[1] as MapDataPacket - val data2 = packet2.colorContent!!.data - assertTrue { data2.all { it != BLACK_COLOR_PACKET } } - assertEquals(128 * 128, data2.size) - - for (x in 128 until 256) { - for (y in 0 until image.height) { - val colorIndex = x % colorsSecondFrame.size - assertEquals(colorsSecondFrame[colorIndex].second, data2[y * 128 + (x - 128)]) - } - } - } - - @Test - fun `should apply transformation on packets`() { - val mapImage = MapImage() - val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_RGB) - - val colors = listOf( - Color.PINK.rgb to (-110).toByte(), - Color.BLUE.rgb to (49).toByte(), - Color.GREEN.rgb to (-122).toByte() - ) - - for (y in 0 until image.height) { - for (x in 0 until image.width) { - val colorIndex = x % colors.size - image.setRGB(x, y, colors[colorIndex].first) - } - } - - mapImage.loadImageAsPackets(image) { - rotate(Math.toRadians(90.0), it.width / 2.0, it.height / 2.0) - } - - val packets = assertNotNull(mapImage.packets) - assertEquals(1, packets.size) - val packet = packets[0] as MapDataPacket - - val data = packet.colorContent!!.data - assertEquals(128 * 128, data.size) - - for (x in 0 until 128) { - for (y in 0 until 128) { - // Use Y due to rotation - val colorIndex = y % colors.size - assertEquals(colors[colorIndex].second, data[y * 128 + x]) - } - } - } - - @Test - fun `should load image from resources`() { - val mapImage = MapImage() - val packets = mapImage.loadImageAsPacketsFromResources("map_image.png") - assertImagePacket(packets) - } - - @Test - fun `should load image from input stream`() { - val mapImage = MapImage() - MapImageTest::class.java.getResourceAsStream("/map_image.png")!!.buffered().use { - val packets = mapImage.loadImageAsPacketsFromInputStream(it) - assertImagePacket(packets) - } - } - - private fun assertImagePacket(packets: Array) { - val colors = listOf( - Color(84, 70, 162) to (23).toByte(), - Color(55, 215, 61) to (-122).toByte(), - Color(215, 55, 214) to (66).toByte(), - Color(49, 61, 50) to (84).toByte(), - ) - - assertNotNull(packets) - assertEquals(4, packets.size) - packets.zip(colors).forEach { (packet, color) -> - val data = (packet as MapDataPacket).colorContent!!.data - data.forEach { - assertEquals(color.second, it) - } - } - } - - } - - @Nested - @EnvTest - inner class RemoveItemFrames { - - @Test - fun `should do nothing if no item frames`(env: Env) { - val mapImage = MapImage() - assertNull(mapImage.itemFrames) - mapImage.removeItemFrames() - assertNull(mapImage.itemFrames) - } - - @Test - fun `should remove item frames`(env: Env) = runTest { - val instance = env.createFlatInstance() - val mapImage = MapImage() - mapImage.loadImageAsPackets(BufferedImage(512, 512, BufferedImage.TYPE_INT_RGB)) - mapImage.createItemFrames(instance, Pos(0.0, 0.0, 0.0), ItemFrameMeta.Orientation.NORTH) - - val frames = assertNotNull(mapImage.itemFrames) - - assertTrue { frames.all { !it.isRemoved } } - mapImage.removeItemFrames() - assertTrue { frames.all { it.isRemoved } } - - assertNull(mapImage.itemFrames) - } - } - - @Test - fun `constant value should be correct`() { - assertEquals(128, MapImage.MAP_ITEM_FRAME_PIXELS) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/item/InventoryConditionSuspendTest.kt b/src/test/kotlin/com/github/rushyverse/api/item/InventoryConditionSuspendTest.kt deleted file mode 100644 index dd97df8e..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/item/InventoryConditionSuspendTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.github.rushyverse.api.item - -import com.github.rushyverse.api.utils.assertCoroutineContextFromScope -import com.github.rushyverse.api.utils.randomInt -import com.github.rushyverse.api.utils.randomString -import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.yield -import net.minestom.server.entity.Player -import net.minestom.server.inventory.click.ClickType -import net.minestom.server.inventory.condition.InventoryConditionResult -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit -import kotlin.coroutines.coroutineContext -import kotlin.test.Test -import kotlin.test.assertEquals - -@Timeout(5, unit = TimeUnit.SECONDS) -class InventoryConditionSuspendTest { - - @Nested - inner class AsNative { - - @Test - fun `should use params sent to the native inventory`() { - val expectedPlayer = mockk(randomString()) - val expectedSlot = randomInt() - val expectedClickType = mockk(randomString()) - val expectedInventoryConditionResult = mockk(randomString()) - - val inventoryConditionSuspend = InventoryConditionSuspend { player, clickedSlot, clickType, result -> - assertEquals(expectedPlayer, player) - assertEquals(expectedSlot, clickedSlot) - assertEquals(expectedClickType, clickType) - assertEquals(expectedInventoryConditionResult, result) - } - - val coroutineScope = CoroutineScope(Dispatchers.Default) - val inventoryCondition = inventoryConditionSuspend.asNative(coroutineScope) - inventoryCondition.accept(expectedPlayer, expectedSlot, expectedClickType, expectedInventoryConditionResult) - } - - @Test - fun `should stay in current thread before suspend point`() { - val thread = Thread.currentThread().id - val inventoryConditionSuspend = InventoryConditionSuspend { _, _, _, _ -> - assertEquals(thread, Thread.currentThread().id) - } - - val coroutineScope = CoroutineScope(Dispatchers.Default) - val inventoryCondition = inventoryConditionSuspend.asNative(coroutineScope) - inventoryCondition.accept(mockk(), 0, mockk(), mockk()) - } - - @Test - fun `should change thread context after suspend point`() { - val coroutineScope = CoroutineScope(Dispatchers.Default) - - val inventoryConditionSuspend = InventoryConditionSuspend { _, _, _, _ -> - yield() - assertCoroutineContextFromScope(coroutineScope, coroutineContext) - } - - val inventoryCondition = inventoryConditionSuspend.asNative(coroutineScope) - inventoryCondition.accept(mockk(), 0, mockk(), mockk()) - } - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/item/ItemComparatorTest.kt b/src/test/kotlin/com/github/rushyverse/api/item/ItemComparatorTest.kt deleted file mode 100644 index 51ad6a3a..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/item/ItemComparatorTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.github.rushyverse.api.item - -import net.kyori.adventure.text.Component -import net.minestom.server.item.ItemStack -import net.minestom.server.item.Material -import org.junit.jupiter.api.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class ItemComparatorTest { - - @Test - fun `similar should check the similarity without the amount`() { - val item1 = ItemStack.of(Material.DIAMOND) - val item2 = ItemStack.of(Material.DIAMOND) - assertTrue(ItemComparator.SIMILAR.areSame(item1, item2)) - - val item3 = ItemStack.of(Material.DIAMOND).withDisplayName(Component.text("Test")) - assertFalse(ItemComparator.SIMILAR.areSame(item1, item3)) - - val item4 = ItemStack.of(Material.DIAMOND, 2).withDisplayName(Component.text("Test")) - assertTrue(ItemComparator.SIMILAR.areSame(item3, item4)) - - val item5 = ItemStack.of(Material.DIAMOND_SWORD) - assertFalse(ItemComparator.SIMILAR.areSame(item1, item5)) - } - - @Test - fun `equals should check the similarity with the amount`() { - val item1 = ItemStack.of(Material.DIAMOND) - val item2 = ItemStack.of(Material.DIAMOND) - assertTrue(ItemComparator.EQUALS.areSame(item1, item2)) - - val item3 = ItemStack.of(Material.DIAMOND).withDisplayName(Component.text("Test")) - assertFalse(ItemComparator.EQUALS.areSame(item1, item3)) - - val item4 = ItemStack.of(Material.DIAMOND, 2).withDisplayName(Component.text("Test")) - assertFalse(ItemComparator.EQUALS.areSame(item3, item4)) - - val item5 = ItemStack.of(Material.DIAMOND_SWORD) - assertFalse(ItemComparator.EQUALS.areSame(item1, item5)) - } - - @Test - fun `custom should respect the custom comparator`() { - val comparator = ItemComparator { a, b -> a.material() == b.material() } - - val item1 = ItemStack.of(Material.DIAMOND) - val item2 = ItemStack.of(Material.DIAMOND) - assertTrue(comparator.areSame(item1, item2)) - - val item3 = ItemStack.of(Material.DIAMOND, 2) - assertTrue(comparator.areSame(item1, item3)) - - val item4 = ItemStack.of(Material.DIAMOND).withDisplayName(Component.text("Test")) - assertTrue(comparator.areSame(item1, item4)) - - val item5 = ItemStack.of(Material.DIAMOND_SWORD) - assertFalse(comparator.areSame(item1, item5)) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/koin/CraftContextTest.kt b/src/test/kotlin/com/github/rushyverse/api/koin/CraftContextTest.kt new file mode 100644 index 00000000..8efb797f --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/koin/CraftContextTest.kt @@ -0,0 +1,341 @@ +package com.github.rushyverse.api.koin + +import com.github.rushyverse.api.utils.randomString +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.koin.core.KoinApplication +import org.koin.core.error.ClosedScopeException +import org.koin.core.error.KoinAppAlreadyStartedException +import org.koin.core.error.NoBeanDefFoundException +import org.koin.dsl.module +import kotlin.test.* + +class CraftContextTest { + + @AfterTest + fun onAfter() { + CraftContext.koins.toMap().forEach { (id, _) -> + CraftContext.stopKoin(id) + } + } + + @Nested + @DisplayName("Get koin context") + inner class Get { + + @Test + fun `when no instance exists for the id`() { + assertThrows { + CraftContext.get(randomString()) + } + } + + @Test + fun `when instance is registered for the id`() { + val id = randomString() + CraftContext.startKoin(id) + val koin = CraftContext.get(id) + assertNotNull(koin) + } + } + + @Nested + @DisplayName("Get koin context or null") + inner class GetOrNull { + + @Test + fun `when no instance exists for the id`() { + assertNull(CraftContext.getOrNull(randomString())) + } + + @Test + fun `when instance is registered for the id`() { + val id = randomString() + CraftContext.startKoin(id) + val koin = CraftContext.get(id) + assertNotNull(koin) + } + } + + @Nested + @DisplayName("Stop koin context") + inner class StopKoin { + + @Test + fun `when no instance exists for the id`() { + CraftContext.stopKoin(randomString()) + } + + @Test + fun `when instance is registered for the id`() { + assertEquals(0, CraftContext.koins.size) + val id = randomString() + CraftContext.startKoin(id) + val module = module { + single { 1.2 } + } + assertEquals(1, CraftContext.koins.size) + + val koin = CraftContext.get(id) + koin.loadModules(listOf(module)) + assertEquals(1.2, koin.get()) + + CraftContext.stopKoin(id) + assertEquals(0, CraftContext.koins.size) + assertNull(CraftContext.getOrNull(id)) + assertThrows { + assertEquals(1.2, koin.get()) + } + } + } + + @Nested + @DisplayName("Start koin context") + inner class StartKoin { + + @Test + fun `when no instance exists for the id`() { + assertEquals(0, CraftContext.koins.size) + CraftContext.startKoin(randomString()) {} + assertEquals(1, CraftContext.koins.size) + } + + @Test + fun `when instance already exists for the id`() { + assertEquals(0, CraftContext.koins.size) + val id = randomString() + CraftContext.startKoin(id) {} + assertEquals(1, CraftContext.koins.size) + assertThrows { + CraftContext.startKoin(id) {} + } + assertEquals(1, CraftContext.koins.size) + } + + @Test + fun `define module in declaration during starting`() { + assertEquals(0, CraftContext.koins.size) + val id = randomString() + CraftContext.startKoin(id) { + this.modules(module { + single { "hello" } + }) + } + val injectedString = CraftContext.get(id).get() + assertEquals("hello", injectedString) + assertEquals(1, CraftContext.koins.size) + } + } + + @Nested + @DisplayName("Start with koin application") + inner class StartKoinApplication { + + @Test + fun `when no instance exists for the id`() { + assertEquals(0, CraftContext.koins.size) + val koinApplication = KoinApplication.init() + CraftContext.startKoin(randomString(), koinApplication) + assertEquals(1, CraftContext.koins.size) + } + + @Test + fun `when instance already exists for the id`() { + assertEquals(0, CraftContext.koins.size) + val id = randomString() + val koinApplication = KoinApplication.init() + CraftContext.startKoin(id, koinApplication) + assertEquals(1, CraftContext.koins.size) + assertThrows { + CraftContext.startKoin(id, koinApplication) + } + assertEquals(1, CraftContext.koins.size) + } + + @Test + fun `define module in declaration during starting`() { + assertEquals(0, CraftContext.koins.size) + val koinApplication = KoinApplication.init() + koinApplication.modules(module { + single { "hello" } + }) + val id = randomString() + CraftContext.startKoin(id, koinApplication) + val injectedString = CraftContext.get(id).get() + assertEquals("hello", injectedString) + assertEquals(1, CraftContext.koins.size) + } + } + + @Nested + @DisplayName("Load koin module with single module") + inner class LoadKoinSingleModule { + + @Test + fun `when no instance exists for the id`() { + assertThrows { + CraftContext.loadKoinModules(randomString(), module { }) + } + } + + @Test + fun `when instance exists for the id`() { + val id = randomString() + CraftContext.startKoin(id) + CraftContext.loadKoinModules(id, module { + single { "hello" } + }) + assertEquals("hello", CraftContext.get(id).get()) + } + } + + @Nested + @DisplayName("Load koin module with several modules") + inner class LoadKoinListModule { + + @Test + fun `when no instance exists for the id`() { + assertThrows { + CraftContext.loadKoinModules(randomString(), emptyList()) + } + } + + @Test + fun `when instance exists for the id`() { + val id = randomString() + CraftContext.startKoin(id) + CraftContext.loadKoinModules(id, listOf( + module { + single { "hello" } + }, module { + single { 42 } + }) + ) + assertEquals("hello", CraftContext.get(id).get()) + assertEquals(42, CraftContext.get(id).get()) + } + } + + @Nested + @DisplayName("Unload koin module with single module") + inner class UnloadKoinSingleModule { + + @Test + fun `when no instance exists for the id`() { + assertThrows { + CraftContext.unloadKoinModules(randomString(), module { }) + } + } + + @Test + fun `when instance exists for the id but not module linked`() { + val id = randomString() + CraftContext.startKoin(id) + CraftContext.unloadKoinModules(id, module { + single { "hello" } + }) + } + + @Test + fun `when instance exists for the id`() { + val id = randomString() + CraftContext.startKoin(id) + val module = module { + single { "hello" } + } + CraftContext.loadKoinModules(id, module) + assertEquals("hello", CraftContext.get(id).get()) + + CraftContext.unloadKoinModules(id, module) + assertThrows { + assertEquals("hello", CraftContext.get(id).get()) + } + } + } + + @Nested + @DisplayName("Unload koin module with several modules") + inner class UnloadKoinListModule { + + @Test + fun `when no instance exists for the id`() { + assertThrows { + CraftContext.unloadKoinModules(randomString(), emptyList()) + } + } + + @Test + fun `when instance exists for the id but not module linked`() { + val id = randomString() + CraftContext.startKoin(id) + CraftContext.unloadKoinModules(id, listOf(module { + single { "hello" } + })) + } + + @Test + fun `when instance exists for the id`() { + val id = randomString() + CraftContext.startKoin(id) + val module1 = module { + single { "hello" } + } + val module2 = module { + single { 1 } + } + val module3 = module { + single { 1.2 } + } + CraftContext.loadKoinModules(id, listOf(module1, module2, module3)) + assertEquals("hello", CraftContext.get(id).get()) + assertEquals(1, CraftContext.get(id).get()) + assertEquals(1.2, CraftContext.get(id).get()) + + CraftContext.unloadKoinModules(id, listOf(module1)) + assertThrows { + assertEquals("hello", CraftContext.get(id).get()) + } + assertEquals(1, CraftContext.get(id).get()) + assertEquals(1.2, CraftContext.get(id).get()) + + CraftContext.unloadKoinModules(id, listOf(module2, module3)) + assertThrows { + assertEquals(1, CraftContext.get(id).get()) + } + + assertThrows { + assertEquals(1.2, CraftContext.get(id).get()) + } + } + } + + @Nested + @DisplayName("Load koin module with lazy extension function") + inner class LoadKoinSingleModuleExtension { + + @Test + fun `when no instance exists for the id`() { + assertThrows { + loadModule(randomString()) {} + } + } + + @Test + fun `when instance exists for the id with lazy module creation`() { + val id = randomString() + CraftContext.startKoin(id) + var isInit = false + loadModule(id) { + single { + isInit = true + "hello" + } + } + assertFalse { isInit } + assertEquals("hello", CraftContext.get(id).get()) + assertTrue { isInit } + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/listener/EventListenerSuspendTest.kt b/src/test/kotlin/com/github/rushyverse/api/listener/EventListenerSuspendTest.kt deleted file mode 100644 index 5d967174..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/listener/EventListenerSuspendTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.github.rushyverse.api.listener - -import com.github.rushyverse.api.utils.assertCoroutineContextFromScope -import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.yield -import net.minestom.server.event.EventListener -import net.minestom.server.event.player.PlayerLoginEvent -import net.minestom.server.event.player.PlayerMoveEvent -import java.util.concurrent.CountDownLatch -import kotlin.coroutines.coroutineContext -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue - -class EventListenerSuspendTest { - - @Test - fun `should use dispatcher to process event listener suspend`() { - val expectedEvent = mockk() - val currentThread = Thread.currentThread() - val latch = CountDownLatch(1) - var executed = false - - val scope = CoroutineScope(Dispatchers.Default) - val handler = object : EventListenerSuspend(scope) { - override suspend fun runSuspend(event: PlayerMoveEvent) { - assertCoroutineContextFromScope(scope, coroutineContext) - assertEquals(currentThread, Thread.currentThread()) - assertEquals(expectedEvent, event) - - yield() - - assertNotEquals(currentThread, Thread.currentThread()) - latch.countDown() - executed = true - } - - override fun eventType(): Class { - return PlayerMoveEvent::class.java - } - } - - assertEquals(EventListener.Result.SUCCESS, handler.run(expectedEvent)) - latch.await() - assertTrue(executed) - } - - @Test - fun `should return success despite an exception`() { - val expectedEvent = mockk() - val scope = CoroutineScope(Dispatchers.Default) - val handler = object : EventListenerSuspend(scope) { - override suspend fun runSuspend(event: PlayerLoginEvent) { - throw RuntimeException() - } - - override fun eventType(): Class { - return PlayerLoginEvent::class.java - } - } - - assertEquals(EventListener.Result.SUCCESS, handler.run(expectedEvent)) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/listener/NPCListenerBuilderTest.kt b/src/test/kotlin/com/github/rushyverse/api/listener/NPCListenerBuilderTest.kt deleted file mode 100644 index b1f52485..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/listener/NPCListenerBuilderTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.rushyverse.api.listener - -import com.github.rushyverse.api.entity.NPCEntity -import io.mockk.every -import io.mockk.justRun -import io.mockk.mockk -import io.mockk.verify -import net.minestom.server.entity.Player -import net.minestom.server.event.Event -import net.minestom.server.event.EventNode -import net.minestom.server.event.player.PlayerEntityInteractEvent -import org.junit.jupiter.api.Nested -import kotlin.test.BeforeTest -import kotlin.test.Test - -class NPCListenerBuilderTest { - - lateinit var node: EventNode - - @BeforeTest - fun onBefore() { - node = NPCListenerBuilder.createEventNode() - } - - @Nested - inner class CreateEventNode { - - @Test - fun `create event node with npc name`() { - assert(node.name == "npc") - } - - } - - @Nested - inner class AddInteractListener { - - @Test - fun `node event has a register listener for interact event`() { - node.hasListener(PlayerEntityInteractEvent::class.java) - } - - @Test - fun `doesn't trigger when target is not an npc`() { - val event = mockk() - every { event.target } returns mockk() - node.call(event) - verify(exactly = 1) { event.target } - } - - @Test - fun `doesn't trigger when hand is not main hand`() { - val event = mockk() - every { event.target } returns mockk() - every { event.hand } returns Player.Hand.OFF - node.call(event) - verify(exactly = 1) { event.target } - verify(exactly = 1) { event.hand } - } - - @Test - fun `trigger interaction method of npc`() { - val event = mockk() - val target = mockk() - justRun { target.onInteract(event) } - - every { event.target } returns target - every { event.hand } returns Player.Hand.MAIN - - node.call(event) - verify(exactly = 2) { event.target } - verify(exactly = 1) { event.hand } - verify(exactly = 1) { target.onInteract(event) } - } - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/listener/PlayerListenerTest.kt b/src/test/kotlin/com/github/rushyverse/api/listener/PlayerListenerTest.kt new file mode 100644 index 00000000..b1fa0770 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/listener/PlayerListenerTest.kt @@ -0,0 +1,134 @@ +package com.github.rushyverse.api.listener + +import com.github.rushyverse.api.AbstractKoinTest +import com.github.rushyverse.api.player.Client +import com.github.rushyverse.api.player.ClientManager +import com.github.rushyverse.api.player.ClientManagerImpl +import com.github.rushyverse.api.player.exception.ClientAlreadyExistsException +import com.github.rushyverse.api.utils.randomString +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.isActive +import kotlinx.coroutines.test.runTest +import net.kyori.adventure.text.Component +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import java.util.* +import kotlin.test.* + +class PlayerListenerTest : AbstractKoinTest() { + + lateinit var clientManager: ClientManager + + private lateinit var listener: PlayerListener + + @BeforeTest + override fun onBefore() { + super.onBefore() + clientManager = ClientManagerImpl() + loadTestModule { + single { clientManager } + } + + listener = PlayerListener(plugin) + } + + @Test + fun `contains no clients when instance is created`() { + assertTrue { clientManager.clients.isEmpty() } + } + + @Nested + @DisplayName("Player join") + inner class PlayerJoin { + + private lateinit var event: PlayerJoinEvent + private lateinit var player: Player + + @BeforeTest + fun onBefore() { + player = createPlayerMock() + event = createEvent(player) + } + + @Test + fun `create and save a new client`() = runTest { + val client = createClient(player) + every { plugin.createClient(any()) } returns client + + listener.onJoin(event) + + val clients = clientManager.clients + assertEquals(1, clients.size) + assertTrue { clientManager.contains(player) } + assertEquals(client, clients.values.first()) + + val otherPlayer = createPlayerMock() + val otherEvent = createEvent(otherPlayer) + + val otherClient = createClient(otherPlayer) + every { plugin.createClient(otherPlayer) } returns otherClient + + listener.onJoin(otherEvent) + assertEquals(2, clients.size) + assertTrue { clientManager.contains(player) && clientManager.contains(otherPlayer) } + clients.values.containsAll(listOf(client, otherClient)) + } + + @Test + fun `try to store a client with the same name but already exists and keep first instance`() = runTest { + val client = createClient(player) + every { plugin.createClient(player) } returns client + + listener.onJoin(event) + assertThrows { + listener.onJoin(event) + } + } + + private fun createEvent(player: Player): PlayerJoinEvent { + return PlayerJoinEvent(player, mockk()) + } + } + + @Nested + @DisplayName("Player leave") + inner class PlayerLeave { + + @Test + fun `client linked to the player is removed and cancelled`() = runTest { + val player = createPlayerMock() + + val client = Client(player.uniqueId, CoroutineScope(Dispatchers.Main + SupervisorJob())) + clientManager.put(player, client) + + listener.onQuit(createEvent(player)) + + assertEquals(0, clientManager.clients.size) + assertFalse { client.isActive } + } + + private fun createEvent(player: Player): PlayerQuitEvent { + return PlayerQuitEvent(player, mockk(), PlayerQuitEvent.QuitReason.DISCONNECTED) + } + + } + + private fun createPlayerMock(): Player { + val name = randomString() + val player = mockk(name) + every { player.name } returns name + every { player.uniqueId } returns UUID.randomUUID() + return player + } + + private fun createClient(player: Player) = + Client(player.uniqueId, CoroutineScope(Dispatchers.Default + SupervisorJob())) +} diff --git a/src/test/kotlin/com/github/rushyverse/api/listener/VillagerListenerTest.kt b/src/test/kotlin/com/github/rushyverse/api/listener/VillagerListenerTest.kt new file mode 100644 index 00000000..71cb693a --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/listener/VillagerListenerTest.kt @@ -0,0 +1,73 @@ +package com.github.rushyverse.api.listener + +import com.github.rushyverse.api.AbstractKoinTest +import com.github.rushyverse.api.extension.namespacedKeyKeepJob +import com.github.rushyverse.api.utils.randomString +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.bukkit.NamespacedKey +import org.bukkit.entity.Villager +import org.bukkit.event.entity.VillagerCareerChangeEvent +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataType +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import kotlin.test.* + +class VillagerListenerTest : AbstractKoinTest() { + + private lateinit var listener: VillagerListener + + @BeforeTest + override fun onBefore() { + super.onBefore() + listener = VillagerListener(plugin) + } + + @Nested + @DisplayName("Career change event") + inner class CareerChange { + + @Test + fun `cancel event when tag present in entity`() { + val villager = mockk(randomString()) + val container = mockk() + + val slotNamespaced = slot() + every { container.get(capture(slotNamespaced), PersistentDataType.BYTE) } returns 0.toByte() + every { villager.persistentDataContainer } returns container + + val event = VillagerCareerChangeEvent( + villager, + Villager.Profession.LEATHERWORKER, + VillagerCareerChangeEvent.ChangeReason.EMPLOYED + ) + event.isCancelled = false + listener.onChangeCareer(event) + assertEquals(namespacedKeyKeepJob(plugin), slotNamespaced.captured) + assertTrue { event.isCancelled } + } + + @Test + fun `not cancel event when tag present in entity`() { + val villager = mockk(randomString()) + + val container = mockk() + val slotNamespaced = slot() + every { container.get(capture(slotNamespaced), any>()) } returns null + every { villager.persistentDataContainer } returns container + + val event = VillagerCareerChangeEvent( + villager, + Villager.Profession.LEATHERWORKER, + VillagerCareerChangeEvent.ChangeReason.EMPLOYED + ) + event.isCancelled = true + listener.onChangeCareer(event) + assertEquals(namespacedKeyKeepJob(plugin), slotNamespaced.captured) + assertFalse { event.isCancelled } + } + + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/listener/api/LanguageListenerTest.kt b/src/test/kotlin/com/github/rushyverse/api/listener/api/LanguageListenerTest.kt new file mode 100644 index 00000000..1e298586 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/listener/api/LanguageListenerTest.kt @@ -0,0 +1,39 @@ +package com.github.rushyverse.api.listener.api + +import com.github.rushyverse.api.AbstractKoinTest +import com.github.rushyverse.api.player.language.LanguageManager +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import net.kyori.adventure.text.Component +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerQuitEvent +import kotlin.test.BeforeTest +import kotlin.test.Test + +class LanguageListenerTest: AbstractKoinTest() { + + private lateinit var listener: LanguageListener + private lateinit var languageManager: LanguageManager + + @BeforeTest + override fun onBefore() { + super.onBefore() + listener = LanguageListener() + languageManager = mockk() + + loadApiTestModule { + single { languageManager } + } + } + + @Test + fun `should call remove in manager when player leave`() = runTest { + val player = mockk() + coJustRun { languageManager.remove(player) } + listener.onQuit(PlayerQuitEvent(player, Component.empty(), PlayerQuitEvent.QuitReason.DISCONNECTED)) + coVerify(exactly = 1) { languageManager.remove(player) } + } + +} diff --git a/src/test/kotlin/com/github/rushyverse/api/listener/api/ScoreboardListenerTest.kt b/src/test/kotlin/com/github/rushyverse/api/listener/api/ScoreboardListenerTest.kt new file mode 100644 index 00000000..7123f153 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/listener/api/ScoreboardListenerTest.kt @@ -0,0 +1,39 @@ +package com.github.rushyverse.api.listener.api + +import com.github.rushyverse.api.AbstractKoinTest +import com.github.rushyverse.api.player.scoreboard.ScoreboardManager +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import net.kyori.adventure.text.Component +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerQuitEvent +import kotlin.test.BeforeTest +import kotlin.test.Test + +class ScoreboardListenerTest: AbstractKoinTest() { + + private lateinit var listener: ScoreboardListener + private lateinit var scoreboardManager: ScoreboardManager + + @BeforeTest + override fun onBefore() { + super.onBefore() + listener = ScoreboardListener() + scoreboardManager = mockk() + + loadApiTestModule { + single { scoreboardManager } + } + } + + @Test + fun `should call remove in manager when player leave`() = runTest { + val player = mockk() + coJustRun { scoreboardManager.remove(player) } + listener.onQuit(PlayerQuitEvent(player, Component.empty(), PlayerQuitEvent.QuitReason.DISCONNECTED)) + coVerify(exactly = 1) { scoreboardManager.remove(player) } + } + +} diff --git a/src/test/kotlin/com/github/rushyverse/api/player/ClientManagerImplTest.kt b/src/test/kotlin/com/github/rushyverse/api/player/ClientManagerImplTest.kt new file mode 100644 index 00000000..6b136001 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/player/ClientManagerImplTest.kt @@ -0,0 +1,173 @@ +package com.github.rushyverse.api.player + +import com.github.rushyverse.api.AbstractKoinTest +import com.github.rushyverse.api.player.exception.ClientNotFoundException +import com.github.rushyverse.api.utils.randomString +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.test.runTest +import org.bukkit.entity.Player +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import java.util.* +import kotlin.test.* + +class ClientManagerImplTest : AbstractKoinTest() { + + private lateinit var clientManager: ClientManager + + @BeforeTest + override fun onBefore() { + super.onBefore() + clientManager = ClientManagerImpl() + } + + @Test + fun `has no clients when created`() { + assertEquals(0, clientManager.clients.size) + } + + @Nested + @DisplayName("Put client") + inner class Put { + + @Test + fun `save client linked to the name of the player`() = runTest { + val player = createPlayerMock() + val client = createClient(player) + clientManager.put(player, client) + + val clients = clientManager.clients + assertEquals(1, clients.size) + assertEquals(client, clients.values.first()) + assertEquals(player.name, clients.keys.first()) + } + + @Test + fun `save client returns the previous client instance`() = runTest { + val player = createPlayerMock() + val client = createClient(player) + assertNull(clientManager.put(player, client)) + + val client2 = createClient(player) + assertEquals(client, clientManager.put(player, client2)) + + assertEquals(client2, clientManager.getClient(player)) + } + + @Test + fun `save client if there is no instance already linked`() = runTest { + val player = createPlayerMock() + val client = createClient(player) + assertNull(clientManager.put(player, client)) + + val client2 = createClient(player) + assertEquals(client, clientManager.putIfAbsent(player, client2)) + + assertEquals(client, clientManager.getClient(player)) + } + } + + @Nested + @DisplayName("Remove client") + inner class Remove { + + @Test + fun `returns null when client for player is not linked`() = runTest { + assertNull(clientManager.removeClient(createPlayerMock())) + } + + @Test + fun `returns the instance of client linked to the player`() = runTest { + val player = createPlayerMock() + val client = createClient(player) + clientManager.put(player, client) + assertEquals(client, clientManager.removeClient(player)) + } + + } + + @Nested + @DisplayName("Contains player") + inner class Contains { + + @Test + fun `contains a player`() = runTest { + val player = createPlayerMock() + clientManager.put(player, createClient(player)) + assertTrue { clientManager.contains(player) } + + val player2 = createPlayerMock() + clientManager.put(player2, createClient(player2)) + assertTrue { clientManager.contains(player) } + assertTrue { clientManager.contains(player2) } + } + } + + @Nested + @DisplayName("Get client") + inner class Get { + + @Test + fun `retrieve from player`() = runTest { + val player = createPlayerMock() + assertThrows { + clientManager.getClient(player) + } + + val client = createClient(player) + clientManager.put(player, client) + assertEquals(client, clientManager.getClient(player)) + } + + @Test + fun `retrieve from player or null`() = runTest { + val player = createPlayerMock() + assertNull(clientManager.getClientOrNull(player)) + + val client = createClient(player) + clientManager.put(player, client) + assertEquals(client, clientManager.getClientOrNull(player)) + } + + @Test + fun `retrieve from name`() = runTest { + val player = createPlayerMock() + val name = player.name + assertThrows { + clientManager.getClient(name) + } + + val client = createClient(player) + clientManager.put(player, client) + assertEquals(client, clientManager.getClient(name)) + } + + @Test + fun `retrieve from name or null`() = runTest { + val player = createPlayerMock() + val name = player.name + assertNull(clientManager.getClientOrNull(name)) + + val client = createClient(player) + clientManager.put(player, client) + assertEquals(client, clientManager.getClientOrNull(name)) + } + + } + + private fun createPlayerMock(): Player { + val name = randomString() + val player = mockk(name) + every { player.name } returns name + every { player.uniqueId } returns UUID.randomUUID() + return player + } + + private fun createClient(player: Player) = + Client(player.uniqueId, CoroutineScope(Dispatchers.Default + SupervisorJob())) +} diff --git a/src/test/kotlin/com/github/rushyverse/api/player/ClientTest.kt b/src/test/kotlin/com/github/rushyverse/api/player/ClientTest.kt new file mode 100644 index 00000000..554a6237 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/player/ClientTest.kt @@ -0,0 +1,58 @@ +package com.github.rushyverse.api.player + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.ServerMock +import be.seeseemelk.mockbukkit.entity.PlayerMock +import com.github.rushyverse.api.AbstractKoinTest +import com.github.rushyverse.api.player.exception.PlayerNotFoundException +import kotlinx.coroutines.CoroutineScope +import org.junit.jupiter.api.assertThrows +import java.util.* +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.test.* + +class ClientTest : AbstractKoinTest() { + + private lateinit var player: PlayerMock + private lateinit var serverMock: ServerMock + + @BeforeTest + override fun onBefore() { + super.onBefore() + serverMock = MockBukkit.mock().apply { + player = addPlayer() + } + } + + @AfterTest + override fun onAfter() { + MockBukkit.unmock() + super.onAfter() + } + + @Test + fun `retrieve player instance not found returns null`() { + val client = Client(UUID.randomUUID(), CoroutineScope(EmptyCoroutineContext)) + assertNull(client.player) + } + + @Test + fun `retrieve player instance found returns the instance`() { + val client = Client(player.uniqueId, CoroutineScope(EmptyCoroutineContext)) + assertEquals(player, client.player) + } + + @Test + fun `require player instance not found throws an exception`() { + val client = Client(UUID.randomUUID(), CoroutineScope(EmptyCoroutineContext)) + assertThrows { + client.requirePlayer() + } + } + + @Test + fun `require player instance found returns the instance`() { + val client = Client(player.uniqueId, CoroutineScope(EmptyCoroutineContext)) + assertEquals(player, client.requirePlayer()) + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/player/language/LanguageManagerTest.kt b/src/test/kotlin/com/github/rushyverse/api/player/language/LanguageManagerTest.kt new file mode 100644 index 00000000..0b4a15bb --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/player/language/LanguageManagerTest.kt @@ -0,0 +1,104 @@ +package com.github.rushyverse.api.player.language + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.ServerMock +import com.github.rushyverse.api.translation.SupportedLanguage +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Nested +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class LanguageManagerTest { + + private lateinit var manager: LanguageManager + + private lateinit var server: ServerMock + + @BeforeTest + fun onBefore() { + manager = LanguageManager() + server = MockBukkit.mock() + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Get { + + @Test + fun `should return the language associated with the player`() = runTest { + val player = server.addPlayer() + manager.set(player, SupportedLanguage.FRENCH) + manager.get(player) shouldBe SupportedLanguage.FRENCH + } + + @Test + fun `should return the default language if the player has no language`() = runTest { + val player = server.addPlayer() + manager.get(player) shouldBe SupportedLanguage.ENGLISH + } + + } + + @Nested + inner class Set { + + @Test + fun `should overwrite the language for the player`() = runTest { + val player = server.addPlayer() + manager.set(player, SupportedLanguage.FRENCH) + manager.get(player) shouldBe SupportedLanguage.FRENCH + + manager.set(player, SupportedLanguage.ENGLISH) + manager.get(player) shouldBe SupportedLanguage.ENGLISH + } + + @Test + fun `should set for several players`() = runTest { + val player1 = server.addPlayer() + val player2 = server.addPlayer() + + manager.set(player1, SupportedLanguage.FRENCH) + manager.set(player2, SupportedLanguage.GERMAN) + + manager.get(player1) shouldBe SupportedLanguage.FRENCH + manager.get(player2) shouldBe SupportedLanguage.GERMAN + } + + } + + @Nested + inner class Remove { + + @Test + fun `should remove the language associated with the player`() = runTest { + val player = server.addPlayer() + val player2 = server.addPlayer() + + manager.set(player, SupportedLanguage.FRENCH) + manager.get(player) shouldBe SupportedLanguage.FRENCH + + manager.set(player2, SupportedLanguage.GERMAN) + manager.get(player2) shouldBe SupportedLanguage.GERMAN + + manager.remove(player) + + manager.get(player) shouldBe SupportedLanguage.ENGLISH + manager.get(player2) shouldBe SupportedLanguage.GERMAN + } + + @Test + fun `should do nothing if the player has no language`() = runTest { + val player = server.addPlayer() + manager.remove(player) + + manager.get(player) shouldBe SupportedLanguage.ENGLISH + } + + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/position/AbstractAreaTest.kt b/src/test/kotlin/com/github/rushyverse/api/position/AbstractAreaTest.kt deleted file mode 100644 index 7c93ba96..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/position/AbstractAreaTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.github.rushyverse.api.position - -import com.github.rushyverse.api.utils.randomString -import io.mockk.mockk -import net.minestom.server.entity.Entity -import org.junit.jupiter.api.Nested -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class AbstractAreaTest { - - @Nested - inner class Instantiation { - - @Test - fun `should have no entities at the creation`() { - val area = object : AbstractArea() { - override fun updateEntitiesInArea(): Pair, Collection> { - error("Not implemented") - } - } - assertTrue { area.entitiesInArea.isEmpty() } - } - } - - @Nested - inner class Update { - - @Test - fun `should add all entities if list is empty`() { - val entities = setOf( - mockk(randomString()), - mockk(randomString()), - mockk(randomString()) - ) - val area = object : AbstractArea() { - override fun updateEntitiesInArea(): Pair, Collection> { - return update(entities) - } - } - - val (added, removed) = area.updateEntitiesInArea() - assertEquals(entities, added) - assertTrue { removed.isEmpty() } - assertEquals(entities, area.entitiesInArea) - } - - @Test - fun `should add and remove all entities if list not in entities list of area`() { - val entity1 = mockk(randomString()) - val entity2 = mockk(randomString()) - val entity3 = mockk(randomString()) - - var entities = setOf(entity1, entity2, entity3) - - val area = object : AbstractArea() { - override fun updateEntitiesInArea(): Pair, Collection> { - return update(entities) - } - } - area.updateEntitiesInArea() - - area.updateEntitiesInArea() - entities = setOf(entity1, entity2) - val (added, removed) = area.updateEntitiesInArea() - assertTrue { added.isEmpty() } - assertEquals(setOf(entity3), removed) - assertEquals(entities, area.entitiesInArea) - - val entity4 = mockk(randomString()) - entities = setOf(entity1, entity4) - val (added2, removed2) = area.updateEntitiesInArea() - assertEquals(setOf(entity4), added2) - assertEquals(setOf(entity2), removed2) - assertEquals(entities, area.entitiesInArea) - } - - @Test - fun `should not change entities in area if entities is always in area`() { - val entity1 = mockk(randomString()) - val entity2 = mockk(randomString()) - val entity3 = mockk(randomString()) - - val entities = setOf(entity1, entity2, entity3) - - val area = object : AbstractArea() { - override fun updateEntitiesInArea(): Pair, Collection> { - return update(entities) - } - } - val (added, removed) = area.updateEntitiesInArea() - assertEquals(entities, added) - assertTrue { removed.isEmpty() } - assertEquals(entities, area.entitiesInArea) - - val (added2, removed2) = area.updateEntitiesInArea() - assertTrue { added2.isEmpty() } - assertTrue { removed2.isEmpty() } - assertEquals(entities, area.entitiesInArea) - } - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/position/CubeAreaTest.kt b/src/test/kotlin/com/github/rushyverse/api/position/CubeAreaTest.kt deleted file mode 100644 index 8e7e907f..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/position/CubeAreaTest.kt +++ /dev/null @@ -1,260 +0,0 @@ -package com.github.rushyverse.api.position - -import com.github.rushyverse.api.utils.randomPos -import com.github.rushyverse.api.utils.randomString -import io.mockk.every -import io.mockk.mockk -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.Entity -import net.minestom.server.entity.Player -import net.minestom.server.instance.Instance -import org.junit.jupiter.api.Nested -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class CubeAreaTest { - - @Nested - inner class Instantiation { - - @Test - fun `should have no entities at the creation`() { - val area = CubeArea(mockk(), randomPos(), randomPos()) - assertTrue { area.entitiesInArea.isEmpty() } - } - - @Test - fun `should have the correct min and max positions`() { - val min = Pos(0.0, 10.0, -10.0) - val max = Pos(-20.0, 11.0, -16.0) - val area = CubeArea(mockk(), min, max) - assertEquals(Pos(-20.0, 10.0, -16.0), area.min) - assertEquals(Pos(0.0, 11.0, -10.0), area.max) - } - - @Test - fun `should have the correct min and max positions if min and max are already ordered`() { - val min = Pos(-1.0, -2.0, -3.0) - val max = Pos(0.0, 1.0, 2.0) - val area = CubeArea(mockk(), min, max) - assertEquals(min, area.min) - assertEquals(max, area.max) - } - - @Test - fun `position should be the center of the area`() { - val min = Pos(0.0, 10.0, -10.0) - val max = Pos(-20.0, 11.0, -16.0) - val area = CubeArea(mockk(), min, max) - assertEquals(Pos(-10.0, 10.5, -13.0), area.position) - } - - } - - @Nested - inner class SetPosition { - - @Test - fun `should keep the same position if the new value is the same`() { - val area = CubeArea(mockk(), Pos(0.0, 0.0, 0.0), Pos(10.5, 10.5, 10.5)) - val oldMin = area.min - val oldMax = area.max - area.position = area.position - assertEquals(oldMin, area.min) - assertEquals(oldMax, area.max) - } - - @Test - fun `should change the position if the new positive value is different`() { - val min = Pos(0.0, 0.0, 0.0) - val max = Pos(10.0, 10.0, 10.0) - val area = CubeArea(mockk(), min, max) - val newPosition = Pos(20.0, 20.0, 20.0) - area.position = newPosition - assertEquals(newPosition, area.position) - assertEquals(Pos(15.0, 15.0, 15.0), area.min) - assertEquals(Pos(25.0, 25.0, 25.0), area.max) - } - - @Test - fun `should change the position if the new negative value is different`() { - val min = Pos(0.0, 0.0, 0.0) - val max = Pos(10.0, 10.0, 10.0) - val area = CubeArea(mockk(), min, max) - val newPosition = Pos(-20.0, -20.0, -20.0) - area.position = newPosition - assertEquals(newPosition, area.position) - assertEquals(Pos(-25.0, -25.0, -25.0), area.min) - assertEquals(Pos(-15.0, -15.0, -15.0), area.max) - } - - @Test - fun `should change the position if the new mixed value is different`() { - val min = Pos(0.0, 0.0, 0.0) - val max = Pos(10.0, 10.0, 10.0) - val area = CubeArea(mockk(), min, max) - val newPosition = Pos(20.0, -20.0, -20.0) - area.position = newPosition - assertEquals(newPosition, area.position) - assertEquals(Pos(15.0, -25.0, -25.0), area.min) - assertEquals(Pos(25.0, -15.0, -15.0), area.max) - } - - } - - @Nested - inner class GetPosition { - - @Test - fun `should return the center of the area with positive values`() { - val min = Pos(10.0, 10.0, 10.0) - val max = Pos(20.0, 20.0, 20.0) - val area = CubeArea(mockk(), min, max) - assertEquals(Pos(15.0, 15.0, 15.0), area.position) - } - - @Test - fun `should return the center of the area with negative values`() { - val min = Pos(-20.0, -20.0, -20.0) - val max = Pos(-10.0, -10.0, -10.0) - val area = CubeArea(mockk(), min, max) - assertEquals(Pos(-15.0, -15.0, -15.0), area.position) - } - - @Test - fun `should return the center of the area with mixed values`() { - val min = Pos(-20.0, 10.0, -20.0) - val max = Pos(-10.0, 20.0, -10.0) - val area = CubeArea(mockk(), min, max) - assertEquals(Pos(-15.0, 15.0, -15.0), area.position) - } - - @Test - fun `should return the center of the area with decimal value`() { - val min = Pos(10.6, 10.8, 10.4) - val max = Pos(10.0, 10.0, 20.0) - val area = CubeArea(mockk(), min, max) - assertEquals(Pos(10.3, 10.4, 15.2), area.position) - } - } - - @Nested - inner class UpdateEntitiesInArea { - - @Test - fun `should have filtered entities on type`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val player2 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, player2, entity) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CubeArea(instance, min, min) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, player2), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, player2), area.entitiesInArea) - } - - @Test - fun `should have entities in the area`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(10.0, 10.0, 10.0) - } - val entity2 = mockk(randomString()) { - every { position } returns Pos(11.0, 5.0, 5.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, entity, entity2) - } - - val min = Pos(0.0, 0.0, 0.0) - val max = Pos(10.0, 10.0, 10.0) - val area = CubeArea(instance, min, max) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, entity), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, entity), area.entitiesInArea) - } - - @Test - fun `should remove entities in the area`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(10.0, 10.0, 10.0) - } - val entity2 = mockk(randomString()) { - every { position } returns Pos(11.0, 5.0, 5.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, entity, entity2) - } - - val min = Pos(0.0, 0.0, 0.0) - val max = Pos(10.0, 10.0, 10.0) - val area = CubeArea(instance, min, max) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, entity), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, entity), area.entitiesInArea) - - every { player.position } returns Pos(11.0, 5.0, 5.0) - every { entity2.position } returns Pos(0.0, 6.0, 0.0) - - val (added2, removed2) = area.updateEntitiesInArea() - assertContentEquals(listOf(entity2), added2) - assertContentEquals(listOf(player), removed2) - assertContentEquals(listOf(entity, entity2), area.entitiesInArea) - } - - @Test - fun `should not change entities in area if entities is always in area`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity2 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, entity, entity2) - } - - val min = Pos(0.0, 0.0, 0.0) - val max = Pos(10.0, 10.0, 10.0) - val area = CubeArea(instance, min, max) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, entity, entity2), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, entity, entity2), area.entitiesInArea) - - val (added2, removed2) = area.updateEntitiesInArea() - assertContentEquals(emptyList(), added2) - assertContentEquals(emptyList(), removed2) - assertContentEquals(listOf(player, entity, entity2), area.entitiesInArea) - } - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/position/CylinderAreaTest.kt b/src/test/kotlin/com/github/rushyverse/api/position/CylinderAreaTest.kt deleted file mode 100644 index feecc865..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/position/CylinderAreaTest.kt +++ /dev/null @@ -1,353 +0,0 @@ -package com.github.rushyverse.api.position - -import com.github.rushyverse.api.utils.randomPos -import com.github.rushyverse.api.utils.randomString -import io.mockk.every -import io.mockk.mockk -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.Entity -import net.minestom.server.entity.Player -import net.minestom.server.instance.Instance -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.assertThrows -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class CylinderAreaTest { - - @Nested - inner class Instantiation { - - @Test - fun `should have no entities at the creation`() { - val area = CylinderArea(mockk(), randomPos(), 0.0, 0.0..0.0) - assertTrue { area.entitiesInArea.isEmpty() } - } - - @Test - fun `should throw an exception if the radius is negative`() { - assertThrows { - CylinderArea(mockk(), randomPos(), -1.0, 0.0..0.0) - } - } - - @Test - fun `should throw an exception if the radius is set`() { - val area = CylinderArea(mockk(), randomPos(), 0.0, 0.0..0.0) - assertThrows { - area.radius = -1.0 - } - } - - @Test - fun `should set the radius without exception if value is zero or positive`() { - val area = CylinderArea(mockk(), randomPos(), 0.0, 0.0..0.0) - - area.radius = 0.0 - assertEquals(0.0, area.radius) - - area.radius = 1.0 - assertEquals(1.0, area.radius) - } - } - - @Nested - inner class UpdateWithYChange { - - @Test - fun `should use negative y limit`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, -5.0, 0.0) - } - val player2 = mockk(randomString()) { - every { position } returns Pos(0.0, -8.1, 0.0) - } - val player3 = mockk(randomString()) { - every { position } returns Pos(0.0, -11.0, 0.0) - } - val player4 = mockk(randomString()) { - every { position } returns Pos(0.0, -4.9, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, player2, player3, player4) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 1.0, -10.0..-5.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, player2), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, player2), area.entitiesInArea) - } - - @Test - fun `should use positive y limit`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 5.0, 0.0) - } - val player2 = mockk(randomString()) { - every { position } returns Pos(0.0, 7.3, 0.0) - } - val player3 = mockk(randomString()) { - every { position } returns Pos(0.0, 4.3, 0.0) - } - val player4 = mockk(randomString()) { - every { position } returns Pos(0.0, 10.1, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, player2, player3, player4) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 0.0, 5.0..10.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, player2), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, player2), area.entitiesInArea) - } - - @Test - fun `should use negative and positive y limit`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val player2 = mockk(randomString()) { - every { position } returns Pos(0.0, -3.0, 0.0) - } - val player3 = mockk(randomString()) { - every { position } returns Pos(0.0, 8.0, 0.0) - } - val player4 = mockk(randomString()) { - every { position } returns Pos(0.0, 10.1, 0.0) - } - val player5 = mockk(randomString()) { - every { position } returns Pos(0.0, -5.1, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, player2, player3, player4, player5) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 0.0, -5.0..10.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, player2, player3), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, player2, player3), area.entitiesInArea) - } - - @Test - fun `should use zero y limit`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val player2 = mockk(randomString()) { - every { position } returns Pos(0.0, -0.1, 0.0) - } - val player3 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.1, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, player2, player3) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 0.0, 0.0..0.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player), area.entitiesInArea) - } - - } - - @Nested - inner class UpdateWithRadiusChange { - - @Test - fun `should use zero for radius`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val player2 = mockk(randomString()) { - every { position } returns Pos(0.1, 0.0, 0.0) - } - val player3 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.1) - } - val player4 = mockk(randomString()) { - every { position } returns Pos(-0.1, 0.0, 0.0) - } - val player5 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, -0.1) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, player2, player3, player4, player5) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 0.0, 0.0..0.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player), area.entitiesInArea) - } - - @Test - fun `should use positive for radius`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val player2 = mockk(randomString()) { - every { position } returns Pos(1.0, 0.0, 0.0) - } - val player3 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 1.0) - } - val player4 = mockk(randomString()) { - every { position } returns Pos(-1.0, 0.0, 0.0) - } - val player5 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, -1.0) - } - val player6 = mockk(randomString()) { - every { position } returns Pos(1.0, 0.0, 0.1) - } - val player7 = mockk(randomString()) { - every { position } returns Pos(-1.0, 0.0, -0.1) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, player2, player3, player4, player5, player6, player7) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 1.0, 0.0..0.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, player2, player3, player4, player5), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, player2, player3, player4, player5), area.entitiesInArea) - } - } - - @Nested - inner class UpdateEntitiesInArea { - - @Test - fun `should have filtered entities on type`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val player2 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, player2, entity) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 1.0, 0.0..0.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, player2), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, player2), area.entitiesInArea) - } - - @Test - fun `should have entities in the area`() { - val player = mockk(randomString()) { - every { position } returns Pos(1.0, 2.0, 4.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(2.0, 2.0, 4.0) - } - val entity2 = mockk(randomString()) { - every { position } returns Pos(5.0, 5.0, 5.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, entity, entity2) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 5.0, 0.0..2.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, entity), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, entity), area.entitiesInArea) - } - - @Test - fun `should remove entities in the area`() { - val player = mockk(randomString()) { - every { position } returns Pos(1.0, 2.0, 4.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(2.0, 2.0, 4.0) - } - val entity2 = mockk(randomString()) { - every { position } returns Pos(5.0, 5.0, 5.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, entity, entity2) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 5.0, 0.0..3.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, entity), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, entity), area.entitiesInArea) - - every { player.position } returns Pos(10.0, 0.0, 0.0) - every { entity2.position } returns Pos(0.0, 1.0, 0.0) - - val (added2, removed2) = area.updateEntitiesInArea() - assertContentEquals(listOf(entity2), added2) - assertContentEquals(listOf(player), removed2) - assertContentEquals(listOf(entity, entity2), area.entitiesInArea) - } - - @Test - fun `should not change entities in area if entities is always in area`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity2 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, entity, entity2) - } - - val min = Pos(0.0, 0.0, 0.0) - val area = CylinderArea(instance, min, 5.0, 0.0..3.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, entity, entity2), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, entity, entity2), area.entitiesInArea) - - val (added2, removed2) = area.updateEntitiesInArea() - assertContentEquals(emptyList(), added2) - assertContentEquals(emptyList(), removed2) - assertContentEquals(listOf(player, entity, entity2), area.entitiesInArea) - } - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/position/MultiAreaTest.kt b/src/test/kotlin/com/github/rushyverse/api/position/MultiAreaTest.kt deleted file mode 100644 index f09b8056..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/position/MultiAreaTest.kt +++ /dev/null @@ -1,247 +0,0 @@ -package com.github.rushyverse.api.position - -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import net.minestom.server.entity.Entity -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class MultiAreaTest { - - @Nested - inner class Instantiation { - - @Test - fun `should have no entities at the creation`() { - val area = MultiArea() - assertTrue { area.areas.isEmpty() } - assertTrue { area.entitiesInArea.isEmpty() } - } - - @Test - fun `should have the areas passed in the constructor`() { - val area1 = mockk>() - val area2 = mockk>() - val area = MultiArea(mutableSetOf(area1, area2)) - assertEquals(setOf(area1, area2), area.areas) - } - - } - - @Nested - inner class AddArea { - - @Test - fun `should add an area`() { - val area = MultiArea() - val area2 = mockk>() - assertTrue { area.addArea(area2) } - assertEquals(1, area.areas.size) - } - - @Test - fun `should not add an area twice`() { - val area = MultiArea() - val area2 = mockk>() - assertTrue { area.addArea(area2) } - assertFalse { area.addArea(area2) } - assertEquals(1, area.areas.size) - } - - } - - @Nested - inner class RemoveArea { - - @Test - fun `should remove an area`() { - val area = MultiArea() - val area2 = mockk>() - assertTrue { area.addArea(area2) } - assertTrue { area.removeArea(area2) } - assertEquals(0, area.areas.size) - } - - @Test - fun `should not remove an area if it is not in the list`() { - val area = MultiArea() - val area2 = mockk>() - assertFalse { area.removeArea(area2) } - assertEquals(0, area.areas.size) - } - - } - - @Nested - inner class RemoveAllAreas { - - @Test - fun `should do nothing if there are no areas`() { - val area = MultiArea() - area.removeAllAreas() - assertEquals(0, area.areas.size) - } - - @Test - fun `should remove all areas`() { - val area = MultiArea() - val area2 = mockk>() - val area3 = mockk>() - assertTrue { area.addArea(area2) } - assertTrue { area.addArea(area3) } - area.removeAllAreas() - assertEquals(0, area.areas.size) - } - - } - - @Nested - inner class UpdateEntitiesInArea { - - @Test - fun `should call updateEntitiesInArea on all areas`() { - val area = MultiArea() - val area2 = mockk>() { - every { updateEntitiesInArea() } returns Pair(emptyList(), emptyList()) - every { entitiesInArea } returns emptySet() - } - val area3 = mockk>() { - every { updateEntitiesInArea() } returns Pair(emptyList(), emptyList()) - every { entitiesInArea } returns emptySet() - } - - area.addArea(area2) - area.addArea(area3) - area.updateEntitiesInArea() - - verify(exactly = 1) { area2.updateEntitiesInArea() } - verify(exactly = 1) { area2.entitiesInArea } - verify(exactly = 1) { area3.updateEntitiesInArea() } - verify(exactly = 1) { area3.entitiesInArea } - } - - @Test - fun `should register only one times an entity if in several areas`() { - val area = MultiArea() - val entity1 = mockk() - val entity2 = mockk() - - val area2 = mockk>() { - every { updateEntitiesInArea() } returns Pair(listOf(entity1, entity2), emptyList()) - every { entitiesInArea } returns setOf(entity1, entity2) - } - val area3 = mockk>() { - every { updateEntitiesInArea() } returns Pair(listOf(entity1), emptyList()) - every { entitiesInArea } returns setOf(entity1) - } - - area.addArea(area2) - area.addArea(area3) - val (added, removed) = area.updateEntitiesInArea() - - assertTrue { removed.isEmpty() } - - val expectedRegisteredEntities = setOf(entity1, entity2) - assertEquals(expectedRegisteredEntities, added) - assertEquals(expectedRegisteredEntities, area.entitiesInArea) - } - - @Test - fun `should remove an entity if it is not in any area`() { - val area = MultiArea() - val entity1 = mockk() - val entity2 = mockk() - - val area2 = mockk>() { - every { updateEntitiesInArea() } returns Pair(listOf(entity1, entity2), emptyList()) - every { entitiesInArea } returns setOf(entity1, entity2) - } - val area3 = mockk>() { - every { updateEntitiesInArea() } returns Pair(listOf(entity1), emptyList()) - every { entitiesInArea } returns setOf(entity1) - } - - area.addArea(area2) - area.addArea(area3) - area.updateEntitiesInArea() - - every { area2.updateEntitiesInArea() } returns Pair(emptyList(), listOf(entity1)) - every { area2.entitiesInArea } returns setOf(entity2) - every { area3.updateEntitiesInArea() } returns Pair(emptyList(), listOf(entity1)) - every { area3.entitiesInArea } returns setOf() - - val (added, removed) = area.updateEntitiesInArea() - - assertEquals(setOf(entity1), removed) - assertTrue { added.isEmpty() } - assertEquals(setOf(entity2), area.entitiesInArea) - } - - @Test - fun `should not remove an entity if he's at least in an area`() { - val area = MultiArea() - val entity1 = mockk() - val entity2 = mockk() - - val area2 = mockk>() { - every { updateEntitiesInArea() } returns Pair(listOf(entity1, entity2), emptyList()) - every { entitiesInArea } returns setOf(entity1, entity2) - } - val area3 = mockk>() { - every { updateEntitiesInArea() } returns Pair(listOf(entity1), emptyList()) - every { entitiesInArea } returns setOf(entity1) - } - - area.addArea(area2) - area.addArea(area3) - area.updateEntitiesInArea() - - every { area2.updateEntitiesInArea() } returns Pair(emptyList(), listOf(entity1)) - every { area2.entitiesInArea } returns setOf(entity2) - every { area3.updateEntitiesInArea() } returns Pair(emptyList(), emptyList()) - every { area3.entitiesInArea } returns setOf(entity1) - - val (added, removed) = area.updateEntitiesInArea() - - assertTrue { removed.isEmpty() } - assertTrue { added.isEmpty() } - assertEquals(setOf(entity1, entity2), area.entitiesInArea) - } - - @Test - fun `should not change entities in area if entities is always in areas`() { - val area = MultiArea() - val entity1 = mockk() - val entity2 = mockk() - - val area2 = mockk>() { - every { updateEntitiesInArea() } returns Pair(listOf(entity1, entity2), emptyList()) - every { entitiesInArea } returns setOf(entity1, entity2) - } - val area3 = mockk>() { - every { updateEntitiesInArea() } returns Pair(listOf(entity1, entity2), emptyList()) - every { entitiesInArea } returns setOf(entity1, entity2) - } - - area.addArea(area2) - area.addArea(area3) - area.updateEntitiesInArea() - - every { area2.updateEntitiesInArea() } returns Pair(emptyList(), emptyList()) - every { area2.entitiesInArea } returns setOf(entity1, entity2) - every { area3.updateEntitiesInArea() } returns Pair(emptyList(), emptyList()) - every { area3.entitiesInArea } returns setOf(entity1, entity2) - - val (added, removed) = area.updateEntitiesInArea() - - assertTrue { removed.isEmpty() } - assertTrue { added.isEmpty() } - assertEquals(setOf(entity1, entity2), area.entitiesInArea) - } - - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/position/SphereAreaTest.kt b/src/test/kotlin/com/github/rushyverse/api/position/SphereAreaTest.kt deleted file mode 100644 index 98dfe39f..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/position/SphereAreaTest.kt +++ /dev/null @@ -1,183 +0,0 @@ -package com.github.rushyverse.api.position - -import com.github.rushyverse.api.utils.randomPos -import com.github.rushyverse.api.utils.randomString -import io.mockk.every -import io.mockk.mockk -import net.minestom.server.coordinate.Pos -import net.minestom.server.entity.Entity -import net.minestom.server.entity.Player -import net.minestom.server.instance.Instance -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.assertThrows -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class SphereAreaTest { - - @Nested - inner class Instantiation { - - @Test - fun `should have no entities at the creation`() { - val area = SphereArea(mockk(), randomPos(), 0.0) - assertTrue { area.entitiesInArea.isEmpty() } - } - - @Test - fun `should throw an exception if the radius is set`() { - val area = SphereArea(mockk(), randomPos(), 0.0) - assertThrows { - area.radius = -1.0 - } - } - - @Test - fun `should set the radius without exception if value is zero or positive`() { - val area = SphereArea(mockk(), randomPos(), 0.0) - - area.radius = 0.0 - assertEquals(0.0, area.radius) - - area.radius = 1.0 - assertEquals(1.0, area.radius) - } - } - - @Nested - inner class UpdateEntitiesInArea { - - @Test - fun `should have filtered entities on type`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val player2 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, player2, entity) - every { getNearbyEntities(any(), any()) } answers { - val pos = arg(0) - val range = arg(1) - entities.filter { it.position.distance(pos) <= range } - } - } - - val min = Pos(0.0, 0.0, 0.0) - val area = SphereArea(instance, min, 1.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, player2), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, player2), area.entitiesInArea) - } - - @Test - fun `should have entities in the area`() { - val player = mockk(randomString()) { - every { position } returns Pos(1.0, 2.0, 4.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(2.0, 2.0, 4.0) - } - val entity2 = mockk(randomString()) { - every { position } returns Pos(5.0, 5.0, 5.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, entity, entity2) - every { getNearbyEntities(any(), any()) } answers { - val pos = arg(0) - val range = arg(1) - entities.filter { it.position.distance(pos) <= range } - } - } - - val min = Pos(0.0, 0.0, 0.0) - val area = SphereArea(instance, min, 5.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, entity), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, entity), area.entitiesInArea) - } - - @Test - fun `should remove entities in the area`() { - val player = mockk(randomString()) { - every { position } returns Pos(1.0, 2.0, 4.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(2.0, 2.0, 4.0) - } - val entity2 = mockk(randomString()) { - every { position } returns Pos(5.0, 5.0, 5.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, entity, entity2) - every { getNearbyEntities(any(), any()) } answers { - val pos = arg(0) - val range = arg(1) - entities.filter { it.position.distance(pos) <= range } - } - } - - val min = Pos(0.0, 0.0, 0.0) - val area = SphereArea(instance, min, 5.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, entity), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, entity), area.entitiesInArea) - - every { player.position } returns Pos(10.0, 0.0, 0.0) - every { entity2.position } returns Pos(0.0, 5.0, 0.0) - - val (added2, removed2) = area.updateEntitiesInArea() - assertContentEquals(listOf(entity2), added2) - assertContentEquals(listOf(player), removed2) - assertContentEquals(listOf(entity, entity2), area.entitiesInArea) - } - - @Test - fun `should not change entities in area if entities is always in areas`() { - val player = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val entity2 = mockk(randomString()) { - every { position } returns Pos(0.0, 0.0, 0.0) - } - val instance = mockk(randomString()) { - every { entities } returns setOf(player, entity, entity2) - every { getNearbyEntities(any(), any()) } answers { - val pos = arg(0) - val range = arg(1) - entities.filter { it.position.distance(pos) <= range } - } - } - - val min = Pos(0.0, 0.0, 0.0) - val area = SphereArea(instance, min, 5.0) - val (added, removed) = area.updateEntitiesInArea() - - assertContentEquals(listOf(player, entity, entity2), added) - assertContentEquals(emptyList(), removed) - assertContentEquals(listOf(player, entity, entity2), area.entitiesInArea) - - val (added2, removed2) = area.updateEntitiesInArea() - assertContentEquals(emptyList(), added2) - assertContentEquals(emptyList(), removed2) - assertContentEquals(listOf(player, entity, entity2), area.entitiesInArea) - } - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/schedule/SchedulerTaskTest.kt b/src/test/kotlin/com/github/rushyverse/api/schedule/SchedulerTaskTest.kt new file mode 100644 index 00000000..61249e64 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/schedule/SchedulerTaskTest.kt @@ -0,0 +1,456 @@ +package com.github.rushyverse.api.schedule + +import com.github.rushyverse.api.utils.randomString +import kotlinx.coroutines.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes + +class SchedulerTaskTest { + + @Test + fun `no task when created`() { + assertTrue { SchedulerTask(scope(), 1.minutes).tasks.isEmpty() } + } + + @Nested + inner class Add { + + @Nested + inner class Index { + + @Test + fun `out of the list`() { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + assertThrows { + scheduler.addAtUnsafe(-1) {} + } + + assertThrows { + scheduler.addAtUnsafe(1) {} + } + + runBlocking { + scheduler.add {} + } + + assertThrows { + scheduler.addAtUnsafe(2) {} + } + } + + @Test + fun `during running`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + scheduler.start() + + val body1: suspend SchedulerTask.Task.() -> Unit = { } + val id1 = randomString() + val task1 = scheduler.addAt(0, id1, body1) + assertEquals(id1, task1.id) + assertEquals(scheduler, task1.parent) + assertEquals(body1, task1.body) + assertEquals(listOf(task1), scheduler.tasks) + + val body2: suspend SchedulerTask.Task.() -> Unit = { } + val id2 = randomString() + val task2 = scheduler.addAt(1, id2, body2) + assertEquals(id2, task2.id) + assertEquals(scheduler, task2.parent) + assertEquals(body2, task2.body) + assertEquals(listOf(task1, task2), scheduler.tasks) + + val body3: suspend SchedulerTask.Task.() -> Unit = { } + val id3 = randomString() + val task3 = scheduler.addAt(1, id3, body3) + assertEquals(id3, task3.id) + assertEquals(scheduler, task3.parent) + assertEquals(body3, task3.body) + assertEquals(listOf(task1, task3, task2), scheduler.tasks) + } + + @Test + fun `during idle`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + val body1: suspend SchedulerTask.Task.() -> Unit = { } + val id1 = randomString() + val task1 = scheduler.addAtUnsafe(0, id1, body1) + assertEquals(id1, task1.id) + assertEquals(scheduler, task1.parent) + assertEquals(body1, task1.body) + assertEquals(listOf(task1), scheduler.tasks) + + val body2: suspend SchedulerTask.Task.() -> Unit = { } + val id2 = randomString() + val task2 = scheduler.addAtUnsafe(1, id2, body2) + assertEquals(id2, task2.id) + assertEquals(scheduler, task2.parent) + assertEquals(body2, task2.body) + assertEquals(listOf(task1, task2), scheduler.tasks) + + val body3: suspend SchedulerTask.Task.() -> Unit = { } + val id3 = randomString() + val task3 = scheduler.addAtUnsafe(0, id3, body3) + assertEquals(id3, task3.id) + assertEquals(scheduler, task3.parent) + assertEquals(body3, task3.body) + assertEquals(listOf(task3, task1, task2), scheduler.tasks) + } + } + + @Test + fun `during running`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + scheduler.start() + + val body1: suspend SchedulerTask.Task.() -> Unit = { } + val id1 = randomString() + val task1 = scheduler.add(id1, body1) + assertEquals(id1, task1.id) + assertEquals(scheduler, task1.parent) + assertEquals(body1, task1.body) + assertEquals(listOf(task1), scheduler.tasks) + + val body2: suspend SchedulerTask.Task.() -> Unit = { } + val id2 = randomString() + val task2 = scheduler.add(id2, body2) + assertEquals(id2, task2.id) + assertEquals(scheduler, task2.parent) + assertEquals(body2, task2.body) + assertEquals(listOf(task1, task2), scheduler.tasks) + } + + @Test + fun `during idle`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + val body1: suspend SchedulerTask.Task.() -> Unit = { } + val id1 = randomString() + val task1 = scheduler.addUnsafe(id1, body1) + assertEquals(id1, task1.id) + assertEquals(scheduler, task1.parent) + assertEquals(body1, task1.body) + assertEquals(listOf(task1), scheduler.tasks) + + val body2: suspend SchedulerTask.Task.() -> Unit = { } + val id2 = randomString() + val task2 = scheduler.addUnsafe(id2, body2) + assertEquals(id2, task2.id) + assertEquals(scheduler, task2.parent) + assertEquals(body2, task2.body) + assertEquals(listOf(task1, task2), scheduler.tasks) + } + } + + @Nested + inner class Remove { + + @Nested + inner class Index { + + @Test + fun `out of the list`() { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + assertThrows { + scheduler.removeAtUnsafe(-1) + } + + assertThrows { + scheduler.removeAtUnsafe(0) + } + + runBlocking { + scheduler.add {} + } + + assertThrows { + scheduler.removeAtUnsafe(1) + } + } + + @Test + fun `during running`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + val task1 = scheduler.add { } + scheduler.add { } + val task3 = scheduler.add { } + scheduler.start() + + scheduler.removeAt(1) + assertEquals(listOf(task1, task3), scheduler.tasks) + scheduler.removeAt(1) + assertEquals(listOf(task1), scheduler.tasks) + scheduler.removeAt(0) + assertEquals(listOf(), scheduler.tasks) + } + + @Test + fun `during idle`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + val task1 = scheduler.add { } + val task2 = scheduler.add { } + scheduler.add { } + scheduler.start() + + scheduler.removeAt(2) + assertEquals(listOf(task1, task2), scheduler.tasks) + scheduler.removeAt(0) + assertEquals(listOf(task2), scheduler.tasks) + scheduler.removeAt(0) + assertEquals(listOf(), scheduler.tasks) + } + } + + @Test + fun `during running`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + scheduler.start() + + val task1 = scheduler.add {} + val task2 = scheduler.add {} + + assertTrue { scheduler.remove(task1.id) } + assertEquals(listOf(task2), scheduler.tasks) + assertTrue { scheduler.remove(task2.id) } + assertEquals(listOf(), scheduler.tasks) + } + + @Test + fun `during idle`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + val task1 = scheduler.add {} + val task2 = scheduler.add {} + + assertTrue { scheduler.removeUnsafe(task1.id) } + assertEquals(listOf(task2), scheduler.tasks) + assertTrue { scheduler.removeUnsafe(task2.id) } + assertEquals(listOf(), scheduler.tasks) + } + } + + @Nested + @DisplayName("Delay before first execution") + inner class DelayBefore { + + @Test + fun `enable option`() = runBlocking { + var isExecuted = false + val scheduler = SchedulerTask( + scope(), + 1.minutes, + delayBefore = true + ) + + scheduler.addUnsafe { + isExecuted = true + } + + scheduler.start() + delay(100) + assertFalse { isExecuted } + } + + @Test + fun `disable option`() = runBlocking { + var isExecuted = false + val scheduler = SchedulerTask( + scope(), + 1.minutes, + delayBefore = false + ) + + scheduler.addUnsafe { + isExecuted = true + } + + scheduler.start() + delay(100) + assertTrue { isExecuted } + } + } + + @Nested + @DisplayName("Safe in case of error") + inner class Error { + + @Test + fun `continue schedule if error into the body`() = runBlocking { + var counter = 0 + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + scheduler.addUnsafe { + counter++ + error("Error in body") + } + + scheduler.start() + val countDownLatch = CountDownLatch(10) + while (counter <= 1) { + assertFalse { countDownLatch.await(10, TimeUnit.MILLISECONDS) } + countDownLatch.countDown() + } + + assertTrue { counter >= 2 } + + } + } + + @Nested + inner class Run { + + @Test + fun `body is executed several times`() = runBlocking { + var counter = 0 + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + scheduler.addUnsafe { counter++ } + + scheduler.start() + delay(100) + assertTrue { counter in 2..10 } + } + + @Test + fun `is running enabled when the scheduler is executed`() { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + assertFalse { scheduler.running } + scheduler.start() + assertTrue { scheduler.running } + } + + @Test + fun `can be stop and re-start`() = runBlocking { + var counter = 0 + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + scheduler.addUnsafe { counter++ } + + scheduler.start() + scheduler.cancel() + assertFalse { scheduler.running } + + counter = 0 + scheduler.start() + assertTrue { scheduler.running } + delay(50) + assertTrue { counter > 1 } + } + + @Test + fun `coroutine to run scheduler is the children of the coroutine scope`() { + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val scheduler = SchedulerTask( + scope, + 10.milliseconds + ) + + scheduler.start() + scope.cancel("Cancel parent") + assertFalse { scheduler.running } + } + + } + + @Nested + inner class Cancel { + + @Test + fun `cancel will change the state of the scheduler`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds + ) + + scheduler.start() + assertTrue { scheduler.running } + scheduler.cancel() + assertFalse { scheduler.running } + } + + @Test + fun `cancel when no task`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds, + stopWhenNoTask = true + ) + val task = scheduler.add { } + scheduler.start() + assertTrue { scheduler.running } + scheduler.remove(task.id) + assertFalse { scheduler.running } + } + + @Test + fun `keep alive despite missing task`() = runBlocking { + val scheduler = SchedulerTask( + scope(), + 10.milliseconds, + stopWhenNoTask = false + ) + val task = scheduler.add { } + scheduler.start() + assertTrue { scheduler.running } + scheduler.remove(task.id) + assertTrue { scheduler.running } + } + + } + + private fun scope() = CoroutineScope(Dispatchers.IO + SupervisorJob()) +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/ComponentSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/ComponentSerializerTest.kt new file mode 100644 index 00000000..97bdb5f9 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/ComponentSerializerTest.kt @@ -0,0 +1,230 @@ +package com.github.rushyverse.api.serializer + +import com.github.rushyverse.api.utils.randomString +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.Json +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextColor +import net.kyori.adventure.text.format.TextDecoration +import org.junit.jupiter.api.Nested +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.ValueSource +import kotlin.test.Test + +class ComponentSerializerTest { + + @Nested + inner class Serialize { + + @ParameterizedTest + @ValueSource( + strings = [ + "", + " ", + "My string" + ] + ) + fun `without decoration`(value: String) { + val component = Component.text(value) + Json.encodeToString(ComponentSerializer, component) shouldEqualJson """ + "$value" + """.trimIndent() + } + + @Test + fun `with named color`() { + fun assertColor(color: NamedTextColor) { + val string = randomString() + val colorName = color.toString() + val component = Component.text(string, color) + Json.encodeToString(ComponentSerializer, component) shouldEqualJson """ + "<$colorName>$string" + """.trimIndent() + } + + assertColor(NamedTextColor.AQUA) + assertColor(NamedTextColor.BLACK) + assertColor(NamedTextColor.BLUE) + assertColor(NamedTextColor.DARK_AQUA) + assertColor(NamedTextColor.DARK_BLUE) + } + + @Test + fun `with custom color`() { + fun assertColor(hex: String, color: NamedTextColor? = null) { + val string = randomString() + val component = Component.text(string, TextColor.fromHexString(hex)) + + val expected = if (color != null) { + val colorName = color.toString() + """ + "<$colorName>$string" + """.trimIndent() + } else { + val colorName = hex.lowercase() + """ + "<$colorName>$string" + """.trimIndent() + } + + Json.encodeToString(ComponentSerializer, component) shouldEqualJson expected + } + + assertColor("#e3e3e3") + assertColor("#000000", NamedTextColor.BLACK) + assertColor("#FFFFFF", NamedTextColor.WHITE) + assertColor("#FF0000") + } + + @ParameterizedTest + @EnumSource(TextDecoration::class) + fun `with decoration`(decoration: TextDecoration) { + val string = randomString() + val component = Component.text(string).decorate(decoration) + val decorationName = decoration.toString() + Json.encodeToString(ComponentSerializer, component) shouldEqualJson """ + "<$decorationName>$string" + """.trimIndent() + } + + @Test + fun `with color and decoration`() { + val string = randomString() + val component = Component.text(string, NamedTextColor.AQUA).decorate(TextDecoration.BOLD) + Json.encodeToString(ComponentSerializer, component) shouldEqualJson """ + "$string" + """.trimIndent() + } + + + } + + @Nested + inner class Deserialize { + + @ParameterizedTest + @ValueSource( + strings = [ + "", + " ", + "My string" + ] + ) + fun `without decoration`(value: String) { + Json.decodeFromString( + ComponentSerializer, + """ + "$value" + """.trimIndent() + ) shouldBe Component.text(value) + } + + @Test + fun `with named color`() { + fun assertColor(color: NamedTextColor) { + val string = randomString() + val colorName = color.toString() + val expectedComponent = Component.text(string).color(color) + + // Non strict mode supported + Json.decodeFromString( + ComponentSerializer, + """ + "<$colorName>$string" + """.trimIndent() + ) shouldBe expectedComponent + + // Strict mode supported + Json.decodeFromString( + ComponentSerializer, + """ + "<$colorName>$string" + """.trimIndent() + ) shouldBe expectedComponent + } + + assertColor(NamedTextColor.AQUA) + assertColor(NamedTextColor.BLACK) + assertColor(NamedTextColor.BLUE) + assertColor(NamedTextColor.DARK_AQUA) + assertColor(NamedTextColor.DARK_BLUE) + } + + @Test + fun `with custom color`() { + fun assertColor(hex: String) { + val string = randomString() + val expectedComponent = Component.text(string, TextColor.fromHexString(hex)) + + // Non strict mode supported + Json.decodeFromString( + ComponentSerializer, + """ + "<$hex>$string" + """.trimIndent() + ) shouldBe expectedComponent + + // Strict mode supported + Json.decodeFromString( + ComponentSerializer, + """ + "<$hex>$string" + """.trimIndent() + ) shouldBe expectedComponent + } + + assertColor("#e3e3e3") + assertColor("#000000") + assertColor("#FFFFFF") + assertColor("#FF0000") + } + + @ParameterizedTest + @EnumSource(TextDecoration::class) + fun `with decoration`(decoration: TextDecoration) { + val string = randomString() + val expectedComponent = Component.text(string).decorate(decoration) + val decorationName = decoration.toString() + + // Non strict mode supported + Json.decodeFromString( + ComponentSerializer, + """ + "<$decorationName>$string" + """.trimIndent() + ) shouldBe expectedComponent + + // Strict mode supported + Json.decodeFromString( + ComponentSerializer, + """ + "<$decorationName>$string" + """.trimIndent() + ) shouldBe expectedComponent + } + + @Test + fun `with color and decoration`() { + val string = randomString() + val expectedComponent = Component.text(string, NamedTextColor.AQUA).decorate(TextDecoration.BOLD) + + Json.decodeFromString( + ComponentSerializer, + """ + "$string" + """.trimIndent() + ) shouldBe expectedComponent + + Json.decodeFromString( + ComponentSerializer, + """ + "$string" + """.trimIndent() + ) shouldBe expectedComponent + } + + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/DyeColorSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/DyeColorSerializerTest.kt new file mode 100644 index 00000000..7b3d83e4 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/DyeColorSerializerTest.kt @@ -0,0 +1,67 @@ +package com.github.rushyverse.api.serializer + +import com.github.rushyverse.api.utils.randomString +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.bukkit.DyeColor +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import kotlin.test.Test + +class DyeColorSerializerTest { + + @Nested + inner class Serialize { + + @ParameterizedTest + @EnumSource(DyeColor::class) + fun `should use enum name`(value: DyeColor) { + val enumName = value.name + Json.encodeToString(DyeColorSerializer, value) shouldEqualJson """ + "$enumName" + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @ParameterizedTest + @EnumSource(DyeColor::class) + fun `should find value with uppercase`(value: DyeColor) { + val enumName = value.name.uppercase() + Json.decodeFromString(DyeColorSerializer, "\"$enumName\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(DyeColor::class) + fun `should find value with lowercase`(value: DyeColor) { + val enumName = value.name.lowercase() + Json.decodeFromString(DyeColorSerializer, "\"$enumName\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(DyeColor::class) + fun `should find value with space`(value: DyeColor) { + val enumName = value.name.replace("_", " ") + Json.decodeFromString(DyeColorSerializer, "\"$enumName\"") shouldBe value + } + + @Test + fun `should throw exception if value is not found`() { + val enumName = randomString() + val exception = assertThrows { + Json.decodeFromString(DyeColorSerializer, "\"$enumName\"") + } + exception.message shouldBe "Invalid enum value: $enumName. Valid values are: ${ + DyeColor.entries.joinToString( + ", " + ) + }" + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/EnchantmentSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/EnchantmentSerializerTest.kt new file mode 100644 index 00000000..4486bb38 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/EnchantmentSerializerTest.kt @@ -0,0 +1,160 @@ +package com.github.rushyverse.api.serializer + +import be.seeseemelk.mockbukkit.MockBukkit +import com.github.rushyverse.api.utils.randomString +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import io.papermc.paper.enchantments.EnchantmentRarity +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import net.kyori.adventure.text.Component +import org.bukkit.NamespacedKey +import org.bukkit.enchantments.Enchantment +import org.bukkit.enchantments.EnchantmentTarget +import org.bukkit.entity.EntityCategory +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemStack +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class EnchantmentSerializerTest { + + class EnchantmentMock(private val enchantmentName: String, namespace: NamespacedKey) : Enchantment(namespace) { + override fun translationKey(): String = error("Not implemented") + + @Deprecated("Deprecated in Java") + override fun getName(): String = enchantmentName + override fun getMaxLevel(): Int = error("Not implemented") + override fun getStartLevel(): Int = error("Not implemented") + override fun getItemTarget(): EnchantmentTarget = error("Not implemented") + override fun isTreasure(): Boolean = error("Not implemented") + override fun isCursed(): Boolean = error("Not implemented") + override fun conflictsWith(other: Enchantment): Boolean = error("Not implemented") + override fun canEnchantItem(item: ItemStack): Boolean = error("Not implemented") + override fun displayName(level: Int): Component = error("Not implemented") + override fun isTradeable(): Boolean = error("Not implemented") + override fun isDiscoverable(): Boolean = error("Not implemented") + override fun getRarity(): EnchantmentRarity = error("Not implemented") + override fun getDamageIncrease(level: Int, entityCategory: EntityCategory): Float = + error("Not implemented") + + override fun getActiveSlots(): MutableSet = error("Not implemented") + } + + @BeforeTest + fun onBefore() { + MockBukkit.mock() + Enchantment.values().isEmpty() shouldBe false + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Serialize { + + @Test + fun `should use namespace and key`() { + fun assertEnchant(enchant: Enchantment) { + val namespace = enchant.key.namespace + val key = enchant.key.key + Json.encodeToString(EnchantmentSerializer, enchant) shouldEqualJson """ + "$namespace:$key" + """.trimIndent() + } + + Enchantment.values().forEach(::assertEnchant) + } + + @Test + fun `should serialize custom enchantment`() { + val namespace = randomAcceptableNamespace() + val key = randomAcceptableNamespace() + val namespacedKey = NamespacedKey(namespace, key) + val enchantment = EnchantmentMock(key, namespacedKey) + Enchantment.registerEnchantment(enchantment) + + Json.encodeToString(EnchantmentSerializer, enchantment) shouldEqualJson """ + "$namespace:$key" + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @Test + fun `should find with only key if minecraft is namespace`() { + val enchant = Enchantment.values().random() + val key = enchant.key.key + val json = """ + "$key" + """.trimIndent() + + Json.decodeFromString(EnchantmentSerializer, json) shouldBe enchant + } + + @Test + fun `should find with namespace and key`() { + val enchant = Enchantment.values().random() + val namespace = enchant.key.namespace + val key = enchant.key.key + val json = """ + "$namespace:$key" + """.trimIndent() + + Json.decodeFromString(EnchantmentSerializer, json) shouldBe enchant + } + + @Test + fun `should find with uppercase instead of underscore`() { + val enchant = Enchantment.FIRE_ASPECT + fun decode(key: String) { + val json = """ + "$key" + """.trimIndent() + + Json.decodeFromString(EnchantmentSerializer, json) shouldBe enchant + } + decode("fire_aspect") + decode("fireAspect") + } + + @Test + fun `should find custom enchantment`() { + val namespace = randomAcceptableNamespace() + val key = randomAcceptableNamespace() + val namespacedKey = NamespacedKey(namespace, key) + val enchantment = EnchantmentMock(key, namespacedKey) + Enchantment.registerEnchantment(enchantment) + + val json = """ + "$namespace:$key" + """.trimIndent() + Json.decodeFromString(EnchantmentSerializer, json) shouldBe enchantment + } + + @Test + fun `should throw if not found`() { + val namespace = randomAcceptableNamespace() + val key = randomAcceptableNamespace() + val json = """ + "$namespace:$key" + """.trimIndent() + + val exception = assertThrows { + Json.decodeFromString(EnchantmentSerializer, json) + } + + exception.message shouldBe "Unable to find enchantment with namespaced key: $namespace:$key. " + + "Valid enchantments are: ${Enchantment.values().joinToString(", ") { it.key.toString() }}" + } + } + + fun randomAcceptableNamespace() = randomString(('a'..'z') + ('0'..'9') + '_') +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/EnumSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/EnumSerializerTest.kt new file mode 100644 index 00000000..322de8de --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/EnumSerializerTest.kt @@ -0,0 +1,70 @@ +package com.github.rushyverse.api.serializer + +import com.github.rushyverse.api.utils.randomString +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import kotlin.test.Test + +class EnumSerializerTest { + + @Suppress("unused") + enum class TestEnum { + TEST_VALUE1, + TestValue2, + } + + data object TestEnumSerializer : EnumSerializer("testEnum", TestEnum.entries) + + @Nested + inner class Serialize { + + @ParameterizedTest + @EnumSource(TestEnum::class) + fun `should use enum name`(value: TestEnum) { + val enumName = value.name + Json.encodeToString(TestEnumSerializer, value) shouldEqualJson """ + "$enumName" + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @ParameterizedTest + @EnumSource(TestEnum::class) + fun `should find value with uppercase`(value: TestEnum) { + val enumName = value.name.uppercase() + Json.decodeFromString(TestEnumSerializer, "\"$enumName\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(TestEnum::class) + fun `should find value with lowercase`(value: TestEnum) { + val enumName = value.name.lowercase() + Json.decodeFromString(TestEnumSerializer, "\"$enumName\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(TestEnum::class) + fun `should find value with space`(value: TestEnum) { + val enumName = value.name.replace("_", " ") + Json.decodeFromString(TestEnumSerializer, "\"$enumName\"") shouldBe value + } + + @Test + fun `should throw exception if value is not found`() { + val enumName = randomString() + val exception = assertThrows { + Json.decodeFromString(TestEnumSerializer, "\"$enumName\"") + } + exception.message shouldBe "Invalid enum value: $enumName. Valid values are: TEST_VALUE1, TestValue2" + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/ItemFlagSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/ItemFlagSerializerTest.kt new file mode 100644 index 00000000..2dd55974 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/ItemFlagSerializerTest.kt @@ -0,0 +1,67 @@ +package com.github.rushyverse.api.serializer + +import com.github.rushyverse.api.utils.randomString +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.bukkit.inventory.ItemFlag +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import kotlin.test.Test + +class ItemFlagSerializerTest { + + @Nested + inner class Serialize { + + @ParameterizedTest + @EnumSource(ItemFlag::class) + fun `should use enum name`(item: ItemFlag) { + val enumName = item.name + Json.encodeToString(ItemFlagSerializer, item) shouldEqualJson """ + "$enumName" + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @ParameterizedTest + @EnumSource(ItemFlag::class) + fun `should find value with uppercase`(value: ItemFlag) { + val enumName = value.name.uppercase() + Json.decodeFromString(ItemFlagSerializer, "\"$enumName\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(ItemFlag::class) + fun `should find value with lowercase`(value: ItemFlag) { + val enumName = value.name.lowercase() + Json.decodeFromString(ItemFlagSerializer, "\"$enumName\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(ItemFlag::class) + fun `should find value with space`(value: ItemFlag) { + val enumName = value.name.replace("_", " ") + Json.decodeFromString(ItemFlagSerializer, "\"$enumName\"") shouldBe value + } + + @Test + fun `should throw exception if value is not found`() { + val enumName = randomString() + val exception = assertThrows { + Json.decodeFromString(ItemFlagSerializer, "\"$enumName\"") + } + exception.message shouldBe "Invalid enum value: $enumName. Valid values are: ${ + ItemFlag.entries.joinToString( + ", " + ) + }" + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/ItemStackSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/ItemStackSerializerTest.kt new file mode 100644 index 00000000..670724ff --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/ItemStackSerializerTest.kt @@ -0,0 +1,78 @@ +package com.github.rushyverse.api.serializer + +import be.seeseemelk.mockbukkit.MockBukkit +import com.github.rushyverse.api.utils.randomEnum +import com.github.rushyverse.api.utils.randomInt +import io.kotest.assertions.json.shouldEqualJson +import kotlinx.serialization.json.Json +import org.bukkit.Material +import org.bukkit.enchantments.Enchantment +import org.bukkit.inventory.ItemStack +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Nested +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class ItemStackSerializerTest { + + @BeforeTest + fun onBefore() { + MockBukkit.mock() + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Serialize { + + @Test + fun `with only material`() { + val material = randomEnum() + val amount = randomInt(1, 64) + val itemStack = ItemStack(material, amount) + val json = Json.encodeToString(ItemStackSerializer, itemStack) + json shouldEqualJson """ + { + "material": "${material.name}", + "amount": $amount, + "enchantments": {} + } + """.trimIndent() + } + + @Test + @Disabled // Waiting for https://github.com/MockBukkit/MockBukkit/pull/831 + fun `with enchantments`() { + val itemStack = ItemStack(Material.WOODEN_AXE).apply { + addUnsafeEnchantment(Enchantment.ARROW_DAMAGE, 1) + addUnsafeEnchantment(Enchantment.ARROW_FIRE, 42) + } + val json = Json.encodeToString(ItemStackSerializer, itemStack) + json shouldEqualJson """ + { + "material": "WOODEN_AXE", + "amount": 1, + "enchantments": { + "power": 1, + "flame": 42 + }, + "unbreakable": null, + "customMetaModel": null, + "destroyableKeys": [], + "placeableKeys": null, + "displayName": null, + "lore": null, + "durability": null, + "texture": null, + "patterns": null, + "flags": null + } + """.trimIndent() + } + + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/LocationSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/LocationSerializerTest.kt new file mode 100644 index 00000000..6ecd18fe --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/LocationSerializerTest.kt @@ -0,0 +1,323 @@ +package com.github.rushyverse.api.serializer + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.WorldMock +import com.github.rushyverse.api.utils.randomDouble +import com.github.rushyverse.api.utils.randomFloat +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.bukkit.Location +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class LocationSerializerTest { + + private lateinit var world: WorldMock + + @BeforeTest + fun onBefore() { + world = WorldMock() + MockBukkit.mock().addWorld(world) + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Serialize { + + @Nested + inner class OnlyCoordinate { + + @Test + fun `with positive values`() { + assertSerialize(14.0, 2.0, 375.0) + } + + @Test + fun `with negative values`() { + assertSerialize(-58518.0, -7.0, -6828126.0) + } + + @Test + fun `with zero values`() { + assertSerialize(0.0, 0.0, 0.0) + } + + @Test + fun `with decimal values`() { + assertSerialize(0.5, 0.7, 0.6) + } + + @Test + fun `with decimal values and negative values`() { + assertSerialize(-0.5, -0.7, -0.6) + } + + @Test + fun `with mixed values`() { + assertSerialize(0.5, -0.7, 0.6) + } + + } + + @Nested + inner class WithRotation { + + @Test + fun `with positive values`() { + assertSerialize(14.0, 2.0, 375.0, 0.5f, 0.7f) + } + + @Test + fun `with negative values`() { + assertSerialize(-58518.0, -7.0, -6828126.0, -0.5f, -0.7f) + } + + @Test + fun `with zero values`() { + assertSerialize(0.0, 0.0, 0.0, 0.0f, 0.0f) + } + + @Test + fun `with decimal values`() { + assertSerialize(0.5, 0.7, 0.6, 0.5f, 0.7f) + } + + @Test + fun `with decimal values and negative values`() { + assertSerialize(-0.5, -0.7, -0.6, -0.5f, -0.7f) + } + + @Test + fun `with mixed values`() { + assertSerialize(0.5, -0.7, 0.6, 0.1f, -0.2f) + } + + } + + @Test + fun `without world`() { + val loc = Location(null, randomDouble(), randomDouble(), randomDouble(), randomFloat(), randomFloat()) + val json = Json.encodeToString(LocationSerializer, loc) + json shouldEqualJson """ + { + "x": ${loc.x}, + "y": ${loc.y}, + "z": ${loc.z}, + "yaw": ${loc.yaw}, + "pitch": ${loc.pitch}, + "world": null + } + """.trimIndent() + } + + private fun assertSerialize(x: Double, y: Double, z: Double, yaw: Float = 0f, pitch: Float = 0f) { + val loc = Location(world, x, y, z, yaw, pitch) + val json = Json.encodeToString(LocationSerializer, loc) + json shouldEqualJson """ + { + "x": $x, + "y": $y, + "z": $z, + "yaw": $yaw, + "pitch": $pitch, + "world": "${world.name}" + } + """.trimIndent() + } + + } + + @Nested + inner class Deserialize { + + @Nested + inner class OnlyCoordinate { + + @Test + fun `with positive values`() { + assertDeserialize(14.0, 2.0, 375.0) + } + + @Test + fun `with negative values`() { + assertDeserialize(-58518.0, -7.0, -6828126.0) + } + + @Test + fun `with zero values`() { + assertDeserialize(0.0, 0.0, 0.0) + } + + @Test + fun `with decimal values`() { + assertDeserialize(0.5, 0.7, 0.6) + } + + @Test + fun `with decimal values and negative values`() { + assertDeserialize(-0.5, -0.7, -0.6) + } + + @Test + fun `with mixed values`() { + assertDeserialize(0.5, -0.7, 0.6) + } + + } + + @Nested + inner class WithRotation { + + @Test + fun `with positive values`() { + assertDeserialize(14.0, 2.0, 375.0, 0.5f, 0.7f) + } + + @Test + fun `with negative values`() { + assertDeserialize(-58518.0, -7.0, -6828126.0, -0.5f, -0.7f) + } + + @Test + fun `with zero values`() { + assertDeserialize(0.0, 0.0, 0.0, 0.0f, 0.0f) + } + + @Test + fun `with decimal values`() { + assertDeserialize(0.5, 0.7, 0.6, 0.5f, 0.7f) + } + + @Test + fun `with decimal values and negative values`() { + assertDeserialize(-0.5, -0.7, -0.6, -0.5f, -0.7f) + } + + @Test + fun `with mixed values`() { + assertDeserialize(0.5, -0.7, 0.6, 0.1f, -0.2f) + } + + } + + @Nested + inner class MissingField { + + @Test + fun `with missing x`() { + val json = """ + { + "y": 0.0, + "z": 0.0, + "yaw": 0.0, + "pitch": 0.0, + "world": "${world.name}" + } + """.trimIndent() + val exception = + assertThrows { Json.decodeFromString(LocationSerializer, json) } + exception.message shouldBe "The field x is missing" + } + + @Test + fun `with missing y`() { + val json = """ + { + "x": 0.0, + "z": 0.0, + "yaw": 0.0, + "pitch": 0.0, + "world": "${world.name}" + } + """.trimIndent() + val exception = + assertThrows { Json.decodeFromString(LocationSerializer, json) } + exception.message shouldBe "The field y is missing" + } + + @Test + fun `with missing z`() { + val json = """ + { + "x": 0.0, + "y": 0.0, + "yaw": 0.0, + "pitch": 0.0, + "world": "${world.name}" + } + """.trimIndent() + val exception = + assertThrows { Json.decodeFromString(LocationSerializer, json) } + exception.message shouldBe "The field z is missing" + } + + @Test + fun `with missing yaw`() { + val json = """ + { + "x": 1.0, + "y": 2.0, + "z": 3.0, + "pitch": 4.0, + "world": "${world.name}" + } + """.trimIndent() + val location = Json.decodeFromString(LocationSerializer, json) + location shouldBe Location(world, 1.0, 2.0, 3.0, 0.0f, 4.0f) + } + + @Test + fun `with missing pitch`() { + val json = """ + { + "x": 1.0, + "y": 2.0, + "z": 3.0, + "yaw": 4.0, + "world": "${world.name}" + } + """.trimIndent() + val location = Json.decodeFromString(LocationSerializer, json) + location shouldBe Location(world, 1.0, 2.0, 3.0, 4.0f, 0.0f) + } + + @Test + fun `with missing world`() { + val json = """ + { + "x": 1.0, + "y": 2.0, + "z": 3.0, + "yaw": 4.0, + "pitch": 5.0 + } + """.trimIndent() + val location = Json.decodeFromString(LocationSerializer, json) + location shouldBe Location(null, 1.0, 2.0, 3.0, 4.0f, 5.0f) + } + } + + private fun assertDeserialize(x: Double, y: Double, z: Double, yaw: Float = 0f, pitch: Float = 0f) { + val json = """ + { + "x": $x, + "y": $y, + "z": $z, + "yaw": $yaw, + "pitch": $pitch + } + """.trimIndent() + val location = Json.decodeFromString(LocationSerializer, json) + location shouldBe Location(null, x, y, z, yaw, pitch) + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/MaterialSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/MaterialSerializerTest.kt new file mode 100644 index 00000000..3aea51fe --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/MaterialSerializerTest.kt @@ -0,0 +1,67 @@ +package com.github.rushyverse.api.serializer + +import com.github.rushyverse.api.utils.randomString +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.bukkit.Material +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import kotlin.test.Test + +class MaterialSerializerTest { + + @Nested + inner class Serialize { + + @ParameterizedTest + @EnumSource(Material::class) + fun `should use enum name`(color: Material) { + val enumName = color.name + Json.encodeToString(MaterialSerializer, color) shouldEqualJson """ + "$enumName" + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @ParameterizedTest + @EnumSource(Material::class) + fun `should find value with uppercase`(color: Material) { + val enumName = color.name.uppercase() + Json.decodeFromString(MaterialSerializer, "\"$enumName\"") shouldBe color + } + + @ParameterizedTest + @EnumSource(Material::class) + fun `should find value with lowercase`(color: Material) { + val enumName = color.name.lowercase() + Json.decodeFromString(MaterialSerializer, "\"$enumName\"") shouldBe color + } + + @ParameterizedTest + @EnumSource(Material::class) + fun `should find value with space`(color: Material) { + val enumName = color.name.replace("_", " ") + Json.decodeFromString(MaterialSerializer, "\"$enumName\"") shouldBe color + } + + @Test + fun `should throw exception if value is not found`() { + val enumName = randomString() + val exception = assertThrows { + Json.decodeFromString(MaterialSerializer, "\"$enumName\"") + } + exception.message shouldBe "Invalid enum value: $enumName. Valid values are: ${ + Material.entries.joinToString( + ", " + ) + }" + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/NamespacedSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/NamespacedSerializerTest.kt new file mode 100644 index 00000000..3d38aca3 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/NamespacedSerializerTest.kt @@ -0,0 +1,95 @@ +package com.github.rushyverse.api.serializer + +import be.seeseemelk.mockbukkit.MockBukkit +import com.github.rushyverse.api.utils.randomString +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.Json +import org.bukkit.NamespacedKey +import org.bukkit.enchantments.Enchantment +import org.junit.jupiter.api.Nested +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class NamespacedSerializerTest { + + @BeforeTest + fun onBefore() { + MockBukkit.mock() + Enchantment.values().isEmpty() shouldBe false + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Serialize { + + @Test + fun `should use namespace and key`() { + val namespace = randomAcceptableNamespace() + val key = randomAcceptableNamespace() + Json.encodeToString(NamespacedSerializer, NamespacedKey(namespace, key)) shouldEqualJson """ + "$namespace:$key" + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @Test + fun `should create with minecraft namespace by default if not defined`() { + val key = randomAcceptableNamespace() + val json = """ + "$key" + """.trimIndent() + + Json.decodeFromString(NamespacedSerializer, json) shouldBe NamespacedKey("minecraft", key) + } + + @Test + fun `should create with namespace and key`() { + val namespace = randomAcceptableNamespace() + val key = randomAcceptableNamespace() + val json = """ + "$namespace:$key" + """.trimIndent() + + Json.decodeFromString(NamespacedSerializer, json) shouldBe NamespacedKey(namespace, key) + } + + @Test + fun `should replace uppercase by underscore and lowercase`() { + fun decode(namespace: String?, key: String, expected: NamespacedKey) { + val json = """ + "${if (namespace != null) "$namespace:" else ""}$key" + """.trimIndent() + + Json.decodeFromString(NamespacedSerializer, json) shouldBe expected + } + decode("test", "myKey", NamespacedKey("test", "my_key")) + decode("myNamespace", "myKey", NamespacedKey("my_namespace", "my_key")) + decode(null, "myKey", NamespacedKey.minecraft("my_key")) + } + + @Test + fun `should replace space by underscore`() { + fun decode(namespace: String?, key: String, expected: NamespacedKey) { + val json = """ + "${if (namespace != null) "$namespace:" else ""}$key" + """.trimIndent() + + Json.decodeFromString(NamespacedSerializer, json) shouldBe expected + } + decode("test", "my key", NamespacedKey("test", "my_key")) + decode("my namespace", "my key", NamespacedKey("my_namespace", "my_key")) + decode(null, "my key", NamespacedKey.minecraft("my_key")) + } + } + + fun randomAcceptableNamespace() = randomString(('a'..'z') + ('0'..'9') + '.' + '_' + '-') +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/PatternSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/PatternSerializerTest.kt new file mode 100644 index 00000000..6e5118de --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/PatternSerializerTest.kt @@ -0,0 +1,85 @@ +package com.github.rushyverse.api.serializer + +import com.github.rushyverse.api.utils.randomEnum +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.bukkit.DyeColor +import org.bukkit.block.banner.Pattern +import org.bukkit.block.banner.PatternType +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test + +class PatternSerializerTest { + + @Nested + inner class Serialize { + + @Test + fun `should serialize using type identifier and color name`() { + val dyeColor = randomEnum() + val patternType = randomEnum() + val pattern = Pattern(dyeColor, patternType) + Json.encodeToString(PatternSerializer, pattern) shouldEqualJson """ + { + "color": "${dyeColor.name}", + "type": "${patternType.identifier}" + } + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @Test + fun `should throw if color is missing`() { + val patternType = randomEnum() + val json = """ + { + "type": "${patternType.identifier}" + } + """.trimIndent() + assertThrows { + Json.decodeFromString(PatternSerializer, json) + } + } + + @Test + fun `should throw if type is missing`() { + val dyeColor = randomEnum() + val json = """ + { + "color": "${dyeColor.name}" + } + """.trimIndent() + assertThrows { + Json.decodeFromString(PatternSerializer, json) + } + } + + @Test + fun `should deserialize using type and color name`() { + val dyeColor = randomEnum() + val patternType = randomEnum() + + fun decode(color: String, type: String) { + val json = """ + { + "color": "$color", + "type": "$type" + } + """.trimIndent() + val pattern = Json.decodeFromString(PatternSerializer, json) + pattern.color shouldBe dyeColor + pattern.pattern shouldBe patternType + } + + decode(dyeColor.name, patternType.identifier) + decode(dyeColor.name, patternType.name) + } + + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/PatternTypeSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/PatternTypeSerializerTest.kt new file mode 100644 index 00000000..e3fe8d24 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/PatternTypeSerializerTest.kt @@ -0,0 +1,79 @@ +package com.github.rushyverse.api.serializer + +import com.github.rushyverse.api.utils.randomString +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.bukkit.block.banner.PatternType +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import kotlin.test.Test + +class PatternTypeSerializerTest { + + @Nested + inner class Serialize { + + @ParameterizedTest + @EnumSource(PatternType::class) + fun `should use identifier`(value: PatternType) { + val identifier = value.identifier.lowercase() + Json.encodeToString(PatternTypeSerializer, value) shouldEqualJson """ + "$identifier" + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @ParameterizedTest + @EnumSource(PatternType::class) + fun `should find value with identifier lowercase`(value: PatternType) { + val identifier = value.identifier.lowercase() + Json.decodeFromString(PatternTypeSerializer, "\"$identifier\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(PatternType::class) + fun `should find value with identifier uppercase`(value: PatternType) { + val identifier = value.identifier.uppercase() + Json.decodeFromString(PatternTypeSerializer, "\"$identifier\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(PatternType::class) + fun `should find value with name uppercase`(value: PatternType) { + val enumName = value.name.uppercase() + Json.decodeFromString(PatternTypeSerializer, "\"$enumName\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(PatternType::class) + fun `should find value with name lowercase`(value: PatternType) { + val enumName = value.name.lowercase() + Json.decodeFromString(PatternTypeSerializer, "\"$enumName\"") shouldBe value + } + + @ParameterizedTest + @EnumSource(PatternType::class) + fun `should find value with name space`(value: PatternType) { + val enumName = value.name.replace("_", " ") + Json.decodeFromString(PatternTypeSerializer, "\"$enumName\"") shouldBe value + } + + @Test + fun `should throw exception if value is not found`() { + val enumName = randomString() + val exception = assertThrows { + Json.decodeFromString(PatternTypeSerializer, "\"$enumName\"") + } + exception.message shouldBe "Invalid enum value: $enumName. Valid values are: ${ + PatternType.entries.joinToString(", ") + }" + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/PosSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/PosSerializerTest.kt deleted file mode 100644 index 661860ec..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/serializer/PosSerializerTest.kt +++ /dev/null @@ -1,212 +0,0 @@ -package com.github.rushyverse.api.serializer - -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json -import net.minestom.server.coordinate.Pos -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.assertThrows -import kotlin.test.Test -import kotlin.test.assertEquals - -class PosSerializerTest { - - @Nested - inner class Serialize { - - @Nested - inner class OnlyCoordinate { - - @Test - fun `with positive values`() { - assertSerialize(14.0, 2.0, 375.0) - } - - @Test - fun `with negative values`() { - assertSerialize(-58518.0, -7.0, -6828126.0) - } - - @Test - fun `with zero values`() { - assertSerialize(0.0, 0.0, 0.0) - } - - @Test - fun `with decimal values`() { - assertSerialize(0.5, 0.7, 0.6) - } - - @Test - fun `with decimal values and negative values`() { - assertSerialize(-0.5, -0.7, -0.6) - } - - @Test - fun `with mixed values`() { - assertSerialize(0.5, -0.7, 0.6) - } - - } - - @Nested - inner class WithRotation { - - @Test - fun `with positive values`() { - assertSerialize(14.0, 2.0, 375.0, 0.5f, 0.7f) - } - - @Test - fun `with negative values`() { - assertSerialize(-58518.0, -7.0, -6828126.0, -0.5f, -0.7f) - } - - @Test - fun `with zero values`() { - assertSerialize(0.0, 0.0, 0.0, 0.0f, 0.0f) - } - - @Test - fun `with decimal values`() { - assertSerialize(0.5, 0.7, 0.6, 0.5f, 0.7f) - } - - @Test - fun `with decimal values and negative values`() { - assertSerialize(-0.5, -0.7, -0.6, -0.5f, -0.7f) - } - - @Test - fun `with mixed values`() { - assertSerialize(0.5, -0.7, 0.6, 0.1f, -0.2f) - } - - } - - private fun assertSerialize(x: Double, y: Double, z: Double, yaw: Float = 0f, pitch: Float = 0f) { - val pos = Pos(x, y, z, yaw, pitch) - val json = Json.encodeToString(PosSerializer, pos) - assertEquals("{\"x\":$x,\"y\":$y,\"z\":$z,\"yaw\":$yaw,\"pitch\":$pitch}", json) - } - - } - - @Nested - inner class Deserialize { - - @Nested - inner class OnlyCoordinate { - - @Test - fun `with positive values`() { - assertDeserialize(14.0, 2.0, 375.0) - } - - @Test - fun `with negative values`() { - assertDeserialize(-58518.0, -7.0, -6828126.0) - } - - @Test - fun `with zero values`() { - assertDeserialize(0.0, 0.0, 0.0) - } - - @Test - fun `with decimal values`() { - assertDeserialize(0.5, 0.7, 0.6) - } - - @Test - fun `with decimal values and negative values`() { - assertDeserialize(-0.5, -0.7, -0.6) - } - - @Test - fun `with mixed values`() { - assertDeserialize(0.5, -0.7, 0.6) - } - - } - - @Nested - inner class WithRotation { - - @Test - fun `with positive values`() { - assertDeserialize(14.0, 2.0, 375.0, 0.5f, 0.7f) - } - - @Test - fun `with negative values`() { - assertDeserialize(-58518.0, -7.0, -6828126.0, -0.5f, -0.7f) - } - - @Test - fun `with zero values`() { - assertDeserialize(0.0, 0.0, 0.0, 0.0f, 0.0f) - } - - @Test - fun `with decimal values`() { - assertDeserialize(0.5, 0.7, 0.6, 0.5f, 0.7f) - } - - @Test - fun `with decimal values and negative values`() { - assertDeserialize(-0.5, -0.7, -0.6, -0.5f, -0.7f) - } - - @Test - fun `with mixed values`() { - assertDeserialize(0.5, -0.7, 0.6, 0.1f, -0.2f) - } - - } - - @Nested - inner class MissingField { - - @Test - fun `with missing x`() { - val json = "{\"y\":0.0,\"z\":0.0,\"yaw\":0.0,\"pitch\":0.0}" - val exception = assertThrows { Json.decodeFromString(PosSerializer, json) } - assertEquals("The field x is missing", exception.message) - } - - @Test - fun `with missing y`() { - val json = "{\"x\":0.0,\"z\":0.0,\"yaw\":0.0,\"pitch\":0.0}" - val exception = assertThrows { Json.decodeFromString(PosSerializer, json) } - assertEquals("The field y is missing", exception.message) - } - - @Test - fun `with missing z`() { - val json = "{\"x\":0.0,\"y\":0.0,\"yaw\":0.0,\"pitch\":0.0}" - val exception = assertThrows { Json.decodeFromString(PosSerializer, json) } - assertEquals("The field z is missing", exception.message) - } - - @Test - fun `with missing yaw`() { - val json = "{\"x\":1.0,\"y\":2.0,\"z\":3.0,\"pitch\":4.0}" - val pos = Json.decodeFromString(PosSerializer, json) - assertEquals(Pos(1.0, 2.0, 3.0, 0.0f, 4.0f), pos) - } - - @Test - fun `with missing pitch`() { - val json = "{\"x\":1.0,\"y\":2.0,\"z\":3.0,\"yaw\":4.0}" - val pos = Json.decodeFromString(PosSerializer, json) - assertEquals(Pos(1.0, 2.0, 3.0, 4.0f, 0.0f), pos) - } - } - - private fun assertDeserialize(x: Double, y: Double, z: Double, yaw: Float = 0f, pitch: Float = 0f) { - val json = "{\"x\":$x,\"y\":$y,\"z\":$z,\"yaw\":$yaw,\"pitch\":$pitch}" - val pos = Json.decodeFromString(PosSerializer, json) - assertEquals(Pos(x, y, z, yaw, pitch), pos) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/serializer/RangeDoubleSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/serializer/RangeDoubleSerializerTest.kt new file mode 100644 index 00000000..3459a741 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/serializer/RangeDoubleSerializerTest.kt @@ -0,0 +1,73 @@ +package com.github.rushyverse.api.serializer + +import com.github.rushyverse.api.utils.randomDouble +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test + +class RangeDoubleSerializerTest { + + @Nested + inner class Serialize { + + @Test + fun `should serialize using start and end`() { + val start = randomDouble(-100.0, 100.0) + val end = randomDouble(100.0, 200.0) + Json.encodeToString(RangeDoubleSerializer, start..end) shouldEqualJson """ + { + "start": $start, + "end": $end + } + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @Test + fun `should throw if start is missing`() { + val end = randomDouble(100.0, 200.0) + val json = """ + { + "end": $end + } + """.trimIndent() + assertThrows { + Json.decodeFromString(RangeDoubleSerializer, json) + } + } + + @Test + fun `should throw if end is missing`() { + val start = randomDouble(-100.0, 100.0) + val json = """ + { + "start": $start + } + """.trimIndent() + assertThrows { + Json.decodeFromString(RangeDoubleSerializer, json) + } + } + + @Test + fun `should deserialize using start and end`() { + val start = randomDouble(-100.0, 100.0) + val end = randomDouble(100.0, 200.0) + val json = """ + { + "start": $start, + "end": $end + } + """.trimIndent() + Json.decodeFromString(RangeDoubleSerializer, json) shouldBe start..end + } + + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslationsProviderTest.kt b/src/test/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslatorTest.kt similarity index 50% rename from src/test/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslationsProviderTest.kt rename to src/test/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslatorTest.kt index afa06086..364ec528 100644 --- a/src/test/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslationsProviderTest.kt +++ b/src/test/kotlin/com/github/rushyverse/api/translation/ResourceBundleTranslatorTest.kt @@ -1,24 +1,25 @@ package com.github.rushyverse.api.translation import com.github.rushyverse.api.utils.randomString +import io.kotest.matchers.shouldBe +import net.kyori.adventure.text.Component import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.util.* import kotlin.test.BeforeTest -import kotlin.test.assertEquals +import kotlin.test.Test private const val BUNDLE_NAME = "test_bundle" private const val SECOND_BUNDLE_NAME = "test_bundle_2" -class ResourceBundleTranslationsProviderTest { +class ResourceBundleTranslatorTest { - private lateinit var provider: ResourceBundleTranslationsProvider + private lateinit var provider: ResourceBundleTranslator @BeforeTest fun onBefore() { - provider = ResourceBundleTranslationsProvider() + provider = ResourceBundleTranslator(BUNDLE_NAME) } @Nested @@ -28,17 +29,17 @@ class ResourceBundleTranslationsProviderTest { fun `should load a resource bundle`() { val locale = SupportedLanguage.ENGLISH.locale provider.registerResourceBundle(BUNDLE_NAME, locale, ResourceBundle::getBundle) - assertEquals("english_value_1", provider.get("test1", locale, BUNDLE_NAME)) - assertEquals("english_value_2", provider.get("test2", locale, BUNDLE_NAME)) + provider.get("test1", locale, bundleName = BUNDLE_NAME) shouldBe "english_value_1" + provider.get("test2", locale, bundleName = BUNDLE_NAME) shouldBe "english_value_2" } @Test fun `should load a resource bundle for all supported locales`() { provider.registerResourceBundleForSupportedLocales(BUNDLE_NAME, ResourceBundle::getBundle) - SupportedLanguage.values().forEach { + SupportedLanguage.entries.forEach { val displayName = it.displayName.lowercase() - assertEquals("${displayName}_value_1", provider.get("test1", it.locale, BUNDLE_NAME)) - assertEquals("${displayName}_value_2", provider.get("test2", it.locale, BUNDLE_NAME)) + provider.get("test1", it.locale, bundleName = BUNDLE_NAME) shouldBe "${displayName}_value_1" + provider.get("test2", it.locale, bundleName = BUNDLE_NAME) shouldBe "${displayName}_value_2" } } @@ -47,42 +48,8 @@ class ResourceBundleTranslationsProviderTest { val locale = SupportedLanguage.ENGLISH.locale provider.registerResourceBundle(BUNDLE_NAME, locale, ResourceBundle::getBundle) provider.registerResourceBundle(SECOND_BUNDLE_NAME, locale, ResourceBundle::getBundle) - assertEquals("english_value_1", provider.get("test1", locale, BUNDLE_NAME)) - assertEquals("English value", provider.get("simple_value", locale, SECOND_BUNDLE_NAME)) - } - } - - @Nested - inner class GetValue { - - @Test - fun `should throw an exception if the bundle is not registered`() { - val locale = SupportedLanguage.ENGLISH.locale - val ex = assertThrows { - provider.get("test1", locale, BUNDLE_NAME) - } - assertEquals(BUNDLE_NAME, ex.bundleName) - assertEquals(locale, ex.locale) - } - - @Test - fun `should throw an exception if the key is not found`() { - provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertThrows { - provider.get(randomString(), SupportedLanguage.ENGLISH.locale, BUNDLE_NAME) - } - } - - @Test - fun `should return the value for the given key`() { - provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertEquals("english_value_1", provider.get("test1", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME)) - } - - @Test - fun `should return the default value if the value is not defined for language`() { - provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertEquals("default_value", provider.get("test_undefined", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME)) + provider.get("test1", locale, bundleName = BUNDLE_NAME) shouldBe "english_value_1" + provider.get("simple_value", locale, bundleName = SECOND_BUNDLE_NAME) shouldBe "English value" } } @@ -92,113 +59,156 @@ class ResourceBundleTranslationsProviderTest { @Test fun `should return the value for the given key`() { provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertEquals("english_value_1", provider.translate("test1", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME)) + provider.get("test1", SupportedLanguage.ENGLISH.locale, bundleName = BUNDLE_NAME) shouldBe "english_value_1" } @Test fun `should return the value for the given key with the given array arguments`() { provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertEquals( - "english_value with arguments", provider.translate( - "test_args", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME, arrayOf( - "with arguments" - ) - ) - ) - } - - @Test - fun `should return the value for the given key with the given list arguments`() { - provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertEquals( - "english_value with arguments", provider.translate( - "test_args", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME, listOf( - "with arguments" - ) - ) - ) + provider.get( + "test_args", SupportedLanguage.ENGLISH.locale, arrayOf( + "with arguments" + ), BUNDLE_NAME + ) shouldBe "english_value with arguments" } @Test fun `should return the key if the bundle is not registered`() { val locale = SupportedLanguage.ENGLISH.locale val ex = assertThrows { - assertEquals("test1", provider.translate("test1", locale, BUNDLE_NAME)) + provider.get("test1", locale, bundleName = BUNDLE_NAME) } - assertEquals(BUNDLE_NAME, ex.bundleName) - assertEquals(locale, ex.locale) + ex.bundleName shouldBe BUNDLE_NAME + ex.locale shouldBe locale } @Test fun `should return the key if the value is not defined for language`() { provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) val key = randomString() - assertEquals(key, provider.translate(key, SupportedLanguage.ENGLISH.locale, BUNDLE_NAME)) + provider.get(key, SupportedLanguage.ENGLISH.locale, bundleName = BUNDLE_NAME) shouldBe key } @Test fun `should return the key if the value is not defined for language with the given array arguments`() { provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) val key = randomString() - assertEquals(key, provider.translate(key, SupportedLanguage.ENGLISH.locale, BUNDLE_NAME, arrayOf("test"))) - } - - @Test - fun `should return the key if the value is not defined for language with the given list arguments`() { - provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - val key = randomString() - assertEquals(key, provider.translate(key, SupportedLanguage.ENGLISH.locale, BUNDLE_NAME, listOf("test"))) + provider.get(key, SupportedLanguage.ENGLISH.locale, arrayOf("test"), BUNDLE_NAME) shouldBe key } @Test fun `should return the default value if the value is not defined for language`() { provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertEquals( - "default_value", - provider.translate("test_undefined", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME) - ) + provider.get( + "test_undefined", + SupportedLanguage.ENGLISH.locale, + bundleName = BUNDLE_NAME + ) shouldBe "default_value" } @Test fun `should return the value with template for args if no replacement args are defined`() { provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertEquals( - "english_value {0}", - provider.translate("test_args", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME) - ) + provider.get( + "test_args", + SupportedLanguage.ENGLISH.locale, + bundleName = BUNDLE_NAME + ) shouldBe "english_value {0}" } @Test fun `should return the value with plural syntax`() { provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertEquals( - "Need 2 players.", - provider.translate("test_plural", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME, arrayOf(2)) - ) + provider.get( + "test_plural", + SupportedLanguage.ENGLISH.locale, + arrayOf(2), + BUNDLE_NAME + ) shouldBe "Need 2 players." } @Test fun `should return the value with singular syntax`() { provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) - assertEquals( - "Need 1 player.", - provider.translate("test_plural", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME, arrayOf(1)) - ) - assertEquals( - "Need 0 player.", - provider.translate("test_plural", SupportedLanguage.ENGLISH.locale, BUNDLE_NAME, arrayOf(0)) - ) + provider.get( + "test_plural", + SupportedLanguage.ENGLISH.locale, + arrayOf(1), + BUNDLE_NAME + ) shouldBe "Need 1 player." + + provider.get( + "test_plural", + SupportedLanguage.ENGLISH.locale, + arrayOf(0), + BUNDLE_NAME + ) shouldBe "Need 0 player." } @Test fun `should return the UTF-8 value`() { provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.FRENCH.locale, ResourceBundle::getBundle) - assertEquals( - "français_value_1", - provider.translate("test1", SupportedLanguage.FRENCH.locale, BUNDLE_NAME) + provider.get("test1", SupportedLanguage.FRENCH.locale, bundleName = BUNDLE_NAME) shouldBe "français_value_1" + } + + @Test + fun `should use default bundle if no bundle is specified`() { + provider = ResourceBundleTranslator(BUNDLE_NAME) + provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) + + provider.get("test1", SupportedLanguage.ENGLISH.locale) shouldBe "english_value_1" + // Key not found in default bundle + provider.get("simple_value", SupportedLanguage.ENGLISH.locale) shouldBe "simple_value" + + provider = ResourceBundleTranslator(SECOND_BUNDLE_NAME) + provider.registerResourceBundle( + SECOND_BUNDLE_NAME, + SupportedLanguage.ENGLISH.locale, + ResourceBundle::getBundle ) + + provider.get("simple_value", SupportedLanguage.ENGLISH.locale) shouldBe "English value" + // Key not found in default bundle + provider.get("test1", SupportedLanguage.ENGLISH.locale) shouldBe "test1" } } -} \ No newline at end of file + + @Nested + inner class TranslateValueToComponent { + + @Test + fun `should return the value for the given key`() { + provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) + provider.getComponent( + "test1", + SupportedLanguage.ENGLISH.locale, + bundleName = BUNDLE_NAME + ) shouldBe Component.text("english_value_1") + } + + @Test + fun `should return the value for the given key with the given array arguments`() { + provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) + provider.getComponent( + "test_args", + SupportedLanguage.ENGLISH.locale, + arrayOf("with arguments"), + bundleName = BUNDLE_NAME + ) shouldBe Component.text("english_value with arguments") + } + + @Test + fun `should return key in component if not found`() { + provider.registerResourceBundle(BUNDLE_NAME, SupportedLanguage.ENGLISH.locale, ResourceBundle::getBundle) + + val key = randomString() + provider.getComponent( + key, + SupportedLanguage.ENGLISH.locale, + bundleName = BUNDLE_NAME + ) shouldBe Component.text(key) + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/utils/Asserts.kt b/src/test/kotlin/com/github/rushyverse/api/utils/Asserts.kt deleted file mode 100644 index 4d63b317..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/utils/Asserts.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.rushyverse.api.utils - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.job -import kotlin.coroutines.CoroutineContext -import kotlin.test.assertEquals - -fun assertCoroutineContextFromScope(scope: CoroutineScope, coroutineContext: CoroutineContext) { - assertEquals(scope.coroutineContext.job.key, coroutineContext.job.key) -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/utils/FileUtilsTest.kt b/src/test/kotlin/com/github/rushyverse/api/utils/FileUtilsTest.kt deleted file mode 100644 index b260b8a2..00000000 --- a/src/test/kotlin/com/github/rushyverse/api/utils/FileUtilsTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.rushyverse.api.utils - -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.io.TempDir -import java.io.File -import kotlin.test.assertEquals - -class FileUtilsTest { - - @Test - fun `should get the current directory where is executed the program`(@TempDir tmpDirectory: File) { - assertEquals(File(System.getProperty("user.dir")), workingDirectory) - - val initCurrentDirectory = System.getProperty("user.dir") - - System.setProperty("user.dir", tmpDirectory.absolutePath) - assertEquals(tmpDirectory, workingDirectory) - - System.setProperty("user.dir", initCurrentDirectory) - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/rushyverse/api/utils/Generator.kt b/src/test/kotlin/com/github/rushyverse/api/utils/Generator.kt index 5da473e5..721a4201 100644 --- a/src/test/kotlin/com/github/rushyverse/api/utils/Generator.kt +++ b/src/test/kotlin/com/github/rushyverse/api/utils/Generator.kt @@ -1,25 +1,40 @@ package com.github.rushyverse.api.utils -import net.minestom.server.coordinate.Pos -import java.net.ServerSocket -import java.util.* +import org.bukkit.Location +import org.bukkit.World import kotlin.random.Random -private val stringGenerator = generateSequence { UUID.randomUUID().toString() }.distinct().iterator() +fun randomString( + allowedChar: List = ('a'..'z') + ('A'..'Z') + ('0'..'9'), + size: Int = 50 +): String { + return List(size) { allowedChar.random() }.joinToString("") +} -fun randomString() = stringGenerator.next() +fun randomBoolean() = Random.nextBoolean() -private val intGenerator = generateSequence { Random.nextInt() }.distinct().iterator() +fun randomInt(from: Int = Int.MIN_VALUE, until: Int = Int.MAX_VALUE) = Random.nextInt(from, until) -fun randomInt() = intGenerator.next() +fun randomLong(from: Long = Long.MIN_VALUE, until: Long = Long.MAX_VALUE) = Random.nextLong(from, until) -private val posGenerator = - generateSequence { Pos(Random.nextDouble(), Random.nextDouble(), Random.nextDouble()) }.distinct().iterator() +fun randomFloat(from: Float = Float.MIN_VALUE, until: Float = Float.MAX_VALUE) = + randomDouble(from.toDouble(), until.toDouble()).toFloat() -fun randomPos() = posGenerator.next() +fun randomDouble(from: Double = Double.MIN_VALUE, until: Double = Double.MAX_VALUE) = Random.nextDouble(from, until) -fun getAvailablePort(): Int { - return ServerSocket(0).use { - it.localPort - } -} \ No newline at end of file +inline fun > randomEnum(): T { + return enumValues().random() +} + +const val LIMIT_RANDOM_COORDINATE = 1000.0 + +fun randomLocation(world: World? = null): Location { + return Location( + world, + Random.nextDouble(LIMIT_RANDOM_COORDINATE), + Random.nextDouble(LIMIT_RANDOM_COORDINATE), + Random.nextDouble(LIMIT_RANDOM_COORDINATE), + Random.nextDouble(LIMIT_RANDOM_COORDINATE).toFloat(), + Random.nextDouble(LIMIT_RANDOM_COORDINATE).toFloat() + ) +} diff --git a/src/test/kotlin/com/github/rushyverse/api/utils/LocationTestUtils.kt b/src/test/kotlin/com/github/rushyverse/api/utils/LocationTestUtils.kt new file mode 100644 index 00000000..a1eb9923 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/utils/LocationTestUtils.kt @@ -0,0 +1,15 @@ +package com.github.rushyverse.api.utils + +import org.bukkit.Location +import kotlin.test.assertEquals + +fun assertEqualsLocation(loc1: Location, loc2: Location) { + with(loc1) { + assertEquals(world, loc2.world) + assertEquals(x, loc2.x) + assertEquals(y, loc2.y) + assertEquals(z, loc2.z) + assertEquals(yaw, loc2.yaw) + assertEquals(pitch, loc2.pitch) + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/world/cube/CubeAreaSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/world/cube/CubeAreaSerializerTest.kt new file mode 100644 index 00000000..0e94fa94 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/world/cube/CubeAreaSerializerTest.kt @@ -0,0 +1,174 @@ +package com.github.rushyverse.api.world.cube + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.WorldMock +import com.github.rushyverse.api.extension.minMaxOf +import com.github.rushyverse.api.utils.randomDouble +import com.github.rushyverse.api.utils.randomFloat +import com.github.rushyverse.api.world.CubeArea +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.Json +import org.bukkit.Location +import org.junit.jupiter.api.Nested +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class CubeAreaSerializerTest { + + private lateinit var world: WorldMock + + @BeforeTest + fun onBefore() { + world = WorldMock() + MockBukkit.mock().addWorld(world) + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Serialize { + + @Test + fun `should with only coordinate for location`() { + val xs = minMaxOf(randomDouble(), randomDouble()) + val ys = minMaxOf(randomDouble(), randomDouble()) + val zs = minMaxOf(randomDouble(), randomDouble()) + val min = Location(null, xs.first, ys.first, zs.first) + val max = Location(null, xs.second, ys.second, zs.second) + val area = CubeArea(min, max) + + val json = Json.encodeToString(CubeArea.serializer(), area) + json shouldEqualJson """ + { + "location1": { + "x": ${min.x}, + "y": ${min.y}, + "z": ${min.z}, + "yaw": 0.0, + "pitch": 0.0, + "world": null + }, + "location2": { + "x": ${max.x}, + "y": ${max.y}, + "z": ${max.z}, + "yaw": 0.0, + "pitch": 0.0, + "world": null + } + } + """.trimIndent() + } + + @Test + fun `should with direction coordinate for location`() { + val xs = minMaxOf(randomDouble(), randomDouble()) + val ys = minMaxOf(randomDouble(), randomDouble()) + val zs = minMaxOf(randomDouble(), randomDouble()) + val yaws = minMaxOf(randomFloat(), randomFloat()) + val pitchs = minMaxOf(randomFloat(), randomFloat()) + val min = Location(null, xs.first, ys.first, zs.first, yaws.first, pitchs.first) + val max = Location(null, xs.second, ys.second, zs.second, yaws.second, pitchs.second) + val area = CubeArea(min, max) + + val json = Json.encodeToString(CubeArea.serializer(), area) + json shouldEqualJson """ + { + "location1": { + "x": ${min.x}, + "y": ${min.y}, + "z": ${min.z}, + "yaw": 0.0, + "pitch": 0.0, + "world": null + }, + "location2": { + "x": ${max.x}, + "y": ${max.y}, + "z": ${max.z}, + "yaw": 0.0, + "pitch": 0.0, + "world": null + } + } + """.trimIndent() + } + + @Test + fun `should with all fields`() { + val xs = minMaxOf(randomDouble(), randomDouble()) + val ys = minMaxOf(randomDouble(), randomDouble()) + val zs = minMaxOf(randomDouble(), randomDouble()) + val yaws = minMaxOf(randomFloat(), randomFloat()) + val pitchs = minMaxOf(randomFloat(), randomFloat()) + val min = Location(world, xs.first, ys.first, zs.first, yaws.first, pitchs.first) + val max = Location(world, xs.second, ys.second, zs.second, yaws.second, pitchs.second) + val area = CubeArea(min, max) + + val json = Json.encodeToString(CubeArea.serializer(), area) + json shouldEqualJson """ + { + "location1": { + "x": ${min.x}, + "y": ${min.y}, + "z": ${min.z}, + "yaw": 0.0, + "pitch": 0.0, + "world": "${world.name}" + }, + "location2": { + "x": ${max.x}, + "y": ${max.y}, + "z": ${max.z}, + "yaw": 0.0, + "pitch": 0.0, + "world": "${world.name}" + } + } + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @Test + fun `should with all fields`() { + val xs = minMaxOf(randomDouble(), randomDouble()) + val ys = minMaxOf(randomDouble(), randomDouble()) + val zs = minMaxOf(randomDouble(), randomDouble()) + val yaws = minMaxOf(randomFloat(), randomFloat()) + val pitchs = minMaxOf(randomFloat(), randomFloat()) + + val json = """ + { + "location1": { + "x": ${xs.first}, + "y": ${ys.second}, + "z": ${zs.second}, + "yaw": ${yaws.first}, + "pitch": ${pitchs.second}, + "world": "${world.name}" + }, + "location2": { + "x": ${xs.second}, + "y": ${ys.first}, + "z": ${zs.first}, + "yaw": ${yaws.second}, + "pitch": ${pitchs.first}, + "world": "${world.name}" + } + } + """.trimIndent() + Json.decodeFromString(CubeArea.serializer(), json) shouldBe CubeArea( + Location(world, xs.first, ys.first, zs.first, 0f, 0f), + Location(world, xs.second, ys.second, zs.second, 0f, 0f) + ) + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/world/cube/CubeAreaTest.kt b/src/test/kotlin/com/github/rushyverse/api/world/cube/CubeAreaTest.kt new file mode 100644 index 00000000..658c7248 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/world/cube/CubeAreaTest.kt @@ -0,0 +1,267 @@ +package com.github.rushyverse.api.world.cube + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.ServerMock +import be.seeseemelk.mockbukkit.WorldMock +import com.github.rushyverse.api.extension.copy +import com.github.rushyverse.api.utils.assertEqualsLocation +import com.github.rushyverse.api.utils.randomFloat +import com.github.rushyverse.api.utils.randomLocation +import com.github.rushyverse.api.utils.randomString +import com.github.rushyverse.api.world.CubeArea +import com.github.rushyverse.api.world.isIn +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.bukkit.Location +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class CubeAreaTest { + + private lateinit var serverMock: ServerMock + private lateinit var worldMock: WorldMock + + @BeforeTest + fun onBefore() { + serverMock = MockBukkit.mock() + worldMock = serverMock.addSimpleWorld("world") + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Instantiation { + + @Test + fun `should throw if locations are not in the same world`() { + assertThrows { + CubeArea(randomLocation(mockk()), randomLocation(mockk())) + } + } + + @Test + fun `should set min and max with null world`() { + val loc1 = Location(null, 0.0, 0.0, 0.0) + val loc2 = Location(null, 1.0, 1.0, 1.0) + CubeArea(loc1, loc2).apply { + assertEqualsLocation(loc1, min) + assertEqualsLocation(loc2, max) + } + } + + @Test + fun `should set min and max ignoring direction`() { + val loc1 = Location(worldMock, 0.0, 0.0, 0.0, randomFloat(), randomFloat()) + val loc2 = Location(worldMock, 1.0, 1.0, 1.0, randomFloat(), randomFloat()) + CubeArea(loc1, loc2).apply { + assertEqualsLocation(loc1.copy(yaw = 0f, pitch = 0f), min) + assertEqualsLocation(loc2.copy(yaw = 0f, pitch = 0f), max) + } + } + + @Test + fun `should set min and max with same order if coordinate is correctly ordered`() { + val loc1 = Location(worldMock, 0.0, 0.0, 0.0) + val loc2 = Location(worldMock, 1.0, 1.0, 1.0) + CubeArea(loc1, loc2).apply { + assertEqualsLocation(loc1, min) + assertEqualsLocation(loc2, max) + } + } + + @Test + fun `should set min and max with change x order`() { + val loc1 = Location(worldMock, 4.0, 0.0, 0.0) + val loc2 = Location(worldMock, -5.0, 1.0, 1.0) + CubeArea(loc1, loc2).apply { + assertEqualsLocation(loc1.copy(x = loc2.x), min) + assertEqualsLocation(loc2.copy(x = loc1.x), max) + } + } + + @Test + fun `should set min and max with change y order`() { + val loc1 = Location(worldMock, 0.0, 3.0, 0.0) + val loc2 = Location(worldMock, 1.0, -10.0, 1.0) + CubeArea(loc1, loc2).apply { + assertEqualsLocation(loc1.copy(y = loc2.y), min) + assertEqualsLocation(loc2.copy(y = loc1.y), max) + } + } + + @Test + fun `should set min and max with change z order`() { + val loc1 = Location(worldMock, 0.0, 3.0, 17.0) + val loc2 = Location(worldMock, 1.0, 6.0, 2.0) + CubeArea(loc1, loc2).apply { + assertEqualsLocation(loc1.copy(z = loc2.z), min) + assertEqualsLocation(loc2.copy(z = loc1.z), max) + } + } + + @Test + fun `should set min and max with change all coordinates order`() { + val loc1 = Location(worldMock, -1.0, 17.0, 30.0) + val loc2 = Location(worldMock, -9.0, 10.7, 0.0) + CubeArea(loc1, loc2).apply { + assertEqualsLocation(loc2, min) + assertEqualsLocation(loc1, max) + } + } + + } + + @Nested + inner class InAreaWithWorld { + + @Test + fun `should return true if world is null for both`() { + val min = Location(null, 0.0, 0.0, 0.0) + val max = Location(null, 1.0, 1.0, 1.0) + val area = CubeArea(min, max) + + min isIn area shouldBe true + } + + @Test + fun `should return false if world is different`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val max = Location(worldMock, 1.0, 1.0, 1.0) + val area = CubeArea(min, max) + + min.copy(world = serverMock.addSimpleWorld(randomString())) isIn area shouldBe false + } + + @Test + fun `should return true if world is same and in area`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val max = Location(worldMock, 1.0, 1.0, 1.0) + val area = CubeArea(min, max) + + min isIn area shouldBe true + } + } + + @Nested + inner class InArea { + + @Test + fun `should detect if location is in area`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val max = Location(worldMock, 10.0, 10.0, 10.0) + val area = CubeArea(min, max) + + for (x in 0..10) { + for (y in 0..10) { + for (z in 0..10) { + val loc = Location(worldMock, x.toDouble(), y.toDouble(), z.toDouble()) + loc isIn area shouldBe true + } + } + } + + min.copy(x = 10.1) isIn area shouldBe false + min.copy(y = 10.1) isIn area shouldBe false + min.copy(z = 10.1) isIn area shouldBe false + max.copy(x = -0.1) isIn area shouldBe false + max.copy(y = -0.1) isIn area shouldBe false + max.copy(z = -0.1) isIn area shouldBe false + } + + } + + @Nested + inner class SetPosition { + + @Test + fun `should keep the same position if the new value is the same`() { + val area = CubeArea(Location(worldMock, 0.0, 0.0, 0.0), Location(worldMock, 10.5, 10.5, 10.5)) + val oldMin = area.min + val oldMax = area.max + + area.location = area.location + area.min shouldBe oldMin + area.max shouldBe oldMax + } + + @Test + fun `should change the position if the new positive value is different`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val max = Location(worldMock, 10.0, 10.0, 10.0) + val area = CubeArea(min, max) + val newLocation = Location(worldMock, 20.0, 20.0, 20.0) + area.location = newLocation + + area.location shouldBe newLocation + area.min shouldBe Location(worldMock, 15.0, 15.0, 15.0) + area.max shouldBe Location(worldMock, 25.0, 25.0, 25.0) + } + + @Test + fun `should change the position if the new negative value is different`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val max = Location(worldMock, 10.0, 10.0, 10.0) + val area = CubeArea(min, max) + val newLocation = Location(worldMock, -20.0, -20.0, -20.0) + area.location = newLocation + area.location shouldBe newLocation + area.min shouldBe Location(worldMock, -25.0, -25.0, -25.0) + area.max shouldBe Location(worldMock, -15.0, -15.0, -15.0) + } + + @Test + fun `should change the position if the new mixed value is different`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val max = Location(worldMock, 10.0, 10.0, 10.0) + val area = CubeArea(min, max) + val newLocation = Location(worldMock, 20.0, -20.0, -20.0) + area.location = newLocation + area.location shouldBe newLocation + area.min shouldBe Location(worldMock, 15.0, -25.0, -25.0) + area.max shouldBe Location(worldMock, 25.0, -15.0, -15.0) + } + + } + + @Nested + inner class GetPosition { + + @Test + fun `should return the center of the area with positive values`() { + val min = Location(worldMock, 10.0, 10.0, 10.0) + val max = Location(worldMock, 20.0, 20.0, 20.0) + val area = CubeArea(min, max) + area.location shouldBe Location(worldMock, 15.0, 15.0, 15.0) + } + + @Test + fun `should return the center of the area with negative values`() { + val min = Location(worldMock, -20.0, -20.0, -20.0) + val max = Location(worldMock, -10.0, -10.0, -10.0) + val area = CubeArea(min, max) + area.location shouldBe Location(worldMock, -15.0, -15.0, -15.0) + } + + @Test + fun `should return the center of the area with mixed values`() { + val min = Location(worldMock, -20.0, 10.0, -20.0) + val max = Location(worldMock, -10.0, 20.0, -10.0) + val area = CubeArea(min, max) + area.location shouldBe Location(worldMock, -15.0, 15.0, -15.0) + } + + @Test + fun `should return the center of the area with decimal value`() { + val min = Location(worldMock, 10.6, 10.8, 10.4) + val max = Location(worldMock, 10.0, 10.0, 20.0) + val area = CubeArea(min, max) + area.location shouldBe Location(worldMock, 10.3, 10.4, 15.2) + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/world/cylinder/CylinderAreaSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/world/cylinder/CylinderAreaSerializerTest.kt new file mode 100644 index 00000000..f23a9549 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/world/cylinder/CylinderAreaSerializerTest.kt @@ -0,0 +1,168 @@ +package com.github.rushyverse.api.world.cylinder + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.WorldMock +import com.github.rushyverse.api.utils.randomDouble +import com.github.rushyverse.api.utils.randomFloat +import com.github.rushyverse.api.world.CylinderArea +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.Json +import org.bukkit.Location +import org.junit.jupiter.api.Nested +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class CylinderAreaSerializerTest { + + private lateinit var world: WorldMock + + @BeforeTest + fun onBefore() { + world = WorldMock() + MockBukkit.mock().addWorld(world) + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Serialize { + + @Test + fun `should with only coordinate for location`() { + val loc = Location(null, randomDouble(), randomDouble(), randomDouble()) + val radius = randomDouble(from = 0.0) + val height = randomDouble()..randomDouble() + val area = CylinderArea(loc, radius, height) + val json = Json.encodeToString(CylinderArea.serializer(), area) + json shouldEqualJson """ + { + "location": { + "x": ${loc.x}, + "y": ${loc.y}, + "z": ${loc.z}, + "yaw": 0.0, + "pitch": 0.0, + "world": null + }, + "radius": $radius, + "height": { + "start": ${height.start}, + "end": ${height.endInclusive} + } + } + """.trimIndent() + } + + @Test + fun `should with direction coordinate for location`() { + val loc = Location(null, randomDouble(), randomDouble(), randomDouble(), randomFloat(), randomFloat()) + val radius = randomDouble(from = 0.0) + val height = randomDouble()..randomDouble() + val area = CylinderArea(loc, radius, height) + val json = Json.encodeToString(CylinderArea.serializer(), area) + json shouldEqualJson """ + { + "location": { + "x": ${loc.x}, + "y": ${loc.y}, + "z": ${loc.z}, + "yaw": ${loc.yaw}, + "pitch": ${loc.pitch}, + "world": null + }, + "radius": $radius, + "height": { + "start": ${height.start}, + "end": ${height.endInclusive} + } + } + """.trimIndent() + } + + @Test + fun `should with all fields`() { + val loc = Location(world, randomDouble(), randomDouble(), randomDouble(), randomFloat(), randomFloat()) + val radius = randomDouble(from = 0.0) + val height = randomDouble()..randomDouble() + val area = CylinderArea(loc, radius, height) + val json = Json.encodeToString(CylinderArea.serializer(), area) + json shouldEqualJson """ + { + "location": { + "x": ${loc.x}, + "y": ${loc.y}, + "z": ${loc.z}, + "yaw": ${loc.yaw}, + "pitch": ${loc.pitch}, + "world": "${world.name}" + }, + "radius": $radius, + "height": { + "start": ${height.start}, + "end": ${height.endInclusive} + } + } + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @Test + fun `should with all fields`() { + val loc = Location(world, randomDouble(), randomDouble(), randomDouble(), randomFloat(), randomFloat()) + val radius = randomDouble(from = 0.0) + val height = randomDouble()..randomDouble() + val json = """ + { + "location": { + "x": ${loc.x}, + "y": ${loc.y}, + "z": ${loc.z}, + "yaw": ${loc.yaw}, + "pitch": ${loc.pitch}, + "world": "${world.name}" + }, + "radius": $radius, + "height": { + "start": ${height.start}, + "end": ${height.endInclusive} + } + } + """.trimIndent() + Json.decodeFromString(CylinderArea.serializer(), json) shouldBe CylinderArea(loc, radius, height) + } + + @Test + fun `should throw if radius is negative`() { + val json = """ + { + "location": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "yaw": 0.0, + "pitch": 0.0, + "world": null + }, + "radius": -1, + "height": { + "start": 0.0, + "end": 0.0 + } + } + """.trimIndent() + + shouldThrow { + Json.decodeFromString(CylinderArea.serializer(), json) + } + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/world/cylinder/CylinderAreaTest.kt b/src/test/kotlin/com/github/rushyverse/api/world/cylinder/CylinderAreaTest.kt new file mode 100644 index 00000000..a77b13a3 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/world/cylinder/CylinderAreaTest.kt @@ -0,0 +1,179 @@ +package com.github.rushyverse.api.world.cylinder + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.ServerMock +import be.seeseemelk.mockbukkit.WorldMock +import com.github.rushyverse.api.extension.copy +import com.github.rushyverse.api.utils.randomString +import com.github.rushyverse.api.world.CylinderArea +import com.github.rushyverse.api.world.isIn +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.bukkit.Location +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class CylinderAreaTest { + + private lateinit var serverMock: ServerMock + private lateinit var worldMock: WorldMock + + @BeforeTest + fun onBefore() { + serverMock = MockBukkit.mock() + worldMock = serverMock.addSimpleWorld("world") + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Instantiation { + + @Test + fun `should throw an exception if the radius is negative`() { + shouldThrow { + CylinderArea(mockk(), -1.0, 0.0..0.0) + } + } + + @Test + fun `should throw an exception if the radius is set`() { + val area = CylinderArea(mockk(), 0.0, 0.0..0.0) + assertThrows { + area.radius = -1.0 + } + } + + @Test + fun `should set the radius without exception if value is zero or positive`() { + val area = CylinderArea(mockk(), 0.0, 0.0..0.0) + + area.radius = 0.0 + area.radius shouldBe 0.0 + + area.radius = 1.0 + area.radius shouldBe 1.0 + } + } + + @Nested + inner class InAreaWithWorld { + + @Test + fun `should return false if world is different`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = CylinderArea(min, 1.0, 0.0..0.0) + + min.copy(world = serverMock.addSimpleWorld(randomString())) isIn area shouldBe false + } + + @Test + fun `should return true if world is same and in area`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = CylinderArea(min, 1.0, 0.0..0.0) + + min isIn area shouldBe true + } + } + + @Nested + inner class InAreaWithHeight { + + @Test + fun `should use negative height limit`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = CylinderArea(min, 1.0, -10.0..-5.0) + + min.copy(y = -5.0) isIn area shouldBe true + min.copy(y = -8.1) isIn area shouldBe true + min.copy(y = -11.0) isIn area shouldBe false + min.copy(y = -4.9) isIn area shouldBe false + } + + @Test + fun `should use positive height limit`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = CylinderArea(min, 0.0, 5.0..10.0) + + min.copy(y = 5.0) isIn area shouldBe true + min.copy(y = 7.3) isIn area shouldBe true + min.copy(y = 4.3) isIn area shouldBe false + min.copy(y = 10.1) isIn area shouldBe false + } + + @Test + fun `should use negative and positive height limit`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = CylinderArea(min, 0.0, -5.0..10.0) + + min.copy(y = 0.0) isIn area shouldBe true + min.copy(y = -3.0) isIn area shouldBe true + min.copy(y = 8.0) isIn area shouldBe true + min.copy(y = 10.1) isIn area shouldBe false + min.copy(y = -5.1) isIn area shouldBe false + } + + @Test + fun `should use zero height limit`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = CylinderArea(min, 0.0, 0.0..0.0) + + min.copy(y = 0.0) isIn area shouldBe true + min.copy(y = -0.1) isIn area shouldBe false + min.copy(y = 0.1) isIn area shouldBe false + } + + } + + @Nested + inner class InAreaWithRadius { + + @Test + fun `should use zero for radius`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = CylinderArea(min, 0.0, 0.0..0.0) + + min.copy(x = 0.0, z = 0.0) isIn area shouldBe true + min.copy(x = 0.1, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, z = 0.1) isIn area shouldBe false + min.copy(x = -0.1, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, z = -0.1) isIn area shouldBe false + } + + @Test + fun `should use positive for radius`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = CylinderArea(min, 1.0, 0.0..0.0) + + min.copy(x = 0.0, z = 0.0) isIn area shouldBe true + min.copy(x = 1.0, z = 0.0) isIn area shouldBe true + min.copy(x = 0.0, z = 1.0) isIn area shouldBe true + min.copy(x = -1.0, z = 0.0) isIn area shouldBe true + min.copy(x = 0.0, z = -1.0) isIn area shouldBe true + min.copy(x = 1.1, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, z = 1.1) isIn area shouldBe false + min.copy(x = -1.1, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, z = -1.1) isIn area shouldBe false + + min.copy(x = 1.0, z = 0.1) isIn area shouldBe false + min.copy(x = -1.0, z = 0.1) isIn area shouldBe false + min.copy(x = 1.0, z = -0.1) isIn area shouldBe false + min.copy(x = -1.0, z = -0.1) isIn area shouldBe false + + min.copy(x = 0.1, z = 1.0) isIn area shouldBe false + min.copy(x = -0.1, z = 1.0) isIn area shouldBe false + min.copy(x = 0.1, z = -1.0) isIn area shouldBe false + min.copy(x = -0.1, z = -1.0) isIn area shouldBe false + + min.copy(y = 0.1) isIn area shouldBe false + min.copy(y = -0.1) isIn area shouldBe false + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/world/sphere/SphereAreaSerializerTest.kt b/src/test/kotlin/com/github/rushyverse/api/world/sphere/SphereAreaSerializerTest.kt new file mode 100644 index 00000000..6d83096f --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/world/sphere/SphereAreaSerializerTest.kt @@ -0,0 +1,145 @@ +package com.github.rushyverse.api.world.sphere + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.WorldMock +import com.github.rushyverse.api.utils.randomDouble +import com.github.rushyverse.api.utils.randomFloat +import com.github.rushyverse.api.world.SphereArea +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.Json +import org.bukkit.Location +import org.junit.jupiter.api.Nested +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class SphereAreaSerializerTest { + + private lateinit var world: WorldMock + + @BeforeTest + fun onBefore() { + world = WorldMock() + MockBukkit.mock().addWorld(world) + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Serialize { + + @Test + fun `should with only coordinate for location`() { + val loc = Location(null, randomDouble(), randomDouble(), randomDouble()) + val radius = randomDouble(from = 0.0) + val area = SphereArea(loc, radius) + val json = Json.encodeToString(SphereArea.serializer(), area) + json shouldEqualJson """ + { + "location": { + "x": ${loc.x}, + "y": ${loc.y}, + "z": ${loc.z}, + "yaw": 0.0, + "pitch": 0.0, + "world": null + }, + "radius": $radius + } + """.trimIndent() + } + + @Test + fun `should with direction coordinate for location`() { + val loc = Location(null, randomDouble(), randomDouble(), randomDouble(), randomFloat(), randomFloat()) + val radius = randomDouble(from = 0.0) + val area = SphereArea(loc, radius) + val json = Json.encodeToString(SphereArea.serializer(), area) + json shouldEqualJson """ + { + "location": { + "x": ${loc.x}, + "y": ${loc.y}, + "z": ${loc.z}, + "yaw": ${loc.yaw}, + "pitch": ${loc.pitch}, + "world": null + }, + "radius": $radius + } + """.trimIndent() + } + + @Test + fun `should with all fields`() { + val loc = Location(world, randomDouble(), randomDouble(), randomDouble(), randomFloat(), randomFloat()) + val radius = randomDouble(from = 0.0) + val area = SphereArea(loc, radius) + val json = Json.encodeToString(SphereArea.serializer(), area) + json shouldEqualJson """ + { + "location": { + "x": ${loc.x}, + "y": ${loc.y}, + "z": ${loc.z}, + "yaw": ${loc.yaw}, + "pitch": ${loc.pitch}, + "world": "${world.name}" + }, + "radius": $radius + } + """.trimIndent() + } + } + + @Nested + inner class Deserialize { + + @Test + fun `should with all fields`() { + val loc = Location(world, randomDouble(), randomDouble(), randomDouble(), randomFloat(), randomFloat()) + val radius = randomDouble(from = 0.0) + val json = """ + { + "location": { + "x": ${loc.x}, + "y": ${loc.y}, + "z": ${loc.z}, + "yaw": ${loc.yaw}, + "pitch": ${loc.pitch}, + "world": "${world.name}" + }, + "radius": $radius + } + """.trimIndent() + + Json.decodeFromString(SphereArea.serializer(), json) shouldBe SphereArea(loc, radius) + } + + @Test + fun `should throw if radius is negative`() { + val json = """ + { + "location": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "yaw": 0.0, + "pitch": 0.0, + "world": null + }, + "radius": -1 + } + """.trimIndent() + + shouldThrow { + Json.decodeFromString(SphereArea.serializer(), json) + } + } + } +} diff --git a/src/test/kotlin/com/github/rushyverse/api/world/sphere/SphereAreaTest.kt b/src/test/kotlin/com/github/rushyverse/api/world/sphere/SphereAreaTest.kt new file mode 100644 index 00000000..52355331 --- /dev/null +++ b/src/test/kotlin/com/github/rushyverse/api/world/sphere/SphereAreaTest.kt @@ -0,0 +1,138 @@ +package com.github.rushyverse.api.world.sphere + +import be.seeseemelk.mockbukkit.MockBukkit +import be.seeseemelk.mockbukkit.ServerMock +import be.seeseemelk.mockbukkit.WorldMock +import com.github.rushyverse.api.extension.copy +import com.github.rushyverse.api.utils.randomString +import com.github.rushyverse.api.world.SphereArea +import com.github.rushyverse.api.world.isIn +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.bukkit.Location +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import kotlin.math.floor +import kotlin.math.sqrt +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class SphereAreaTest { + + private lateinit var serverMock: ServerMock + private lateinit var worldMock: WorldMock + + @BeforeTest + fun onBefore() { + serverMock = MockBukkit.mock() + worldMock = serverMock.addSimpleWorld(randomString()) + } + + @AfterTest + fun onAfter() { + MockBukkit.unmock() + } + + @Nested + inner class Instantiation { + + @Test + fun `should throw an exception if the radius is negative`() { + shouldThrow { + SphereArea(mockk(), -1.0) + } + } + + @Test + fun `should throw an exception if the radius is set`() { + val area = SphereArea(mockk(), 0.0) + assertThrows { + area.radius = -1.0 + } + } + + @Test + fun `should set the radius without exception if value is zero or positive`() { + val area = SphereArea(mockk(), 0.0) + + area.radius = 0.0 + area.radius shouldBe 0.0 + + area.radius = 1.0 + area.radius shouldBe 1.0 + } + } + + @Nested + inner class InAreaWithWorld { + + @Test + fun `should return false if world is different`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = SphereArea(min, 1.0) + + min.copy(world = serverMock.addSimpleWorld(randomString())) isIn area shouldBe false + } + + @Test + fun `should return true if world is same and in area`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = SphereArea(min, 1.0) + + min isIn area shouldBe true + } + } + + @Nested + inner class InAreaWithRadius { + + @Test + fun `should use zero for radius`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = SphereArea(min, 0.0) + + min.copy(x = 0.0, y = 0.0, z = 0.0) isIn area shouldBe true + + min.copy(x = 0.1, y = 0.0, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, y = 0.1, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, y = 0.0, z = 0.1) isIn area shouldBe false + min.copy(x = 0.1, y = 0.1, z = 0.1) isIn area shouldBe false + + min.copy(x = -0.1, y = 0.0, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, y = -0.1, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, y = 0.0, z = -0.1) isIn area shouldBe false + } + + @Test + fun `should use positive for radius`() { + val min = Location(worldMock, 0.0, 0.0, 0.0) + val area = SphereArea(min, 5.0) + + min.copy(x = 0.0, y = 0.0, z = 0.0) isIn area shouldBe true + min.copy(x = 5.0, y = 0.0, z = 0.0) isIn area shouldBe true + min.copy(x = 0.0, y = 5.0, z = 0.0) isIn area shouldBe true + min.copy(x = 0.0, y = 0.0, z = 5.0) isIn area shouldBe true + min.copy(x = -5.0, y = 0.0, z = 0.0) isIn area shouldBe true + min.copy(x = 0.0, y = -5.0, z = 0.0) isIn area shouldBe true + min.copy(x = 0.0, y = 0.0, z = -5.0) isIn area shouldBe true + + min.copy(x = 5.1, y = 0.0, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, y = 5.1, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, y = 0.0, z = 5.1) isIn area shouldBe false + min.copy(x = -5.1, y = 0.0, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, y = -5.1, z = 0.0) isIn area shouldBe false + min.copy(x = 0.0, y = 0.0, z = -5.1) isIn area shouldBe false + + min.copy(x = 5.0, y = 5.0, z = 5.0) isIn area shouldBe false + min.copy(x = -5.0, y = -5.0, z = -5.0) isIn area shouldBe false + + val maxDiagonal = + floor(sqrt(5.0 * 5.0 / 3.0) * 100) / 100 // The limit is 25 for radius 5.0 with x,y,z = ~2.886 + min.copy(x = maxDiagonal, y = maxDiagonal, z = maxDiagonal) isIn area shouldBe true + min.copy(x = -maxDiagonal, y = -maxDiagonal, z = -maxDiagonal) isIn area shouldBe true + min.copy(x = maxDiagonal + 0.1, y = maxDiagonal + 0.1, z = maxDiagonal + 0.1) isIn area shouldBe false + } + } +} diff --git a/src/test/resources/cases/roman/numerals.csv b/src/test/resources/cases/roman/numerals.csv new file mode 100644 index 00000000..43dcb471 --- /dev/null +++ b/src/test/resources/cases/roman/numerals.csv @@ -0,0 +1,3999 @@ +1, I +2, II +3, III +4, IV +5, V +6, VI +7, VII +8, VIII +9, IX +10, X +11, XI +12, XII +13, XIII +14, XIV +15, XV +16, XVI +17, XVII +18, XVIII +19, XIX +20, XX +21, XXI +22, XXII +23, XXIII +24, XXIV +25, XXV +26, XXVI +27, XXVII +28, XXVIII +29, XXIX +30, XXX +31, XXXI +32, XXXII +33, XXXIII +34, XXXIV +35, XXXV +36, XXXVI +37, XXXVII +38, XXXVIII +39, XXXIX +40, XL +41, XLI +42, XLII +43, XLIII +44, XLIV +45, XLV +46, XLVI +47, XLVII +48, XLVIII +49, XLIX +50, L +51, LI +52, LII +53, LIII +54, LIV +55, LV +56, LVI +57, LVII +58, LVIII +59, LIX +60, LX +61, LXI +62, LXII +63, LXIII +64, LXIV +65, LXV +66, LXVI +67, LXVII +68, LXVIII +69, LXIX +70, LXX +71, LXXI +72, LXXII +73, LXXIII +74, LXXIV +75, LXXV +76, LXXVI +77, LXXVII +78, LXXVIII +79, LXXIX +80, LXXX +81, LXXXI +82, LXXXII +83, LXXXIII +84, LXXXIV +85, LXXXV +86, LXXXVI +87, LXXXVII +88, LXXXVIII +89, LXXXIX +90, XC +91, XCI +92, XCII +93, XCIII +94, XCIV +95, XCV +96, XCVI +97, XCVII +98, XCVIII +99, XCIX +100, C +101, CI +102, CII +103, CIII +104, CIV +105, CV +106, CVI +107, CVII +108, CVIII +109, CIX +110, CX +111, CXI +112, CXII +113, CXIII +114, CXIV +115, CXV +116, CXVI +117, CXVII +118, CXVIII +119, CXIX +120, CXX +121, CXXI +122, CXXII +123, CXXIII +124, CXXIV +125, CXXV +126, CXXVI +127, CXXVII +128, CXXVIII +129, CXXIX +130, CXXX +131, CXXXI +132, CXXXII +133, CXXXIII +134, CXXXIV +135, CXXXV +136, CXXXVI +137, CXXXVII +138, CXXXVIII +139, CXXXIX +140, CXL +141, CXLI +142, CXLII +143, CXLIII +144, CXLIV +145, CXLV +146, CXLVI +147, CXLVII +148, CXLVIII +149, CXLIX +150, CL +151, CLI +152, CLII +153, CLIII +154, CLIV +155, CLV +156, CLVI +157, CLVII +158, CLVIII +159, CLIX +160, CLX +161, CLXI +162, CLXII +163, CLXIII +164, CLXIV +165, CLXV +166, CLXVI +167, CLXVII +168, CLXVIII +169, CLXIX +170, CLXX +171, CLXXI +172, CLXXII +173, CLXXIII +174, CLXXIV +175, CLXXV +176, CLXXVI +177, CLXXVII +178, CLXXVIII +179, CLXXIX +180, CLXXX +181, CLXXXI +182, CLXXXII +183, CLXXXIII +184, CLXXXIV +185, CLXXXV +186, CLXXXVI +187, CLXXXVII +188, CLXXXVIII +189, CLXXXIX +190, CXC +191, CXCI +192, CXCII +193, CXCIII +194, CXCIV +195, CXCV +196, CXCVI +197, CXCVII +198, CXCVIII +199, CXCIX +200, CC +201, CCI +202, CCII +203, CCIII +204, CCIV +205, CCV +206, CCVI +207, CCVII +208, CCVIII +209, CCIX +210, CCX +211, CCXI +212, CCXII +213, CCXIII +214, CCXIV +215, CCXV +216, CCXVI +217, CCXVII +218, CCXVIII +219, CCXIX +220, CCXX +221, CCXXI +222, CCXXII +223, CCXXIII +224, CCXXIV +225, CCXXV +226, CCXXVI +227, CCXXVII +228, CCXXVIII +229, CCXXIX +230, CCXXX +231, CCXXXI +232, CCXXXII +233, CCXXXIII +234, CCXXXIV +235, CCXXXV +236, CCXXXVI +237, CCXXXVII +238, CCXXXVIII +239, CCXXXIX +240, CCXL +241, CCXLI +242, CCXLII +243, CCXLIII +244, CCXLIV +245, CCXLV +246, CCXLVI +247, CCXLVII +248, CCXLVIII +249, CCXLIX +250, CCL +251, CCLI +252, CCLII +253, CCLIII +254, CCLIV +255, CCLV +256, CCLVI +257, CCLVII +258, CCLVIII +259, CCLIX +260, CCLX +261, CCLXI +262, CCLXII +263, CCLXIII +264, CCLXIV +265, CCLXV +266, CCLXVI +267, CCLXVII +268, CCLXVIII +269, CCLXIX +270, CCLXX +271, CCLXXI +272, CCLXXII +273, CCLXXIII +274, CCLXXIV +275, CCLXXV +276, CCLXXVI +277, CCLXXVII +278, CCLXXVIII +279, CCLXXIX +280, CCLXXX +281, CCLXXXI +282, CCLXXXII +283, CCLXXXIII +284, CCLXXXIV +285, CCLXXXV +286, CCLXXXVI +287, CCLXXXVII +288, CCLXXXVIII +289, CCLXXXIX +290, CCXC +291, CCXCI +292, CCXCII +293, CCXCIII +294, CCXCIV +295, CCXCV +296, CCXCVI +297, CCXCVII +298, CCXCVIII +299, CCXCIX +300, CCC +301, CCCI +302, CCCII +303, CCCIII +304, CCCIV +305, CCCV +306, CCCVI +307, CCCVII +308, CCCVIII +309, CCCIX +310, CCCX +311, CCCXI +312, CCCXII +313, CCCXIII +314, CCCXIV +315, CCCXV +316, CCCXVI +317, CCCXVII +318, CCCXVIII +319, CCCXIX +320, CCCXX +321, CCCXXI +322, CCCXXII +323, CCCXXIII +324, CCCXXIV +325, CCCXXV +326, CCCXXVI +327, CCCXXVII +328, CCCXXVIII +329, CCCXXIX +330, CCCXXX +331, CCCXXXI +332, CCCXXXII +333, CCCXXXIII +334, CCCXXXIV +335, CCCXXXV +336, CCCXXXVI +337, CCCXXXVII +338, CCCXXXVIII +339, CCCXXXIX +340, CCCXL +341, CCCXLI +342, CCCXLII +343, CCCXLIII +344, CCCXLIV +345, CCCXLV +346, CCCXLVI +347, CCCXLVII +348, CCCXLVIII +349, CCCXLIX +350, CCCL +351, CCCLI +352, CCCLII +353, CCCLIII +354, CCCLIV +355, CCCLV +356, CCCLVI +357, CCCLVII +358, CCCLVIII +359, CCCLIX +360, CCCLX +361, CCCLXI +362, CCCLXII +363, CCCLXIII +364, CCCLXIV +365, CCCLXV +366, CCCLXVI +367, CCCLXVII +368, CCCLXVIII +369, CCCLXIX +370, CCCLXX +371, CCCLXXI +372, CCCLXXII +373, CCCLXXIII +374, CCCLXXIV +375, CCCLXXV +376, CCCLXXVI +377, CCCLXXVII +378, CCCLXXVIII +379, CCCLXXIX +380, CCCLXXX +381, CCCLXXXI +382, CCCLXXXII +383, CCCLXXXIII +384, CCCLXXXIV +385, CCCLXXXV +386, CCCLXXXVI +387, CCCLXXXVII +388, CCCLXXXVIII +389, CCCLXXXIX +390, CCCXC +391, CCCXCI +392, CCCXCII +393, CCCXCIII +394, CCCXCIV +395, CCCXCV +396, CCCXCVI +397, CCCXCVII +398, CCCXCVIII +399, CCCXCIX +400, CD +401, CDI +402, CDII +403, CDIII +404, CDIV +405, CDV +406, CDVI +407, CDVII +408, CDVIII +409, CDIX +410, CDX +411, CDXI +412, CDXII +413, CDXIII +414, CDXIV +415, CDXV +416, CDXVI +417, CDXVII +418, CDXVIII +419, CDXIX +420, CDXX +421, CDXXI +422, CDXXII +423, CDXXIII +424, CDXXIV +425, CDXXV +426, CDXXVI +427, CDXXVII +428, CDXXVIII +429, CDXXIX +430, CDXXX +431, CDXXXI +432, CDXXXII +433, CDXXXIII +434, CDXXXIV +435, CDXXXV +436, CDXXXVI +437, CDXXXVII +438, CDXXXVIII +439, CDXXXIX +440, CDXL +441, CDXLI +442, CDXLII +443, CDXLIII +444, CDXLIV +445, CDXLV +446, CDXLVI +447, CDXLVII +448, CDXLVIII +449, CDXLIX +450, CDL +451, CDLI +452, CDLII +453, CDLIII +454, CDLIV +455, CDLV +456, CDLVI +457, CDLVII +458, CDLVIII +459, CDLIX +460, CDLX +461, CDLXI +462, CDLXII +463, CDLXIII +464, CDLXIV +465, CDLXV +466, CDLXVI +467, CDLXVII +468, CDLXVIII +469, CDLXIX +470, CDLXX +471, CDLXXI +472, CDLXXII +473, CDLXXIII +474, CDLXXIV +475, CDLXXV +476, CDLXXVI +477, CDLXXVII +478, CDLXXVIII +479, CDLXXIX +480, CDLXXX +481, CDLXXXI +482, CDLXXXII +483, CDLXXXIII +484, CDLXXXIV +485, CDLXXXV +486, CDLXXXVI +487, CDLXXXVII +488, CDLXXXVIII +489, CDLXXXIX +490, CDXC +491, CDXCI +492, CDXCII +493, CDXCIII +494, CDXCIV +495, CDXCV +496, CDXCVI +497, CDXCVII +498, CDXCVIII +499, CDXCIX +500, D +501, DI +502, DII +503, DIII +504, DIV +505, DV +506, DVI +507, DVII +508, DVIII +509, DIX +510, DX +511, DXI +512, DXII +513, DXIII +514, DXIV +515, DXV +516, DXVI +517, DXVII +518, DXVIII +519, DXIX +520, DXX +521, DXXI +522, DXXII +523, DXXIII +524, DXXIV +525, DXXV +526, DXXVI +527, DXXVII +528, DXXVIII +529, DXXIX +530, DXXX +531, DXXXI +532, DXXXII +533, DXXXIII +534, DXXXIV +535, DXXXV +536, DXXXVI +537, DXXXVII +538, DXXXVIII +539, DXXXIX +540, DXL +541, DXLI +542, DXLII +543, DXLIII +544, DXLIV +545, DXLV +546, DXLVI +547, DXLVII +548, DXLVIII +549, DXLIX +550, DL +551, DLI +552, DLII +553, DLIII +554, DLIV +555, DLV +556, DLVI +557, DLVII +558, DLVIII +559, DLIX +560, DLX +561, DLXI +562, DLXII +563, DLXIII +564, DLXIV +565, DLXV +566, DLXVI +567, DLXVII +568, DLXVIII +569, DLXIX +570, DLXX +571, DLXXI +572, DLXXII +573, DLXXIII +574, DLXXIV +575, DLXXV +576, DLXXVI +577, DLXXVII +578, DLXXVIII +579, DLXXIX +580, DLXXX +581, DLXXXI +582, DLXXXII +583, DLXXXIII +584, DLXXXIV +585, DLXXXV +586, DLXXXVI +587, DLXXXVII +588, DLXXXVIII +589, DLXXXIX +590, DXC +591, DXCI +592, DXCII +593, DXCIII +594, DXCIV +595, DXCV +596, DXCVI +597, DXCVII +598, DXCVIII +599, DXCIX +600, DC +601, DCI +602, DCII +603, DCIII +604, DCIV +605, DCV +606, DCVI +607, DCVII +608, DCVIII +609, DCIX +610, DCX +611, DCXI +612, DCXII +613, DCXIII +614, DCXIV +615, DCXV +616, DCXVI +617, DCXVII +618, DCXVIII +619, DCXIX +620, DCXX +621, DCXXI +622, DCXXII +623, DCXXIII +624, DCXXIV +625, DCXXV +626, DCXXVI +627, DCXXVII +628, DCXXVIII +629, DCXXIX +630, DCXXX +631, DCXXXI +632, DCXXXII +633, DCXXXIII +634, DCXXXIV +635, DCXXXV +636, DCXXXVI +637, DCXXXVII +638, DCXXXVIII +639, DCXXXIX +640, DCXL +641, DCXLI +642, DCXLII +643, DCXLIII +644, DCXLIV +645, DCXLV +646, DCXLVI +647, DCXLVII +648, DCXLVIII +649, DCXLIX +650, DCL +651, DCLI +652, DCLII +653, DCLIII +654, DCLIV +655, DCLV +656, DCLVI +657, DCLVII +658, DCLVIII +659, DCLIX +660, DCLX +661, DCLXI +662, DCLXII +663, DCLXIII +664, DCLXIV +665, DCLXV +666, DCLXVI +667, DCLXVII +668, DCLXVIII +669, DCLXIX +670, DCLXX +671, DCLXXI +672, DCLXXII +673, DCLXXIII +674, DCLXXIV +675, DCLXXV +676, DCLXXVI +677, DCLXXVII +678, DCLXXVIII +679, DCLXXIX +680, DCLXXX +681, DCLXXXI +682, DCLXXXII +683, DCLXXXIII +684, DCLXXXIV +685, DCLXXXV +686, DCLXXXVI +687, DCLXXXVII +688, DCLXXXVIII +689, DCLXXXIX +690, DCXC +691, DCXCI +692, DCXCII +693, DCXCIII +694, DCXCIV +695, DCXCV +696, DCXCVI +697, DCXCVII +698, DCXCVIII +699, DCXCIX +700, DCC +701, DCCI +702, DCCII +703, DCCIII +704, DCCIV +705, DCCV +706, DCCVI +707, DCCVII +708, DCCVIII +709, DCCIX +710, DCCX +711, DCCXI +712, DCCXII +713, DCCXIII +714, DCCXIV +715, DCCXV +716, DCCXVI +717, DCCXVII +718, DCCXVIII +719, DCCXIX +720, DCCXX +721, DCCXXI +722, DCCXXII +723, DCCXXIII +724, DCCXXIV +725, DCCXXV +726, DCCXXVI +727, DCCXXVII +728, DCCXXVIII +729, DCCXXIX +730, DCCXXX +731, DCCXXXI +732, DCCXXXII +733, DCCXXXIII +734, DCCXXXIV +735, DCCXXXV +736, DCCXXXVI +737, DCCXXXVII +738, DCCXXXVIII +739, DCCXXXIX +740, DCCXL +741, DCCXLI +742, DCCXLII +743, DCCXLIII +744, DCCXLIV +745, DCCXLV +746, DCCXLVI +747, DCCXLVII +748, DCCXLVIII +749, DCCXLIX +750, DCCL +751, DCCLI +752, DCCLII +753, DCCLIII +754, DCCLIV +755, DCCLV +756, DCCLVI +757, DCCLVII +758, DCCLVIII +759, DCCLIX +760, DCCLX +761, DCCLXI +762, DCCLXII +763, DCCLXIII +764, DCCLXIV +765, DCCLXV +766, DCCLXVI +767, DCCLXVII +768, DCCLXVIII +769, DCCLXIX +770, DCCLXX +771, DCCLXXI +772, DCCLXXII +773, DCCLXXIII +774, DCCLXXIV +775, DCCLXXV +776, DCCLXXVI +777, DCCLXXVII +778, DCCLXXVIII +779, DCCLXXIX +780, DCCLXXX +781, DCCLXXXI +782, DCCLXXXII +783, DCCLXXXIII +784, DCCLXXXIV +785, DCCLXXXV +786, DCCLXXXVI +787, DCCLXXXVII +788, DCCLXXXVIII +789, DCCLXXXIX +790, DCCXC +791, DCCXCI +792, DCCXCII +793, DCCXCIII +794, DCCXCIV +795, DCCXCV +796, DCCXCVI +797, DCCXCVII +798, DCCXCVIII +799, DCCXCIX +800, DCCC +801, DCCCI +802, DCCCII +803, DCCCIII +804, DCCCIV +805, DCCCV +806, DCCCVI +807, DCCCVII +808, DCCCVIII +809, DCCCIX +810, DCCCX +811, DCCCXI +812, DCCCXII +813, DCCCXIII +814, DCCCXIV +815, DCCCXV +816, DCCCXVI +817, DCCCXVII +818, DCCCXVIII +819, DCCCXIX +820, DCCCXX +821, DCCCXXI +822, DCCCXXII +823, DCCCXXIII +824, DCCCXXIV +825, DCCCXXV +826, DCCCXXVI +827, DCCCXXVII +828, DCCCXXVIII +829, DCCCXXIX +830, DCCCXXX +831, DCCCXXXI +832, DCCCXXXII +833, DCCCXXXIII +834, DCCCXXXIV +835, DCCCXXXV +836, DCCCXXXVI +837, DCCCXXXVII +838, DCCCXXXVIII +839, DCCCXXXIX +840, DCCCXL +841, DCCCXLI +842, DCCCXLII +843, DCCCXLIII +844, DCCCXLIV +845, DCCCXLV +846, DCCCXLVI +847, DCCCXLVII +848, DCCCXLVIII +849, DCCCXLIX +850, DCCCL +851, DCCCLI +852, DCCCLII +853, DCCCLIII +854, DCCCLIV +855, DCCCLV +856, DCCCLVI +857, DCCCLVII +858, DCCCLVIII +859, DCCCLIX +860, DCCCLX +861, DCCCLXI +862, DCCCLXII +863, DCCCLXIII +864, DCCCLXIV +865, DCCCLXV +866, DCCCLXVI +867, DCCCLXVII +868, DCCCLXVIII +869, DCCCLXIX +870, DCCCLXX +871, DCCCLXXI +872, DCCCLXXII +873, DCCCLXXIII +874, DCCCLXXIV +875, DCCCLXXV +876, DCCCLXXVI +877, DCCCLXXVII +878, DCCCLXXVIII +879, DCCCLXXIX +880, DCCCLXXX +881, DCCCLXXXI +882, DCCCLXXXII +883, DCCCLXXXIII +884, DCCCLXXXIV +885, DCCCLXXXV +886, DCCCLXXXVI +887, DCCCLXXXVII +888, DCCCLXXXVIII +889, DCCCLXXXIX +890, DCCCXC +891, DCCCXCI +892, DCCCXCII +893, DCCCXCIII +894, DCCCXCIV +895, DCCCXCV +896, DCCCXCVI +897, DCCCXCVII +898, DCCCXCVIII +899, DCCCXCIX +900, CM +901, CMI +902, CMII +903, CMIII +904, CMIV +905, CMV +906, CMVI +907, CMVII +908, CMVIII +909, CMIX +910, CMX +911, CMXI +912, CMXII +913, CMXIII +914, CMXIV +915, CMXV +916, CMXVI +917, CMXVII +918, CMXVIII +919, CMXIX +920, CMXX +921, CMXXI +922, CMXXII +923, CMXXIII +924, CMXXIV +925, CMXXV +926, CMXXVI +927, CMXXVII +928, CMXXVIII +929, CMXXIX +930, CMXXX +931, CMXXXI +932, CMXXXII +933, CMXXXIII +934, CMXXXIV +935, CMXXXV +936, CMXXXVI +937, CMXXXVII +938, CMXXXVIII +939, CMXXXIX +940, CMXL +941, CMXLI +942, CMXLII +943, CMXLIII +944, CMXLIV +945, CMXLV +946, CMXLVI +947, CMXLVII +948, CMXLVIII +949, CMXLIX +950, CML +951, CMLI +952, CMLII +953, CMLIII +954, CMLIV +955, CMLV +956, CMLVI +957, CMLVII +958, CMLVIII +959, CMLIX +960, CMLX +961, CMLXI +962, CMLXII +963, CMLXIII +964, CMLXIV +965, CMLXV +966, CMLXVI +967, CMLXVII +968, CMLXVIII +969, CMLXIX +970, CMLXX +971, CMLXXI +972, CMLXXII +973, CMLXXIII +974, CMLXXIV +975, CMLXXV +976, CMLXXVI +977, CMLXXVII +978, CMLXXVIII +979, CMLXXIX +980, CMLXXX +981, CMLXXXI +982, CMLXXXII +983, CMLXXXIII +984, CMLXXXIV +985, CMLXXXV +986, CMLXXXVI +987, CMLXXXVII +988, CMLXXXVIII +989, CMLXXXIX +990, CMXC +991, CMXCI +992, CMXCII +993, CMXCIII +994, CMXCIV +995, CMXCV +996, CMXCVI +997, CMXCVII +998, CMXCVIII +999, CMXCIX +1000, M +1001, MI +1002, MII +1003, MIII +1004, MIV +1005, MV +1006, MVI +1007, MVII +1008, MVIII +1009, MIX +1010, MX +1011, MXI +1012, MXII +1013, MXIII +1014, MXIV +1015, MXV +1016, MXVI +1017, MXVII +1018, MXVIII +1019, MXIX +1020, MXX +1021, MXXI +1022, MXXII +1023, MXXIII +1024, MXXIV +1025, MXXV +1026, MXXVI +1027, MXXVII +1028, MXXVIII +1029, MXXIX +1030, MXXX +1031, MXXXI +1032, MXXXII +1033, MXXXIII +1034, MXXXIV +1035, MXXXV +1036, MXXXVI +1037, MXXXVII +1038, MXXXVIII +1039, MXXXIX +1040, MXL +1041, MXLI +1042, MXLII +1043, MXLIII +1044, MXLIV +1045, MXLV +1046, MXLVI +1047, MXLVII +1048, MXLVIII +1049, MXLIX +1050, ML +1051, MLI +1052, MLII +1053, MLIII +1054, MLIV +1055, MLV +1056, MLVI +1057, MLVII +1058, MLVIII +1059, MLIX +1060, MLX +1061, MLXI +1062, MLXII +1063, MLXIII +1064, MLXIV +1065, MLXV +1066, MLXVI +1067, MLXVII +1068, MLXVIII +1069, MLXIX +1070, MLXX +1071, MLXXI +1072, MLXXII +1073, MLXXIII +1074, MLXXIV +1075, MLXXV +1076, MLXXVI +1077, MLXXVII +1078, MLXXVIII +1079, MLXXIX +1080, MLXXX +1081, MLXXXI +1082, MLXXXII +1083, MLXXXIII +1084, MLXXXIV +1085, MLXXXV +1086, MLXXXVI +1087, MLXXXVII +1088, MLXXXVIII +1089, MLXXXIX +1090, MXC +1091, MXCI +1092, MXCII +1093, MXCIII +1094, MXCIV +1095, MXCV +1096, MXCVI +1097, MXCVII +1098, MXCVIII +1099, MXCIX +1100, MC +1101, MCI +1102, MCII +1103, MCIII +1104, MCIV +1105, MCV +1106, MCVI +1107, MCVII +1108, MCVIII +1109, MCIX +1110, MCX +1111, MCXI +1112, MCXII +1113, MCXIII +1114, MCXIV +1115, MCXV +1116, MCXVI +1117, MCXVII +1118, MCXVIII +1119, MCXIX +1120, MCXX +1121, MCXXI +1122, MCXXII +1123, MCXXIII +1124, MCXXIV +1125, MCXXV +1126, MCXXVI +1127, MCXXVII +1128, MCXXVIII +1129, MCXXIX +1130, MCXXX +1131, MCXXXI +1132, MCXXXII +1133, MCXXXIII +1134, MCXXXIV +1135, MCXXXV +1136, MCXXXVI +1137, MCXXXVII +1138, MCXXXVIII +1139, MCXXXIX +1140, MCXL +1141, MCXLI +1142, MCXLII +1143, MCXLIII +1144, MCXLIV +1145, MCXLV +1146, MCXLVI +1147, MCXLVII +1148, MCXLVIII +1149, MCXLIX +1150, MCL +1151, MCLI +1152, MCLII +1153, MCLIII +1154, MCLIV +1155, MCLV +1156, MCLVI +1157, MCLVII +1158, MCLVIII +1159, MCLIX +1160, MCLX +1161, MCLXI +1162, MCLXII +1163, MCLXIII +1164, MCLXIV +1165, MCLXV +1166, MCLXVI +1167, MCLXVII +1168, MCLXVIII +1169, MCLXIX +1170, MCLXX +1171, MCLXXI +1172, MCLXXII +1173, MCLXXIII +1174, MCLXXIV +1175, MCLXXV +1176, MCLXXVI +1177, MCLXXVII +1178, MCLXXVIII +1179, MCLXXIX +1180, MCLXXX +1181, MCLXXXI +1182, MCLXXXII +1183, MCLXXXIII +1184, MCLXXXIV +1185, MCLXXXV +1186, MCLXXXVI +1187, MCLXXXVII +1188, MCLXXXVIII +1189, MCLXXXIX +1190, MCXC +1191, MCXCI +1192, MCXCII +1193, MCXCIII +1194, MCXCIV +1195, MCXCV +1196, MCXCVI +1197, MCXCVII +1198, MCXCVIII +1199, MCXCIX +1200, MCC +1201, MCCI +1202, MCCII +1203, MCCIII +1204, MCCIV +1205, MCCV +1206, MCCVI +1207, MCCVII +1208, MCCVIII +1209, MCCIX +1210, MCCX +1211, MCCXI +1212, MCCXII +1213, MCCXIII +1214, MCCXIV +1215, MCCXV +1216, MCCXVI +1217, MCCXVII +1218, MCCXVIII +1219, MCCXIX +1220, MCCXX +1221, MCCXXI +1222, MCCXXII +1223, MCCXXIII +1224, MCCXXIV +1225, MCCXXV +1226, MCCXXVI +1227, MCCXXVII +1228, MCCXXVIII +1229, MCCXXIX +1230, MCCXXX +1231, MCCXXXI +1232, MCCXXXII +1233, MCCXXXIII +1234, MCCXXXIV +1235, MCCXXXV +1236, MCCXXXVI +1237, MCCXXXVII +1238, MCCXXXVIII +1239, MCCXXXIX +1240, MCCXL +1241, MCCXLI +1242, MCCXLII +1243, MCCXLIII +1244, MCCXLIV +1245, MCCXLV +1246, MCCXLVI +1247, MCCXLVII +1248, MCCXLVIII +1249, MCCXLIX +1250, MCCL +1251, MCCLI +1252, MCCLII +1253, MCCLIII +1254, MCCLIV +1255, MCCLV +1256, MCCLVI +1257, MCCLVII +1258, MCCLVIII +1259, MCCLIX +1260, MCCLX +1261, MCCLXI +1262, MCCLXII +1263, MCCLXIII +1264, MCCLXIV +1265, MCCLXV +1266, MCCLXVI +1267, MCCLXVII +1268, MCCLXVIII +1269, MCCLXIX +1270, MCCLXX +1271, MCCLXXI +1272, MCCLXXII +1273, MCCLXXIII +1274, MCCLXXIV +1275, MCCLXXV +1276, MCCLXXVI +1277, MCCLXXVII +1278, MCCLXXVIII +1279, MCCLXXIX +1280, MCCLXXX +1281, MCCLXXXI +1282, MCCLXXXII +1283, MCCLXXXIII +1284, MCCLXXXIV +1285, MCCLXXXV +1286, MCCLXXXVI +1287, MCCLXXXVII +1288, MCCLXXXVIII +1289, MCCLXXXIX +1290, MCCXC +1291, MCCXCI +1292, MCCXCII +1293, MCCXCIII +1294, MCCXCIV +1295, MCCXCV +1296, MCCXCVI +1297, MCCXCVII +1298, MCCXCVIII +1299, MCCXCIX +1300, MCCC +1301, MCCCI +1302, MCCCII +1303, MCCCIII +1304, MCCCIV +1305, MCCCV +1306, MCCCVI +1307, MCCCVII +1308, MCCCVIII +1309, MCCCIX +1310, MCCCX +1311, MCCCXI +1312, MCCCXII +1313, MCCCXIII +1314, MCCCXIV +1315, MCCCXV +1316, MCCCXVI +1317, MCCCXVII +1318, MCCCXVIII +1319, MCCCXIX +1320, MCCCXX +1321, MCCCXXI +1322, MCCCXXII +1323, MCCCXXIII +1324, MCCCXXIV +1325, MCCCXXV +1326, MCCCXXVI +1327, MCCCXXVII +1328, MCCCXXVIII +1329, MCCCXXIX +1330, MCCCXXX +1331, MCCCXXXI +1332, MCCCXXXII +1333, MCCCXXXIII +1334, MCCCXXXIV +1335, MCCCXXXV +1336, MCCCXXXVI +1337, MCCCXXXVII +1338, MCCCXXXVIII +1339, MCCCXXXIX +1340, MCCCXL +1341, MCCCXLI +1342, MCCCXLII +1343, MCCCXLIII +1344, MCCCXLIV +1345, MCCCXLV +1346, MCCCXLVI +1347, MCCCXLVII +1348, MCCCXLVIII +1349, MCCCXLIX +1350, MCCCL +1351, MCCCLI +1352, MCCCLII +1353, MCCCLIII +1354, MCCCLIV +1355, MCCCLV +1356, MCCCLVI +1357, MCCCLVII +1358, MCCCLVIII +1359, MCCCLIX +1360, MCCCLX +1361, MCCCLXI +1362, MCCCLXII +1363, MCCCLXIII +1364, MCCCLXIV +1365, MCCCLXV +1366, MCCCLXVI +1367, MCCCLXVII +1368, MCCCLXVIII +1369, MCCCLXIX +1370, MCCCLXX +1371, MCCCLXXI +1372, MCCCLXXII +1373, MCCCLXXIII +1374, MCCCLXXIV +1375, MCCCLXXV +1376, MCCCLXXVI +1377, MCCCLXXVII +1378, MCCCLXXVIII +1379, MCCCLXXIX +1380, MCCCLXXX +1381, MCCCLXXXI +1382, MCCCLXXXII +1383, MCCCLXXXIII +1384, MCCCLXXXIV +1385, MCCCLXXXV +1386, MCCCLXXXVI +1387, MCCCLXXXVII +1388, MCCCLXXXVIII +1389, MCCCLXXXIX +1390, MCCCXC +1391, MCCCXCI +1392, MCCCXCII +1393, MCCCXCIII +1394, MCCCXCIV +1395, MCCCXCV +1396, MCCCXCVI +1397, MCCCXCVII +1398, MCCCXCVIII +1399, MCCCXCIX +1400, MCD +1401, MCDI +1402, MCDII +1403, MCDIII +1404, MCDIV +1405, MCDV +1406, MCDVI +1407, MCDVII +1408, MCDVIII +1409, MCDIX +1410, MCDX +1411, MCDXI +1412, MCDXII +1413, MCDXIII +1414, MCDXIV +1415, MCDXV +1416, MCDXVI +1417, MCDXVII +1418, MCDXVIII +1419, MCDXIX +1420, MCDXX +1421, MCDXXI +1422, MCDXXII +1423, MCDXXIII +1424, MCDXXIV +1425, MCDXXV +1426, MCDXXVI +1427, MCDXXVII +1428, MCDXXVIII +1429, MCDXXIX +1430, MCDXXX +1431, MCDXXXI +1432, MCDXXXII +1433, MCDXXXIII +1434, MCDXXXIV +1435, MCDXXXV +1436, MCDXXXVI +1437, MCDXXXVII +1438, MCDXXXVIII +1439, MCDXXXIX +1440, MCDXL +1441, MCDXLI +1442, MCDXLII +1443, MCDXLIII +1444, MCDXLIV +1445, MCDXLV +1446, MCDXLVI +1447, MCDXLVII +1448, MCDXLVIII +1449, MCDXLIX +1450, MCDL +1451, MCDLI +1452, MCDLII +1453, MCDLIII +1454, MCDLIV +1455, MCDLV +1456, MCDLVI +1457, MCDLVII +1458, MCDLVIII +1459, MCDLIX +1460, MCDLX +1461, MCDLXI +1462, MCDLXII +1463, MCDLXIII +1464, MCDLXIV +1465, MCDLXV +1466, MCDLXVI +1467, MCDLXVII +1468, MCDLXVIII +1469, MCDLXIX +1470, MCDLXX +1471, MCDLXXI +1472, MCDLXXII +1473, MCDLXXIII +1474, MCDLXXIV +1475, MCDLXXV +1476, MCDLXXVI +1477, MCDLXXVII +1478, MCDLXXVIII +1479, MCDLXXIX +1480, MCDLXXX +1481, MCDLXXXI +1482, MCDLXXXII +1483, MCDLXXXIII +1484, MCDLXXXIV +1485, MCDLXXXV +1486, MCDLXXXVI +1487, MCDLXXXVII +1488, MCDLXXXVIII +1489, MCDLXXXIX +1490, MCDXC +1491, MCDXCI +1492, MCDXCII +1493, MCDXCIII +1494, MCDXCIV +1495, MCDXCV +1496, MCDXCVI +1497, MCDXCVII +1498, MCDXCVIII +1499, MCDXCIX +1500, MD +1501, MDI +1502, MDII +1503, MDIII +1504, MDIV +1505, MDV +1506, MDVI +1507, MDVII +1508, MDVIII +1509, MDIX +1510, MDX +1511, MDXI +1512, MDXII +1513, MDXIII +1514, MDXIV +1515, MDXV +1516, MDXVI +1517, MDXVII +1518, MDXVIII +1519, MDXIX +1520, MDXX +1521, MDXXI +1522, MDXXII +1523, MDXXIII +1524, MDXXIV +1525, MDXXV +1526, MDXXVI +1527, MDXXVII +1528, MDXXVIII +1529, MDXXIX +1530, MDXXX +1531, MDXXXI +1532, MDXXXII +1533, MDXXXIII +1534, MDXXXIV +1535, MDXXXV +1536, MDXXXVI +1537, MDXXXVII +1538, MDXXXVIII +1539, MDXXXIX +1540, MDXL +1541, MDXLI +1542, MDXLII +1543, MDXLIII +1544, MDXLIV +1545, MDXLV +1546, MDXLVI +1547, MDXLVII +1548, MDXLVIII +1549, MDXLIX +1550, MDL +1551, MDLI +1552, MDLII +1553, MDLIII +1554, MDLIV +1555, MDLV +1556, MDLVI +1557, MDLVII +1558, MDLVIII +1559, MDLIX +1560, MDLX +1561, MDLXI +1562, MDLXII +1563, MDLXIII +1564, MDLXIV +1565, MDLXV +1566, MDLXVI +1567, MDLXVII +1568, MDLXVIII +1569, MDLXIX +1570, MDLXX +1571, MDLXXI +1572, MDLXXII +1573, MDLXXIII +1574, MDLXXIV +1575, MDLXXV +1576, MDLXXVI +1577, MDLXXVII +1578, MDLXXVIII +1579, MDLXXIX +1580, MDLXXX +1581, MDLXXXI +1582, MDLXXXII +1583, MDLXXXIII +1584, MDLXXXIV +1585, MDLXXXV +1586, MDLXXXVI +1587, MDLXXXVII +1588, MDLXXXVIII +1589, MDLXXXIX +1590, MDXC +1591, MDXCI +1592, MDXCII +1593, MDXCIII +1594, MDXCIV +1595, MDXCV +1596, MDXCVI +1597, MDXCVII +1598, MDXCVIII +1599, MDXCIX +1600, MDC +1601, MDCI +1602, MDCII +1603, MDCIII +1604, MDCIV +1605, MDCV +1606, MDCVI +1607, MDCVII +1608, MDCVIII +1609, MDCIX +1610, MDCX +1611, MDCXI +1612, MDCXII +1613, MDCXIII +1614, MDCXIV +1615, MDCXV +1616, MDCXVI +1617, MDCXVII +1618, MDCXVIII +1619, MDCXIX +1620, MDCXX +1621, MDCXXI +1622, MDCXXII +1623, MDCXXIII +1624, MDCXXIV +1625, MDCXXV +1626, MDCXXVI +1627, MDCXXVII +1628, MDCXXVIII +1629, MDCXXIX +1630, MDCXXX +1631, MDCXXXI +1632, MDCXXXII +1633, MDCXXXIII +1634, MDCXXXIV +1635, MDCXXXV +1636, MDCXXXVI +1637, MDCXXXVII +1638, MDCXXXVIII +1639, MDCXXXIX +1640, MDCXL +1641, MDCXLI +1642, MDCXLII +1643, MDCXLIII +1644, MDCXLIV +1645, MDCXLV +1646, MDCXLVI +1647, MDCXLVII +1648, MDCXLVIII +1649, MDCXLIX +1650, MDCL +1651, MDCLI +1652, MDCLII +1653, MDCLIII +1654, MDCLIV +1655, MDCLV +1656, MDCLVI +1657, MDCLVII +1658, MDCLVIII +1659, MDCLIX +1660, MDCLX +1661, MDCLXI +1662, MDCLXII +1663, MDCLXIII +1664, MDCLXIV +1665, MDCLXV +1666, MDCLXVI +1667, MDCLXVII +1668, MDCLXVIII +1669, MDCLXIX +1670, MDCLXX +1671, MDCLXXI +1672, MDCLXXII +1673, MDCLXXIII +1674, MDCLXXIV +1675, MDCLXXV +1676, MDCLXXVI +1677, MDCLXXVII +1678, MDCLXXVIII +1679, MDCLXXIX +1680, MDCLXXX +1681, MDCLXXXI +1682, MDCLXXXII +1683, MDCLXXXIII +1684, MDCLXXXIV +1685, MDCLXXXV +1686, MDCLXXXVI +1687, MDCLXXXVII +1688, MDCLXXXVIII +1689, MDCLXXXIX +1690, MDCXC +1691, MDCXCI +1692, MDCXCII +1693, MDCXCIII +1694, MDCXCIV +1695, MDCXCV +1696, MDCXCVI +1697, MDCXCVII +1698, MDCXCVIII +1699, MDCXCIX +1700, MDCC +1701, MDCCI +1702, MDCCII +1703, MDCCIII +1704, MDCCIV +1705, MDCCV +1706, MDCCVI +1707, MDCCVII +1708, MDCCVIII +1709, MDCCIX +1710, MDCCX +1711, MDCCXI +1712, MDCCXII +1713, MDCCXIII +1714, MDCCXIV +1715, MDCCXV +1716, MDCCXVI +1717, MDCCXVII +1718, MDCCXVIII +1719, MDCCXIX +1720, MDCCXX +1721, MDCCXXI +1722, MDCCXXII +1723, MDCCXXIII +1724, MDCCXXIV +1725, MDCCXXV +1726, MDCCXXVI +1727, MDCCXXVII +1728, MDCCXXVIII +1729, MDCCXXIX +1730, MDCCXXX +1731, MDCCXXXI +1732, MDCCXXXII +1733, MDCCXXXIII +1734, MDCCXXXIV +1735, MDCCXXXV +1736, MDCCXXXVI +1737, MDCCXXXVII +1738, MDCCXXXVIII +1739, MDCCXXXIX +1740, MDCCXL +1741, MDCCXLI +1742, MDCCXLII +1743, MDCCXLIII +1744, MDCCXLIV +1745, MDCCXLV +1746, MDCCXLVI +1747, MDCCXLVII +1748, MDCCXLVIII +1749, MDCCXLIX +1750, MDCCL +1751, MDCCLI +1752, MDCCLII +1753, MDCCLIII +1754, MDCCLIV +1755, MDCCLV +1756, MDCCLVI +1757, MDCCLVII +1758, MDCCLVIII +1759, MDCCLIX +1760, MDCCLX +1761, MDCCLXI +1762, MDCCLXII +1763, MDCCLXIII +1764, MDCCLXIV +1765, MDCCLXV +1766, MDCCLXVI +1767, MDCCLXVII +1768, MDCCLXVIII +1769, MDCCLXIX +1770, MDCCLXX +1771, MDCCLXXI +1772, MDCCLXXII +1773, MDCCLXXIII +1774, MDCCLXXIV +1775, MDCCLXXV +1776, MDCCLXXVI +1777, MDCCLXXVII +1778, MDCCLXXVIII +1779, MDCCLXXIX +1780, MDCCLXXX +1781, MDCCLXXXI +1782, MDCCLXXXII +1783, MDCCLXXXIII +1784, MDCCLXXXIV +1785, MDCCLXXXV +1786, MDCCLXXXVI +1787, MDCCLXXXVII +1788, MDCCLXXXVIII +1789, MDCCLXXXIX +1790, MDCCXC +1791, MDCCXCI +1792, MDCCXCII +1793, MDCCXCIII +1794, MDCCXCIV +1795, MDCCXCV +1796, MDCCXCVI +1797, MDCCXCVII +1798, MDCCXCVIII +1799, MDCCXCIX +1800, MDCCC +1801, MDCCCI +1802, MDCCCII +1803, MDCCCIII +1804, MDCCCIV +1805, MDCCCV +1806, MDCCCVI +1807, MDCCCVII +1808, MDCCCVIII +1809, MDCCCIX +1810, MDCCCX +1811, MDCCCXI +1812, MDCCCXII +1813, MDCCCXIII +1814, MDCCCXIV +1815, MDCCCXV +1816, MDCCCXVI +1817, MDCCCXVII +1818, MDCCCXVIII +1819, MDCCCXIX +1820, MDCCCXX +1821, MDCCCXXI +1822, MDCCCXXII +1823, MDCCCXXIII +1824, MDCCCXXIV +1825, MDCCCXXV +1826, MDCCCXXVI +1827, MDCCCXXVII +1828, MDCCCXXVIII +1829, MDCCCXXIX +1830, MDCCCXXX +1831, MDCCCXXXI +1832, MDCCCXXXII +1833, MDCCCXXXIII +1834, MDCCCXXXIV +1835, MDCCCXXXV +1836, MDCCCXXXVI +1837, MDCCCXXXVII +1838, MDCCCXXXVIII +1839, MDCCCXXXIX +1840, MDCCCXL +1841, MDCCCXLI +1842, MDCCCXLII +1843, MDCCCXLIII +1844, MDCCCXLIV +1845, MDCCCXLV +1846, MDCCCXLVI +1847, MDCCCXLVII +1848, MDCCCXLVIII +1849, MDCCCXLIX +1850, MDCCCL +1851, MDCCCLI +1852, MDCCCLII +1853, MDCCCLIII +1854, MDCCCLIV +1855, MDCCCLV +1856, MDCCCLVI +1857, MDCCCLVII +1858, MDCCCLVIII +1859, MDCCCLIX +1860, MDCCCLX +1861, MDCCCLXI +1862, MDCCCLXII +1863, MDCCCLXIII +1864, MDCCCLXIV +1865, MDCCCLXV +1866, MDCCCLXVI +1867, MDCCCLXVII +1868, MDCCCLXVIII +1869, MDCCCLXIX +1870, MDCCCLXX +1871, MDCCCLXXI +1872, MDCCCLXXII +1873, MDCCCLXXIII +1874, MDCCCLXXIV +1875, MDCCCLXXV +1876, MDCCCLXXVI +1877, MDCCCLXXVII +1878, MDCCCLXXVIII +1879, MDCCCLXXIX +1880, MDCCCLXXX +1881, MDCCCLXXXI +1882, MDCCCLXXXII +1883, MDCCCLXXXIII +1884, MDCCCLXXXIV +1885, MDCCCLXXXV +1886, MDCCCLXXXVI +1887, MDCCCLXXXVII +1888, MDCCCLXXXVIII +1889, MDCCCLXXXIX +1890, MDCCCXC +1891, MDCCCXCI +1892, MDCCCXCII +1893, MDCCCXCIII +1894, MDCCCXCIV +1895, MDCCCXCV +1896, MDCCCXCVI +1897, MDCCCXCVII +1898, MDCCCXCVIII +1899, MDCCCXCIX +1900, MCM +1901, MCMI +1902, MCMII +1903, MCMIII +1904, MCMIV +1905, MCMV +1906, MCMVI +1907, MCMVII +1908, MCMVIII +1909, MCMIX +1910, MCMX +1911, MCMXI +1912, MCMXII +1913, MCMXIII +1914, MCMXIV +1915, MCMXV +1916, MCMXVI +1917, MCMXVII +1918, MCMXVIII +1919, MCMXIX +1920, MCMXX +1921, MCMXXI +1922, MCMXXII +1923, MCMXXIII +1924, MCMXXIV +1925, MCMXXV +1926, MCMXXVI +1927, MCMXXVII +1928, MCMXXVIII +1929, MCMXXIX +1930, MCMXXX +1931, MCMXXXI +1932, MCMXXXII +1933, MCMXXXIII +1934, MCMXXXIV +1935, MCMXXXV +1936, MCMXXXVI +1937, MCMXXXVII +1938, MCMXXXVIII +1939, MCMXXXIX +1940, MCMXL +1941, MCMXLI +1942, MCMXLII +1943, MCMXLIII +1944, MCMXLIV +1945, MCMXLV +1946, MCMXLVI +1947, MCMXLVII +1948, MCMXLVIII +1949, MCMXLIX +1950, MCML +1951, MCMLI +1952, MCMLII +1953, MCMLIII +1954, MCMLIV +1955, MCMLV +1956, MCMLVI +1957, MCMLVII +1958, MCMLVIII +1959, MCMLIX +1960, MCMLX +1961, MCMLXI +1962, MCMLXII +1963, MCMLXIII +1964, MCMLXIV +1965, MCMLXV +1966, MCMLXVI +1967, MCMLXVII +1968, MCMLXVIII +1969, MCMLXIX +1970, MCMLXX +1971, MCMLXXI +1972, MCMLXXII +1973, MCMLXXIII +1974, MCMLXXIV +1975, MCMLXXV +1976, MCMLXXVI +1977, MCMLXXVII +1978, MCMLXXVIII +1979, MCMLXXIX +1980, MCMLXXX +1981, MCMLXXXI +1982, MCMLXXXII +1983, MCMLXXXIII +1984, MCMLXXXIV +1985, MCMLXXXV +1986, MCMLXXXVI +1987, MCMLXXXVII +1988, MCMLXXXVIII +1989, MCMLXXXIX +1990, MCMXC +1991, MCMXCI +1992, MCMXCII +1993, MCMXCIII +1994, MCMXCIV +1995, MCMXCV +1996, MCMXCVI +1997, MCMXCVII +1998, MCMXCVIII +1999, MCMXCIX +2000, MM +2001, MMI +2002, MMII +2003, MMIII +2004, MMIV +2005, MMV +2006, MMVI +2007, MMVII +2008, MMVIII +2009, MMIX +2010, MMX +2011, MMXI +2012, MMXII +2013, MMXIII +2014, MMXIV +2015, MMXV +2016, MMXVI +2017, MMXVII +2018, MMXVIII +2019, MMXIX +2020, MMXX +2021, MMXXI +2022, MMXXII +2023, MMXXIII +2024, MMXXIV +2025, MMXXV +2026, MMXXVI +2027, MMXXVII +2028, MMXXVIII +2029, MMXXIX +2030, MMXXX +2031, MMXXXI +2032, MMXXXII +2033, MMXXXIII +2034, MMXXXIV +2035, MMXXXV +2036, MMXXXVI +2037, MMXXXVII +2038, MMXXXVIII +2039, MMXXXIX +2040, MMXL +2041, MMXLI +2042, MMXLII +2043, MMXLIII +2044, MMXLIV +2045, MMXLV +2046, MMXLVI +2047, MMXLVII +2048, MMXLVIII +2049, MMXLIX +2050, MML +2051, MMLI +2052, MMLII +2053, MMLIII +2054, MMLIV +2055, MMLV +2056, MMLVI +2057, MMLVII +2058, MMLVIII +2059, MMLIX +2060, MMLX +2061, MMLXI +2062, MMLXII +2063, MMLXIII +2064, MMLXIV +2065, MMLXV +2066, MMLXVI +2067, MMLXVII +2068, MMLXVIII +2069, MMLXIX +2070, MMLXX +2071, MMLXXI +2072, MMLXXII +2073, MMLXXIII +2074, MMLXXIV +2075, MMLXXV +2076, MMLXXVI +2077, MMLXXVII +2078, MMLXXVIII +2079, MMLXXIX +2080, MMLXXX +2081, MMLXXXI +2082, MMLXXXII +2083, MMLXXXIII +2084, MMLXXXIV +2085, MMLXXXV +2086, MMLXXXVI +2087, MMLXXXVII +2088, MMLXXXVIII +2089, MMLXXXIX +2090, MMXC +2091, MMXCI +2092, MMXCII +2093, MMXCIII +2094, MMXCIV +2095, MMXCV +2096, MMXCVI +2097, MMXCVII +2098, MMXCVIII +2099, MMXCIX +2100, MMC +2101, MMCI +2102, MMCII +2103, MMCIII +2104, MMCIV +2105, MMCV +2106, MMCVI +2107, MMCVII +2108, MMCVIII +2109, MMCIX +2110, MMCX +2111, MMCXI +2112, MMCXII +2113, MMCXIII +2114, MMCXIV +2115, MMCXV +2116, MMCXVI +2117, MMCXVII +2118, MMCXVIII +2119, MMCXIX +2120, MMCXX +2121, MMCXXI +2122, MMCXXII +2123, MMCXXIII +2124, MMCXXIV +2125, MMCXXV +2126, MMCXXVI +2127, MMCXXVII +2128, MMCXXVIII +2129, MMCXXIX +2130, MMCXXX +2131, MMCXXXI +2132, MMCXXXII +2133, MMCXXXIII +2134, MMCXXXIV +2135, MMCXXXV +2136, MMCXXXVI +2137, MMCXXXVII +2138, MMCXXXVIII +2139, MMCXXXIX +2140, MMCXL +2141, MMCXLI +2142, MMCXLII +2143, MMCXLIII +2144, MMCXLIV +2145, MMCXLV +2146, MMCXLVI +2147, MMCXLVII +2148, MMCXLVIII +2149, MMCXLIX +2150, MMCL +2151, MMCLI +2152, MMCLII +2153, MMCLIII +2154, MMCLIV +2155, MMCLV +2156, MMCLVI +2157, MMCLVII +2158, MMCLVIII +2159, MMCLIX +2160, MMCLX +2161, MMCLXI +2162, MMCLXII +2163, MMCLXIII +2164, MMCLXIV +2165, MMCLXV +2166, MMCLXVI +2167, MMCLXVII +2168, MMCLXVIII +2169, MMCLXIX +2170, MMCLXX +2171, MMCLXXI +2172, MMCLXXII +2173, MMCLXXIII +2174, MMCLXXIV +2175, MMCLXXV +2176, MMCLXXVI +2177, MMCLXXVII +2178, MMCLXXVIII +2179, MMCLXXIX +2180, MMCLXXX +2181, MMCLXXXI +2182, MMCLXXXII +2183, MMCLXXXIII +2184, MMCLXXXIV +2185, MMCLXXXV +2186, MMCLXXXVI +2187, MMCLXXXVII +2188, MMCLXXXVIII +2189, MMCLXXXIX +2190, MMCXC +2191, MMCXCI +2192, MMCXCII +2193, MMCXCIII +2194, MMCXCIV +2195, MMCXCV +2196, MMCXCVI +2197, MMCXCVII +2198, MMCXCVIII +2199, MMCXCIX +2200, MMCC +2201, MMCCI +2202, MMCCII +2203, MMCCIII +2204, MMCCIV +2205, MMCCV +2206, MMCCVI +2207, MMCCVII +2208, MMCCVIII +2209, MMCCIX +2210, MMCCX +2211, MMCCXI +2212, MMCCXII +2213, MMCCXIII +2214, MMCCXIV +2215, MMCCXV +2216, MMCCXVI +2217, MMCCXVII +2218, MMCCXVIII +2219, MMCCXIX +2220, MMCCXX +2221, MMCCXXI +2222, MMCCXXII +2223, MMCCXXIII +2224, MMCCXXIV +2225, MMCCXXV +2226, MMCCXXVI +2227, MMCCXXVII +2228, MMCCXXVIII +2229, MMCCXXIX +2230, MMCCXXX +2231, MMCCXXXI +2232, MMCCXXXII +2233, MMCCXXXIII +2234, MMCCXXXIV +2235, MMCCXXXV +2236, MMCCXXXVI +2237, MMCCXXXVII +2238, MMCCXXXVIII +2239, MMCCXXXIX +2240, MMCCXL +2241, MMCCXLI +2242, MMCCXLII +2243, MMCCXLIII +2244, MMCCXLIV +2245, MMCCXLV +2246, MMCCXLVI +2247, MMCCXLVII +2248, MMCCXLVIII +2249, MMCCXLIX +2250, MMCCL +2251, MMCCLI +2252, MMCCLII +2253, MMCCLIII +2254, MMCCLIV +2255, MMCCLV +2256, MMCCLVI +2257, MMCCLVII +2258, MMCCLVIII +2259, MMCCLIX +2260, MMCCLX +2261, MMCCLXI +2262, MMCCLXII +2263, MMCCLXIII +2264, MMCCLXIV +2265, MMCCLXV +2266, MMCCLXVI +2267, MMCCLXVII +2268, MMCCLXVIII +2269, MMCCLXIX +2270, MMCCLXX +2271, MMCCLXXI +2272, MMCCLXXII +2273, MMCCLXXIII +2274, MMCCLXXIV +2275, MMCCLXXV +2276, MMCCLXXVI +2277, MMCCLXXVII +2278, MMCCLXXVIII +2279, MMCCLXXIX +2280, MMCCLXXX +2281, MMCCLXXXI +2282, MMCCLXXXII +2283, MMCCLXXXIII +2284, MMCCLXXXIV +2285, MMCCLXXXV +2286, MMCCLXXXVI +2287, MMCCLXXXVII +2288, MMCCLXXXVIII +2289, MMCCLXXXIX +2290, MMCCXC +2291, MMCCXCI +2292, MMCCXCII +2293, MMCCXCIII +2294, MMCCXCIV +2295, MMCCXCV +2296, MMCCXCVI +2297, MMCCXCVII +2298, MMCCXCVIII +2299, MMCCXCIX +2300, MMCCC +2301, MMCCCI +2302, MMCCCII +2303, MMCCCIII +2304, MMCCCIV +2305, MMCCCV +2306, MMCCCVI +2307, MMCCCVII +2308, MMCCCVIII +2309, MMCCCIX +2310, MMCCCX +2311, MMCCCXI +2312, MMCCCXII +2313, MMCCCXIII +2314, MMCCCXIV +2315, MMCCCXV +2316, MMCCCXVI +2317, MMCCCXVII +2318, MMCCCXVIII +2319, MMCCCXIX +2320, MMCCCXX +2321, MMCCCXXI +2322, MMCCCXXII +2323, MMCCCXXIII +2324, MMCCCXXIV +2325, MMCCCXXV +2326, MMCCCXXVI +2327, MMCCCXXVII +2328, MMCCCXXVIII +2329, MMCCCXXIX +2330, MMCCCXXX +2331, MMCCCXXXI +2332, MMCCCXXXII +2333, MMCCCXXXIII +2334, MMCCCXXXIV +2335, MMCCCXXXV +2336, MMCCCXXXVI +2337, MMCCCXXXVII +2338, MMCCCXXXVIII +2339, MMCCCXXXIX +2340, MMCCCXL +2341, MMCCCXLI +2342, MMCCCXLII +2343, MMCCCXLIII +2344, MMCCCXLIV +2345, MMCCCXLV +2346, MMCCCXLVI +2347, MMCCCXLVII +2348, MMCCCXLVIII +2349, MMCCCXLIX +2350, MMCCCL +2351, MMCCCLI +2352, MMCCCLII +2353, MMCCCLIII +2354, MMCCCLIV +2355, MMCCCLV +2356, MMCCCLVI +2357, MMCCCLVII +2358, MMCCCLVIII +2359, MMCCCLIX +2360, MMCCCLX +2361, MMCCCLXI +2362, MMCCCLXII +2363, MMCCCLXIII +2364, MMCCCLXIV +2365, MMCCCLXV +2366, MMCCCLXVI +2367, MMCCCLXVII +2368, MMCCCLXVIII +2369, MMCCCLXIX +2370, MMCCCLXX +2371, MMCCCLXXI +2372, MMCCCLXXII +2373, MMCCCLXXIII +2374, MMCCCLXXIV +2375, MMCCCLXXV +2376, MMCCCLXXVI +2377, MMCCCLXXVII +2378, MMCCCLXXVIII +2379, MMCCCLXXIX +2380, MMCCCLXXX +2381, MMCCCLXXXI +2382, MMCCCLXXXII +2383, MMCCCLXXXIII +2384, MMCCCLXXXIV +2385, MMCCCLXXXV +2386, MMCCCLXXXVI +2387, MMCCCLXXXVII +2388, MMCCCLXXXVIII +2389, MMCCCLXXXIX +2390, MMCCCXC +2391, MMCCCXCI +2392, MMCCCXCII +2393, MMCCCXCIII +2394, MMCCCXCIV +2395, MMCCCXCV +2396, MMCCCXCVI +2397, MMCCCXCVII +2398, MMCCCXCVIII +2399, MMCCCXCIX +2400, MMCD +2401, MMCDI +2402, MMCDII +2403, MMCDIII +2404, MMCDIV +2405, MMCDV +2406, MMCDVI +2407, MMCDVII +2408, MMCDVIII +2409, MMCDIX +2410, MMCDX +2411, MMCDXI +2412, MMCDXII +2413, MMCDXIII +2414, MMCDXIV +2415, MMCDXV +2416, MMCDXVI +2417, MMCDXVII +2418, MMCDXVIII +2419, MMCDXIX +2420, MMCDXX +2421, MMCDXXI +2422, MMCDXXII +2423, MMCDXXIII +2424, MMCDXXIV +2425, MMCDXXV +2426, MMCDXXVI +2427, MMCDXXVII +2428, MMCDXXVIII +2429, MMCDXXIX +2430, MMCDXXX +2431, MMCDXXXI +2432, MMCDXXXII +2433, MMCDXXXIII +2434, MMCDXXXIV +2435, MMCDXXXV +2436, MMCDXXXVI +2437, MMCDXXXVII +2438, MMCDXXXVIII +2439, MMCDXXXIX +2440, MMCDXL +2441, MMCDXLI +2442, MMCDXLII +2443, MMCDXLIII +2444, MMCDXLIV +2445, MMCDXLV +2446, MMCDXLVI +2447, MMCDXLVII +2448, MMCDXLVIII +2449, MMCDXLIX +2450, MMCDL +2451, MMCDLI +2452, MMCDLII +2453, MMCDLIII +2454, MMCDLIV +2455, MMCDLV +2456, MMCDLVI +2457, MMCDLVII +2458, MMCDLVIII +2459, MMCDLIX +2460, MMCDLX +2461, MMCDLXI +2462, MMCDLXII +2463, MMCDLXIII +2464, MMCDLXIV +2465, MMCDLXV +2466, MMCDLXVI +2467, MMCDLXVII +2468, MMCDLXVIII +2469, MMCDLXIX +2470, MMCDLXX +2471, MMCDLXXI +2472, MMCDLXXII +2473, MMCDLXXIII +2474, MMCDLXXIV +2475, MMCDLXXV +2476, MMCDLXXVI +2477, MMCDLXXVII +2478, MMCDLXXVIII +2479, MMCDLXXIX +2480, MMCDLXXX +2481, MMCDLXXXI +2482, MMCDLXXXII +2483, MMCDLXXXIII +2484, MMCDLXXXIV +2485, MMCDLXXXV +2486, MMCDLXXXVI +2487, MMCDLXXXVII +2488, MMCDLXXXVIII +2489, MMCDLXXXIX +2490, MMCDXC +2491, MMCDXCI +2492, MMCDXCII +2493, MMCDXCIII +2494, MMCDXCIV +2495, MMCDXCV +2496, MMCDXCVI +2497, MMCDXCVII +2498, MMCDXCVIII +2499, MMCDXCIX +2500, MMD +2501, MMDI +2502, MMDII +2503, MMDIII +2504, MMDIV +2505, MMDV +2506, MMDVI +2507, MMDVII +2508, MMDVIII +2509, MMDIX +2510, MMDX +2511, MMDXI +2512, MMDXII +2513, MMDXIII +2514, MMDXIV +2515, MMDXV +2516, MMDXVI +2517, MMDXVII +2518, MMDXVIII +2519, MMDXIX +2520, MMDXX +2521, MMDXXI +2522, MMDXXII +2523, MMDXXIII +2524, MMDXXIV +2525, MMDXXV +2526, MMDXXVI +2527, MMDXXVII +2528, MMDXXVIII +2529, MMDXXIX +2530, MMDXXX +2531, MMDXXXI +2532, MMDXXXII +2533, MMDXXXIII +2534, MMDXXXIV +2535, MMDXXXV +2536, MMDXXXVI +2537, MMDXXXVII +2538, MMDXXXVIII +2539, MMDXXXIX +2540, MMDXL +2541, MMDXLI +2542, MMDXLII +2543, MMDXLIII +2544, MMDXLIV +2545, MMDXLV +2546, MMDXLVI +2547, MMDXLVII +2548, MMDXLVIII +2549, MMDXLIX +2550, MMDL +2551, MMDLI +2552, MMDLII +2553, MMDLIII +2554, MMDLIV +2555, MMDLV +2556, MMDLVI +2557, MMDLVII +2558, MMDLVIII +2559, MMDLIX +2560, MMDLX +2561, MMDLXI +2562, MMDLXII +2563, MMDLXIII +2564, MMDLXIV +2565, MMDLXV +2566, MMDLXVI +2567, MMDLXVII +2568, MMDLXVIII +2569, MMDLXIX +2570, MMDLXX +2571, MMDLXXI +2572, MMDLXXII +2573, MMDLXXIII +2574, MMDLXXIV +2575, MMDLXXV +2576, MMDLXXVI +2577, MMDLXXVII +2578, MMDLXXVIII +2579, MMDLXXIX +2580, MMDLXXX +2581, MMDLXXXI +2582, MMDLXXXII +2583, MMDLXXXIII +2584, MMDLXXXIV +2585, MMDLXXXV +2586, MMDLXXXVI +2587, MMDLXXXVII +2588, MMDLXXXVIII +2589, MMDLXXXIX +2590, MMDXC +2591, MMDXCI +2592, MMDXCII +2593, MMDXCIII +2594, MMDXCIV +2595, MMDXCV +2596, MMDXCVI +2597, MMDXCVII +2598, MMDXCVIII +2599, MMDXCIX +2600, MMDC +2601, MMDCI +2602, MMDCII +2603, MMDCIII +2604, MMDCIV +2605, MMDCV +2606, MMDCVI +2607, MMDCVII +2608, MMDCVIII +2609, MMDCIX +2610, MMDCX +2611, MMDCXI +2612, MMDCXII +2613, MMDCXIII +2614, MMDCXIV +2615, MMDCXV +2616, MMDCXVI +2617, MMDCXVII +2618, MMDCXVIII +2619, MMDCXIX +2620, MMDCXX +2621, MMDCXXI +2622, MMDCXXII +2623, MMDCXXIII +2624, MMDCXXIV +2625, MMDCXXV +2626, MMDCXXVI +2627, MMDCXXVII +2628, MMDCXXVIII +2629, MMDCXXIX +2630, MMDCXXX +2631, MMDCXXXI +2632, MMDCXXXII +2633, MMDCXXXIII +2634, MMDCXXXIV +2635, MMDCXXXV +2636, MMDCXXXVI +2637, MMDCXXXVII +2638, MMDCXXXVIII +2639, MMDCXXXIX +2640, MMDCXL +2641, MMDCXLI +2642, MMDCXLII +2643, MMDCXLIII +2644, MMDCXLIV +2645, MMDCXLV +2646, MMDCXLVI +2647, MMDCXLVII +2648, MMDCXLVIII +2649, MMDCXLIX +2650, MMDCL +2651, MMDCLI +2652, MMDCLII +2653, MMDCLIII +2654, MMDCLIV +2655, MMDCLV +2656, MMDCLVI +2657, MMDCLVII +2658, MMDCLVIII +2659, MMDCLIX +2660, MMDCLX +2661, MMDCLXI +2662, MMDCLXII +2663, MMDCLXIII +2664, MMDCLXIV +2665, MMDCLXV +2666, MMDCLXVI +2667, MMDCLXVII +2668, MMDCLXVIII +2669, MMDCLXIX +2670, MMDCLXX +2671, MMDCLXXI +2672, MMDCLXXII +2673, MMDCLXXIII +2674, MMDCLXXIV +2675, MMDCLXXV +2676, MMDCLXXVI +2677, MMDCLXXVII +2678, MMDCLXXVIII +2679, MMDCLXXIX +2680, MMDCLXXX +2681, MMDCLXXXI +2682, MMDCLXXXII +2683, MMDCLXXXIII +2684, MMDCLXXXIV +2685, MMDCLXXXV +2686, MMDCLXXXVI +2687, MMDCLXXXVII +2688, MMDCLXXXVIII +2689, MMDCLXXXIX +2690, MMDCXC +2691, MMDCXCI +2692, MMDCXCII +2693, MMDCXCIII +2694, MMDCXCIV +2695, MMDCXCV +2696, MMDCXCVI +2697, MMDCXCVII +2698, MMDCXCVIII +2699, MMDCXCIX +2700, MMDCC +2701, MMDCCI +2702, MMDCCII +2703, MMDCCIII +2704, MMDCCIV +2705, MMDCCV +2706, MMDCCVI +2707, MMDCCVII +2708, MMDCCVIII +2709, MMDCCIX +2710, MMDCCX +2711, MMDCCXI +2712, MMDCCXII +2713, MMDCCXIII +2714, MMDCCXIV +2715, MMDCCXV +2716, MMDCCXVI +2717, MMDCCXVII +2718, MMDCCXVIII +2719, MMDCCXIX +2720, MMDCCXX +2721, MMDCCXXI +2722, MMDCCXXII +2723, MMDCCXXIII +2724, MMDCCXXIV +2725, MMDCCXXV +2726, MMDCCXXVI +2727, MMDCCXXVII +2728, MMDCCXXVIII +2729, MMDCCXXIX +2730, MMDCCXXX +2731, MMDCCXXXI +2732, MMDCCXXXII +2733, MMDCCXXXIII +2734, MMDCCXXXIV +2735, MMDCCXXXV +2736, MMDCCXXXVI +2737, MMDCCXXXVII +2738, MMDCCXXXVIII +2739, MMDCCXXXIX +2740, MMDCCXL +2741, MMDCCXLI +2742, MMDCCXLII +2743, MMDCCXLIII +2744, MMDCCXLIV +2745, MMDCCXLV +2746, MMDCCXLVI +2747, MMDCCXLVII +2748, MMDCCXLVIII +2749, MMDCCXLIX +2750, MMDCCL +2751, MMDCCLI +2752, MMDCCLII +2753, MMDCCLIII +2754, MMDCCLIV +2755, MMDCCLV +2756, MMDCCLVI +2757, MMDCCLVII +2758, MMDCCLVIII +2759, MMDCCLIX +2760, MMDCCLX +2761, MMDCCLXI +2762, MMDCCLXII +2763, MMDCCLXIII +2764, MMDCCLXIV +2765, MMDCCLXV +2766, MMDCCLXVI +2767, MMDCCLXVII +2768, MMDCCLXVIII +2769, MMDCCLXIX +2770, MMDCCLXX +2771, MMDCCLXXI +2772, MMDCCLXXII +2773, MMDCCLXXIII +2774, MMDCCLXXIV +2775, MMDCCLXXV +2776, MMDCCLXXVI +2777, MMDCCLXXVII +2778, MMDCCLXXVIII +2779, MMDCCLXXIX +2780, MMDCCLXXX +2781, MMDCCLXXXI +2782, MMDCCLXXXII +2783, MMDCCLXXXIII +2784, MMDCCLXXXIV +2785, MMDCCLXXXV +2786, MMDCCLXXXVI +2787, MMDCCLXXXVII +2788, MMDCCLXXXVIII +2789, MMDCCLXXXIX +2790, MMDCCXC +2791, MMDCCXCI +2792, MMDCCXCII +2793, MMDCCXCIII +2794, MMDCCXCIV +2795, MMDCCXCV +2796, MMDCCXCVI +2797, MMDCCXCVII +2798, MMDCCXCVIII +2799, MMDCCXCIX +2800, MMDCCC +2801, MMDCCCI +2802, MMDCCCII +2803, MMDCCCIII +2804, MMDCCCIV +2805, MMDCCCV +2806, MMDCCCVI +2807, MMDCCCVII +2808, MMDCCCVIII +2809, MMDCCCIX +2810, MMDCCCX +2811, MMDCCCXI +2812, MMDCCCXII +2813, MMDCCCXIII +2814, MMDCCCXIV +2815, MMDCCCXV +2816, MMDCCCXVI +2817, MMDCCCXVII +2818, MMDCCCXVIII +2819, MMDCCCXIX +2820, MMDCCCXX +2821, MMDCCCXXI +2822, MMDCCCXXII +2823, MMDCCCXXIII +2824, MMDCCCXXIV +2825, MMDCCCXXV +2826, MMDCCCXXVI +2827, MMDCCCXXVII +2828, MMDCCCXXVIII +2829, MMDCCCXXIX +2830, MMDCCCXXX +2831, MMDCCCXXXI +2832, MMDCCCXXXII +2833, MMDCCCXXXIII +2834, MMDCCCXXXIV +2835, MMDCCCXXXV +2836, MMDCCCXXXVI +2837, MMDCCCXXXVII +2838, MMDCCCXXXVIII +2839, MMDCCCXXXIX +2840, MMDCCCXL +2841, MMDCCCXLI +2842, MMDCCCXLII +2843, MMDCCCXLIII +2844, MMDCCCXLIV +2845, MMDCCCXLV +2846, MMDCCCXLVI +2847, MMDCCCXLVII +2848, MMDCCCXLVIII +2849, MMDCCCXLIX +2850, MMDCCCL +2851, MMDCCCLI +2852, MMDCCCLII +2853, MMDCCCLIII +2854, MMDCCCLIV +2855, MMDCCCLV +2856, MMDCCCLVI +2857, MMDCCCLVII +2858, MMDCCCLVIII +2859, MMDCCCLIX +2860, MMDCCCLX +2861, MMDCCCLXI +2862, MMDCCCLXII +2863, MMDCCCLXIII +2864, MMDCCCLXIV +2865, MMDCCCLXV +2866, MMDCCCLXVI +2867, MMDCCCLXVII +2868, MMDCCCLXVIII +2869, MMDCCCLXIX +2870, MMDCCCLXX +2871, MMDCCCLXXI +2872, MMDCCCLXXII +2873, MMDCCCLXXIII +2874, MMDCCCLXXIV +2875, MMDCCCLXXV +2876, MMDCCCLXXVI +2877, MMDCCCLXXVII +2878, MMDCCCLXXVIII +2879, MMDCCCLXXIX +2880, MMDCCCLXXX +2881, MMDCCCLXXXI +2882, MMDCCCLXXXII +2883, MMDCCCLXXXIII +2884, MMDCCCLXXXIV +2885, MMDCCCLXXXV +2886, MMDCCCLXXXVI +2887, MMDCCCLXXXVII +2888, MMDCCCLXXXVIII +2889, MMDCCCLXXXIX +2890, MMDCCCXC +2891, MMDCCCXCI +2892, MMDCCCXCII +2893, MMDCCCXCIII +2894, MMDCCCXCIV +2895, MMDCCCXCV +2896, MMDCCCXCVI +2897, MMDCCCXCVII +2898, MMDCCCXCVIII +2899, MMDCCCXCIX +2900, MMCM +2901, MMCMI +2902, MMCMII +2903, MMCMIII +2904, MMCMIV +2905, MMCMV +2906, MMCMVI +2907, MMCMVII +2908, MMCMVIII +2909, MMCMIX +2910, MMCMX +2911, MMCMXI +2912, MMCMXII +2913, MMCMXIII +2914, MMCMXIV +2915, MMCMXV +2916, MMCMXVI +2917, MMCMXVII +2918, MMCMXVIII +2919, MMCMXIX +2920, MMCMXX +2921, MMCMXXI +2922, MMCMXXII +2923, MMCMXXIII +2924, MMCMXXIV +2925, MMCMXXV +2926, MMCMXXVI +2927, MMCMXXVII +2928, MMCMXXVIII +2929, MMCMXXIX +2930, MMCMXXX +2931, MMCMXXXI +2932, MMCMXXXII +2933, MMCMXXXIII +2934, MMCMXXXIV +2935, MMCMXXXV +2936, MMCMXXXVI +2937, MMCMXXXVII +2938, MMCMXXXVIII +2939, MMCMXXXIX +2940, MMCMXL +2941, MMCMXLI +2942, MMCMXLII +2943, MMCMXLIII +2944, MMCMXLIV +2945, MMCMXLV +2946, MMCMXLVI +2947, MMCMXLVII +2948, MMCMXLVIII +2949, MMCMXLIX +2950, MMCML +2951, MMCMLI +2952, MMCMLII +2953, MMCMLIII +2954, MMCMLIV +2955, MMCMLV +2956, MMCMLVI +2957, MMCMLVII +2958, MMCMLVIII +2959, MMCMLIX +2960, MMCMLX +2961, MMCMLXI +2962, MMCMLXII +2963, MMCMLXIII +2964, MMCMLXIV +2965, MMCMLXV +2966, MMCMLXVI +2967, MMCMLXVII +2968, MMCMLXVIII +2969, MMCMLXIX +2970, MMCMLXX +2971, MMCMLXXI +2972, MMCMLXXII +2973, MMCMLXXIII +2974, MMCMLXXIV +2975, MMCMLXXV +2976, MMCMLXXVI +2977, MMCMLXXVII +2978, MMCMLXXVIII +2979, MMCMLXXIX +2980, MMCMLXXX +2981, MMCMLXXXI +2982, MMCMLXXXII +2983, MMCMLXXXIII +2984, MMCMLXXXIV +2985, MMCMLXXXV +2986, MMCMLXXXVI +2987, MMCMLXXXVII +2988, MMCMLXXXVIII +2989, MMCMLXXXIX +2990, MMCMXC +2991, MMCMXCI +2992, MMCMXCII +2993, MMCMXCIII +2994, MMCMXCIV +2995, MMCMXCV +2996, MMCMXCVI +2997, MMCMXCVII +2998, MMCMXCVIII +2999, MMCMXCIX +3000, MMM +3001, MMMI +3002, MMMII +3003, MMMIII +3004, MMMIV +3005, MMMV +3006, MMMVI +3007, MMMVII +3008, MMMVIII +3009, MMMIX +3010, MMMX +3011, MMMXI +3012, MMMXII +3013, MMMXIII +3014, MMMXIV +3015, MMMXV +3016, MMMXVI +3017, MMMXVII +3018, MMMXVIII +3019, MMMXIX +3020, MMMXX +3021, MMMXXI +3022, MMMXXII +3023, MMMXXIII +3024, MMMXXIV +3025, MMMXXV +3026, MMMXXVI +3027, MMMXXVII +3028, MMMXXVIII +3029, MMMXXIX +3030, MMMXXX +3031, MMMXXXI +3032, MMMXXXII +3033, MMMXXXIII +3034, MMMXXXIV +3035, MMMXXXV +3036, MMMXXXVI +3037, MMMXXXVII +3038, MMMXXXVIII +3039, MMMXXXIX +3040, MMMXL +3041, MMMXLI +3042, MMMXLII +3043, MMMXLIII +3044, MMMXLIV +3045, MMMXLV +3046, MMMXLVI +3047, MMMXLVII +3048, MMMXLVIII +3049, MMMXLIX +3050, MMML +3051, MMMLI +3052, MMMLII +3053, MMMLIII +3054, MMMLIV +3055, MMMLV +3056, MMMLVI +3057, MMMLVII +3058, MMMLVIII +3059, MMMLIX +3060, MMMLX +3061, MMMLXI +3062, MMMLXII +3063, MMMLXIII +3064, MMMLXIV +3065, MMMLXV +3066, MMMLXVI +3067, MMMLXVII +3068, MMMLXVIII +3069, MMMLXIX +3070, MMMLXX +3071, MMMLXXI +3072, MMMLXXII +3073, MMMLXXIII +3074, MMMLXXIV +3075, MMMLXXV +3076, MMMLXXVI +3077, MMMLXXVII +3078, MMMLXXVIII +3079, MMMLXXIX +3080, MMMLXXX +3081, MMMLXXXI +3082, MMMLXXXII +3083, MMMLXXXIII +3084, MMMLXXXIV +3085, MMMLXXXV +3086, MMMLXXXVI +3087, MMMLXXXVII +3088, MMMLXXXVIII +3089, MMMLXXXIX +3090, MMMXC +3091, MMMXCI +3092, MMMXCII +3093, MMMXCIII +3094, MMMXCIV +3095, MMMXCV +3096, MMMXCVI +3097, MMMXCVII +3098, MMMXCVIII +3099, MMMXCIX +3100, MMMC +3101, MMMCI +3102, MMMCII +3103, MMMCIII +3104, MMMCIV +3105, MMMCV +3106, MMMCVI +3107, MMMCVII +3108, MMMCVIII +3109, MMMCIX +3110, MMMCX +3111, MMMCXI +3112, MMMCXII +3113, MMMCXIII +3114, MMMCXIV +3115, MMMCXV +3116, MMMCXVI +3117, MMMCXVII +3118, MMMCXVIII +3119, MMMCXIX +3120, MMMCXX +3121, MMMCXXI +3122, MMMCXXII +3123, MMMCXXIII +3124, MMMCXXIV +3125, MMMCXXV +3126, MMMCXXVI +3127, MMMCXXVII +3128, MMMCXXVIII +3129, MMMCXXIX +3130, MMMCXXX +3131, MMMCXXXI +3132, MMMCXXXII +3133, MMMCXXXIII +3134, MMMCXXXIV +3135, MMMCXXXV +3136, MMMCXXXVI +3137, MMMCXXXVII +3138, MMMCXXXVIII +3139, MMMCXXXIX +3140, MMMCXL +3141, MMMCXLI +3142, MMMCXLII +3143, MMMCXLIII +3144, MMMCXLIV +3145, MMMCXLV +3146, MMMCXLVI +3147, MMMCXLVII +3148, MMMCXLVIII +3149, MMMCXLIX +3150, MMMCL +3151, MMMCLI +3152, MMMCLII +3153, MMMCLIII +3154, MMMCLIV +3155, MMMCLV +3156, MMMCLVI +3157, MMMCLVII +3158, MMMCLVIII +3159, MMMCLIX +3160, MMMCLX +3161, MMMCLXI +3162, MMMCLXII +3163, MMMCLXIII +3164, MMMCLXIV +3165, MMMCLXV +3166, MMMCLXVI +3167, MMMCLXVII +3168, MMMCLXVIII +3169, MMMCLXIX +3170, MMMCLXX +3171, MMMCLXXI +3172, MMMCLXXII +3173, MMMCLXXIII +3174, MMMCLXXIV +3175, MMMCLXXV +3176, MMMCLXXVI +3177, MMMCLXXVII +3178, MMMCLXXVIII +3179, MMMCLXXIX +3180, MMMCLXXX +3181, MMMCLXXXI +3182, MMMCLXXXII +3183, MMMCLXXXIII +3184, MMMCLXXXIV +3185, MMMCLXXXV +3186, MMMCLXXXVI +3187, MMMCLXXXVII +3188, MMMCLXXXVIII +3189, MMMCLXXXIX +3190, MMMCXC +3191, MMMCXCI +3192, MMMCXCII +3193, MMMCXCIII +3194, MMMCXCIV +3195, MMMCXCV +3196, MMMCXCVI +3197, MMMCXCVII +3198, MMMCXCVIII +3199, MMMCXCIX +3200, MMMCC +3201, MMMCCI +3202, MMMCCII +3203, MMMCCIII +3204, MMMCCIV +3205, MMMCCV +3206, MMMCCVI +3207, MMMCCVII +3208, MMMCCVIII +3209, MMMCCIX +3210, MMMCCX +3211, MMMCCXI +3212, MMMCCXII +3213, MMMCCXIII +3214, MMMCCXIV +3215, MMMCCXV +3216, MMMCCXVI +3217, MMMCCXVII +3218, MMMCCXVIII +3219, MMMCCXIX +3220, MMMCCXX +3221, MMMCCXXI +3222, MMMCCXXII +3223, MMMCCXXIII +3224, MMMCCXXIV +3225, MMMCCXXV +3226, MMMCCXXVI +3227, MMMCCXXVII +3228, MMMCCXXVIII +3229, MMMCCXXIX +3230, MMMCCXXX +3231, MMMCCXXXI +3232, MMMCCXXXII +3233, MMMCCXXXIII +3234, MMMCCXXXIV +3235, MMMCCXXXV +3236, MMMCCXXXVI +3237, MMMCCXXXVII +3238, MMMCCXXXVIII +3239, MMMCCXXXIX +3240, MMMCCXL +3241, MMMCCXLI +3242, MMMCCXLII +3243, MMMCCXLIII +3244, MMMCCXLIV +3245, MMMCCXLV +3246, MMMCCXLVI +3247, MMMCCXLVII +3248, MMMCCXLVIII +3249, MMMCCXLIX +3250, MMMCCL +3251, MMMCCLI +3252, MMMCCLII +3253, MMMCCLIII +3254, MMMCCLIV +3255, MMMCCLV +3256, MMMCCLVI +3257, MMMCCLVII +3258, MMMCCLVIII +3259, MMMCCLIX +3260, MMMCCLX +3261, MMMCCLXI +3262, MMMCCLXII +3263, MMMCCLXIII +3264, MMMCCLXIV +3265, MMMCCLXV +3266, MMMCCLXVI +3267, MMMCCLXVII +3268, MMMCCLXVIII +3269, MMMCCLXIX +3270, MMMCCLXX +3271, MMMCCLXXI +3272, MMMCCLXXII +3273, MMMCCLXXIII +3274, MMMCCLXXIV +3275, MMMCCLXXV +3276, MMMCCLXXVI +3277, MMMCCLXXVII +3278, MMMCCLXXVIII +3279, MMMCCLXXIX +3280, MMMCCLXXX +3281, MMMCCLXXXI +3282, MMMCCLXXXII +3283, MMMCCLXXXIII +3284, MMMCCLXXXIV +3285, MMMCCLXXXV +3286, MMMCCLXXXVI +3287, MMMCCLXXXVII +3288, MMMCCLXXXVIII +3289, MMMCCLXXXIX +3290, MMMCCXC +3291, MMMCCXCI +3292, MMMCCXCII +3293, MMMCCXCIII +3294, MMMCCXCIV +3295, MMMCCXCV +3296, MMMCCXCVI +3297, MMMCCXCVII +3298, MMMCCXCVIII +3299, MMMCCXCIX +3300, MMMCCC +3301, MMMCCCI +3302, MMMCCCII +3303, MMMCCCIII +3304, MMMCCCIV +3305, MMMCCCV +3306, MMMCCCVI +3307, MMMCCCVII +3308, MMMCCCVIII +3309, MMMCCCIX +3310, MMMCCCX +3311, MMMCCCXI +3312, MMMCCCXII +3313, MMMCCCXIII +3314, MMMCCCXIV +3315, MMMCCCXV +3316, MMMCCCXVI +3317, MMMCCCXVII +3318, MMMCCCXVIII +3319, MMMCCCXIX +3320, MMMCCCXX +3321, MMMCCCXXI +3322, MMMCCCXXII +3323, MMMCCCXXIII +3324, MMMCCCXXIV +3325, MMMCCCXXV +3326, MMMCCCXXVI +3327, MMMCCCXXVII +3328, MMMCCCXXVIII +3329, MMMCCCXXIX +3330, MMMCCCXXX +3331, MMMCCCXXXI +3332, MMMCCCXXXII +3333, MMMCCCXXXIII +3334, MMMCCCXXXIV +3335, MMMCCCXXXV +3336, MMMCCCXXXVI +3337, MMMCCCXXXVII +3338, MMMCCCXXXVIII +3339, MMMCCCXXXIX +3340, MMMCCCXL +3341, MMMCCCXLI +3342, MMMCCCXLII +3343, MMMCCCXLIII +3344, MMMCCCXLIV +3345, MMMCCCXLV +3346, MMMCCCXLVI +3347, MMMCCCXLVII +3348, MMMCCCXLVIII +3349, MMMCCCXLIX +3350, MMMCCCL +3351, MMMCCCLI +3352, MMMCCCLII +3353, MMMCCCLIII +3354, MMMCCCLIV +3355, MMMCCCLV +3356, MMMCCCLVI +3357, MMMCCCLVII +3358, MMMCCCLVIII +3359, MMMCCCLIX +3360, MMMCCCLX +3361, MMMCCCLXI +3362, MMMCCCLXII +3363, MMMCCCLXIII +3364, MMMCCCLXIV +3365, MMMCCCLXV +3366, MMMCCCLXVI +3367, MMMCCCLXVII +3368, MMMCCCLXVIII +3369, MMMCCCLXIX +3370, MMMCCCLXX +3371, MMMCCCLXXI +3372, MMMCCCLXXII +3373, MMMCCCLXXIII +3374, MMMCCCLXXIV +3375, MMMCCCLXXV +3376, MMMCCCLXXVI +3377, MMMCCCLXXVII +3378, MMMCCCLXXVIII +3379, MMMCCCLXXIX +3380, MMMCCCLXXX +3381, MMMCCCLXXXI +3382, MMMCCCLXXXII +3383, MMMCCCLXXXIII +3384, MMMCCCLXXXIV +3385, MMMCCCLXXXV +3386, MMMCCCLXXXVI +3387, MMMCCCLXXXVII +3388, MMMCCCLXXXVIII +3389, MMMCCCLXXXIX +3390, MMMCCCXC +3391, MMMCCCXCI +3392, MMMCCCXCII +3393, MMMCCCXCIII +3394, MMMCCCXCIV +3395, MMMCCCXCV +3396, MMMCCCXCVI +3397, MMMCCCXCVII +3398, MMMCCCXCVIII +3399, MMMCCCXCIX +3400, MMMCD +3401, MMMCDI +3402, MMMCDII +3403, MMMCDIII +3404, MMMCDIV +3405, MMMCDV +3406, MMMCDVI +3407, MMMCDVII +3408, MMMCDVIII +3409, MMMCDIX +3410, MMMCDX +3411, MMMCDXI +3412, MMMCDXII +3413, MMMCDXIII +3414, MMMCDXIV +3415, MMMCDXV +3416, MMMCDXVI +3417, MMMCDXVII +3418, MMMCDXVIII +3419, MMMCDXIX +3420, MMMCDXX +3421, MMMCDXXI +3422, MMMCDXXII +3423, MMMCDXXIII +3424, MMMCDXXIV +3425, MMMCDXXV +3426, MMMCDXXVI +3427, MMMCDXXVII +3428, MMMCDXXVIII +3429, MMMCDXXIX +3430, MMMCDXXX +3431, MMMCDXXXI +3432, MMMCDXXXII +3433, MMMCDXXXIII +3434, MMMCDXXXIV +3435, MMMCDXXXV +3436, MMMCDXXXVI +3437, MMMCDXXXVII +3438, MMMCDXXXVIII +3439, MMMCDXXXIX +3440, MMMCDXL +3441, MMMCDXLI +3442, MMMCDXLII +3443, MMMCDXLIII +3444, MMMCDXLIV +3445, MMMCDXLV +3446, MMMCDXLVI +3447, MMMCDXLVII +3448, MMMCDXLVIII +3449, MMMCDXLIX +3450, MMMCDL +3451, MMMCDLI +3452, MMMCDLII +3453, MMMCDLIII +3454, MMMCDLIV +3455, MMMCDLV +3456, MMMCDLVI +3457, MMMCDLVII +3458, MMMCDLVIII +3459, MMMCDLIX +3460, MMMCDLX +3461, MMMCDLXI +3462, MMMCDLXII +3463, MMMCDLXIII +3464, MMMCDLXIV +3465, MMMCDLXV +3466, MMMCDLXVI +3467, MMMCDLXVII +3468, MMMCDLXVIII +3469, MMMCDLXIX +3470, MMMCDLXX +3471, MMMCDLXXI +3472, MMMCDLXXII +3473, MMMCDLXXIII +3474, MMMCDLXXIV +3475, MMMCDLXXV +3476, MMMCDLXXVI +3477, MMMCDLXXVII +3478, MMMCDLXXVIII +3479, MMMCDLXXIX +3480, MMMCDLXXX +3481, MMMCDLXXXI +3482, MMMCDLXXXII +3483, MMMCDLXXXIII +3484, MMMCDLXXXIV +3485, MMMCDLXXXV +3486, MMMCDLXXXVI +3487, MMMCDLXXXVII +3488, MMMCDLXXXVIII +3489, MMMCDLXXXIX +3490, MMMCDXC +3491, MMMCDXCI +3492, MMMCDXCII +3493, MMMCDXCIII +3494, MMMCDXCIV +3495, MMMCDXCV +3496, MMMCDXCVI +3497, MMMCDXCVII +3498, MMMCDXCVIII +3499, MMMCDXCIX +3500, MMMD +3501, MMMDI +3502, MMMDII +3503, MMMDIII +3504, MMMDIV +3505, MMMDV +3506, MMMDVI +3507, MMMDVII +3508, MMMDVIII +3509, MMMDIX +3510, MMMDX +3511, MMMDXI +3512, MMMDXII +3513, MMMDXIII +3514, MMMDXIV +3515, MMMDXV +3516, MMMDXVI +3517, MMMDXVII +3518, MMMDXVIII +3519, MMMDXIX +3520, MMMDXX +3521, MMMDXXI +3522, MMMDXXII +3523, MMMDXXIII +3524, MMMDXXIV +3525, MMMDXXV +3526, MMMDXXVI +3527, MMMDXXVII +3528, MMMDXXVIII +3529, MMMDXXIX +3530, MMMDXXX +3531, MMMDXXXI +3532, MMMDXXXII +3533, MMMDXXXIII +3534, MMMDXXXIV +3535, MMMDXXXV +3536, MMMDXXXVI +3537, MMMDXXXVII +3538, MMMDXXXVIII +3539, MMMDXXXIX +3540, MMMDXL +3541, MMMDXLI +3542, MMMDXLII +3543, MMMDXLIII +3544, MMMDXLIV +3545, MMMDXLV +3546, MMMDXLVI +3547, MMMDXLVII +3548, MMMDXLVIII +3549, MMMDXLIX +3550, MMMDL +3551, MMMDLI +3552, MMMDLII +3553, MMMDLIII +3554, MMMDLIV +3555, MMMDLV +3556, MMMDLVI +3557, MMMDLVII +3558, MMMDLVIII +3559, MMMDLIX +3560, MMMDLX +3561, MMMDLXI +3562, MMMDLXII +3563, MMMDLXIII +3564, MMMDLXIV +3565, MMMDLXV +3566, MMMDLXVI +3567, MMMDLXVII +3568, MMMDLXVIII +3569, MMMDLXIX +3570, MMMDLXX +3571, MMMDLXXI +3572, MMMDLXXII +3573, MMMDLXXIII +3574, MMMDLXXIV +3575, MMMDLXXV +3576, MMMDLXXVI +3577, MMMDLXXVII +3578, MMMDLXXVIII +3579, MMMDLXXIX +3580, MMMDLXXX +3581, MMMDLXXXI +3582, MMMDLXXXII +3583, MMMDLXXXIII +3584, MMMDLXXXIV +3585, MMMDLXXXV +3586, MMMDLXXXVI +3587, MMMDLXXXVII +3588, MMMDLXXXVIII +3589, MMMDLXXXIX +3590, MMMDXC +3591, MMMDXCI +3592, MMMDXCII +3593, MMMDXCIII +3594, MMMDXCIV +3595, MMMDXCV +3596, MMMDXCVI +3597, MMMDXCVII +3598, MMMDXCVIII +3599, MMMDXCIX +3600, MMMDC +3601, MMMDCI +3602, MMMDCII +3603, MMMDCIII +3604, MMMDCIV +3605, MMMDCV +3606, MMMDCVI +3607, MMMDCVII +3608, MMMDCVIII +3609, MMMDCIX +3610, MMMDCX +3611, MMMDCXI +3612, MMMDCXII +3613, MMMDCXIII +3614, MMMDCXIV +3615, MMMDCXV +3616, MMMDCXVI +3617, MMMDCXVII +3618, MMMDCXVIII +3619, MMMDCXIX +3620, MMMDCXX +3621, MMMDCXXI +3622, MMMDCXXII +3623, MMMDCXXIII +3624, MMMDCXXIV +3625, MMMDCXXV +3626, MMMDCXXVI +3627, MMMDCXXVII +3628, MMMDCXXVIII +3629, MMMDCXXIX +3630, MMMDCXXX +3631, MMMDCXXXI +3632, MMMDCXXXII +3633, MMMDCXXXIII +3634, MMMDCXXXIV +3635, MMMDCXXXV +3636, MMMDCXXXVI +3637, MMMDCXXXVII +3638, MMMDCXXXVIII +3639, MMMDCXXXIX +3640, MMMDCXL +3641, MMMDCXLI +3642, MMMDCXLII +3643, MMMDCXLIII +3644, MMMDCXLIV +3645, MMMDCXLV +3646, MMMDCXLVI +3647, MMMDCXLVII +3648, MMMDCXLVIII +3649, MMMDCXLIX +3650, MMMDCL +3651, MMMDCLI +3652, MMMDCLII +3653, MMMDCLIII +3654, MMMDCLIV +3655, MMMDCLV +3656, MMMDCLVI +3657, MMMDCLVII +3658, MMMDCLVIII +3659, MMMDCLIX +3660, MMMDCLX +3661, MMMDCLXI +3662, MMMDCLXII +3663, MMMDCLXIII +3664, MMMDCLXIV +3665, MMMDCLXV +3666, MMMDCLXVI +3667, MMMDCLXVII +3668, MMMDCLXVIII +3669, MMMDCLXIX +3670, MMMDCLXX +3671, MMMDCLXXI +3672, MMMDCLXXII +3673, MMMDCLXXIII +3674, MMMDCLXXIV +3675, MMMDCLXXV +3676, MMMDCLXXVI +3677, MMMDCLXXVII +3678, MMMDCLXXVIII +3679, MMMDCLXXIX +3680, MMMDCLXXX +3681, MMMDCLXXXI +3682, MMMDCLXXXII +3683, MMMDCLXXXIII +3684, MMMDCLXXXIV +3685, MMMDCLXXXV +3686, MMMDCLXXXVI +3687, MMMDCLXXXVII +3688, MMMDCLXXXVIII +3689, MMMDCLXXXIX +3690, MMMDCXC +3691, MMMDCXCI +3692, MMMDCXCII +3693, MMMDCXCIII +3694, MMMDCXCIV +3695, MMMDCXCV +3696, MMMDCXCVI +3697, MMMDCXCVII +3698, MMMDCXCVIII +3699, MMMDCXCIX +3700, MMMDCC +3701, MMMDCCI +3702, MMMDCCII +3703, MMMDCCIII +3704, MMMDCCIV +3705, MMMDCCV +3706, MMMDCCVI +3707, MMMDCCVII +3708, MMMDCCVIII +3709, MMMDCCIX +3710, MMMDCCX +3711, MMMDCCXI +3712, MMMDCCXII +3713, MMMDCCXIII +3714, MMMDCCXIV +3715, MMMDCCXV +3716, MMMDCCXVI +3717, MMMDCCXVII +3718, MMMDCCXVIII +3719, MMMDCCXIX +3720, MMMDCCXX +3721, MMMDCCXXI +3722, MMMDCCXXII +3723, MMMDCCXXIII +3724, MMMDCCXXIV +3725, MMMDCCXXV +3726, MMMDCCXXVI +3727, MMMDCCXXVII +3728, MMMDCCXXVIII +3729, MMMDCCXXIX +3730, MMMDCCXXX +3731, MMMDCCXXXI +3732, MMMDCCXXXII +3733, MMMDCCXXXIII +3734, MMMDCCXXXIV +3735, MMMDCCXXXV +3736, MMMDCCXXXVI +3737, MMMDCCXXXVII +3738, MMMDCCXXXVIII +3739, MMMDCCXXXIX +3740, MMMDCCXL +3741, MMMDCCXLI +3742, MMMDCCXLII +3743, MMMDCCXLIII +3744, MMMDCCXLIV +3745, MMMDCCXLV +3746, MMMDCCXLVI +3747, MMMDCCXLVII +3748, MMMDCCXLVIII +3749, MMMDCCXLIX +3750, MMMDCCL +3751, MMMDCCLI +3752, MMMDCCLII +3753, MMMDCCLIII +3754, MMMDCCLIV +3755, MMMDCCLV +3756, MMMDCCLVI +3757, MMMDCCLVII +3758, MMMDCCLVIII +3759, MMMDCCLIX +3760, MMMDCCLX +3761, MMMDCCLXI +3762, MMMDCCLXII +3763, MMMDCCLXIII +3764, MMMDCCLXIV +3765, MMMDCCLXV +3766, MMMDCCLXVI +3767, MMMDCCLXVII +3768, MMMDCCLXVIII +3769, MMMDCCLXIX +3770, MMMDCCLXX +3771, MMMDCCLXXI +3772, MMMDCCLXXII +3773, MMMDCCLXXIII +3774, MMMDCCLXXIV +3775, MMMDCCLXXV +3776, MMMDCCLXXVI +3777, MMMDCCLXXVII +3778, MMMDCCLXXVIII +3779, MMMDCCLXXIX +3780, MMMDCCLXXX +3781, MMMDCCLXXXI +3782, MMMDCCLXXXII +3783, MMMDCCLXXXIII +3784, MMMDCCLXXXIV +3785, MMMDCCLXXXV +3786, MMMDCCLXXXVI +3787, MMMDCCLXXXVII +3788, MMMDCCLXXXVIII +3789, MMMDCCLXXXIX +3790, MMMDCCXC +3791, MMMDCCXCI +3792, MMMDCCXCII +3793, MMMDCCXCIII +3794, MMMDCCXCIV +3795, MMMDCCXCV +3796, MMMDCCXCVI +3797, MMMDCCXCVII +3798, MMMDCCXCVIII +3799, MMMDCCXCIX +3800, MMMDCCC +3801, MMMDCCCI +3802, MMMDCCCII +3803, MMMDCCCIII +3804, MMMDCCCIV +3805, MMMDCCCV +3806, MMMDCCCVI +3807, MMMDCCCVII +3808, MMMDCCCVIII +3809, MMMDCCCIX +3810, MMMDCCCX +3811, MMMDCCCXI +3812, MMMDCCCXII +3813, MMMDCCCXIII +3814, MMMDCCCXIV +3815, MMMDCCCXV +3816, MMMDCCCXVI +3817, MMMDCCCXVII +3818, MMMDCCCXVIII +3819, MMMDCCCXIX +3820, MMMDCCCXX +3821, MMMDCCCXXI +3822, MMMDCCCXXII +3823, MMMDCCCXXIII +3824, MMMDCCCXXIV +3825, MMMDCCCXXV +3826, MMMDCCCXXVI +3827, MMMDCCCXXVII +3828, MMMDCCCXXVIII +3829, MMMDCCCXXIX +3830, MMMDCCCXXX +3831, MMMDCCCXXXI +3832, MMMDCCCXXXII +3833, MMMDCCCXXXIII +3834, MMMDCCCXXXIV +3835, MMMDCCCXXXV +3836, MMMDCCCXXXVI +3837, MMMDCCCXXXVII +3838, MMMDCCCXXXVIII +3839, MMMDCCCXXXIX +3840, MMMDCCCXL +3841, MMMDCCCXLI +3842, MMMDCCCXLII +3843, MMMDCCCXLIII +3844, MMMDCCCXLIV +3845, MMMDCCCXLV +3846, MMMDCCCXLVI +3847, MMMDCCCXLVII +3848, MMMDCCCXLVIII +3849, MMMDCCCXLIX +3850, MMMDCCCL +3851, MMMDCCCLI +3852, MMMDCCCLII +3853, MMMDCCCLIII +3854, MMMDCCCLIV +3855, MMMDCCCLV +3856, MMMDCCCLVI +3857, MMMDCCCLVII +3858, MMMDCCCLVIII +3859, MMMDCCCLIX +3860, MMMDCCCLX +3861, MMMDCCCLXI +3862, MMMDCCCLXII +3863, MMMDCCCLXIII +3864, MMMDCCCLXIV +3865, MMMDCCCLXV +3866, MMMDCCCLXVI +3867, MMMDCCCLXVII +3868, MMMDCCCLXVIII +3869, MMMDCCCLXIX +3870, MMMDCCCLXX +3871, MMMDCCCLXXI +3872, MMMDCCCLXXII +3873, MMMDCCCLXXIII +3874, MMMDCCCLXXIV +3875, MMMDCCCLXXV +3876, MMMDCCCLXXVI +3877, MMMDCCCLXXVII +3878, MMMDCCCLXXVIII +3879, MMMDCCCLXXIX +3880, MMMDCCCLXXX +3881, MMMDCCCLXXXI +3882, MMMDCCCLXXXII +3883, MMMDCCCLXXXIII +3884, MMMDCCCLXXXIV +3885, MMMDCCCLXXXV +3886, MMMDCCCLXXXVI +3887, MMMDCCCLXXXVII +3888, MMMDCCCLXXXVIII +3889, MMMDCCCLXXXIX +3890, MMMDCCCXC +3891, MMMDCCCXCI +3892, MMMDCCCXCII +3893, MMMDCCCXCIII +3894, MMMDCCCXCIV +3895, MMMDCCCXCV +3896, MMMDCCCXCVI +3897, MMMDCCCXCVII +3898, MMMDCCCXCVIII +3899, MMMDCCCXCIX +3900, MMMCM +3901, MMMCMI +3902, MMMCMII +3903, MMMCMIII +3904, MMMCMIV +3905, MMMCMV +3906, MMMCMVI +3907, MMMCMVII +3908, MMMCMVIII +3909, MMMCMIX +3910, MMMCMX +3911, MMMCMXI +3912, MMMCMXII +3913, MMMCMXIII +3914, MMMCMXIV +3915, MMMCMXV +3916, MMMCMXVI +3917, MMMCMXVII +3918, MMMCMXVIII +3919, MMMCMXIX +3920, MMMCMXX +3921, MMMCMXXI +3922, MMMCMXXII +3923, MMMCMXXIII +3924, MMMCMXXIV +3925, MMMCMXXV +3926, MMMCMXXVI +3927, MMMCMXXVII +3928, MMMCMXXVIII +3929, MMMCMXXIX +3930, MMMCMXXX +3931, MMMCMXXXI +3932, MMMCMXXXII +3933, MMMCMXXXIII +3934, MMMCMXXXIV +3935, MMMCMXXXV +3936, MMMCMXXXVI +3937, MMMCMXXXVII +3938, MMMCMXXXVIII +3939, MMMCMXXXIX +3940, MMMCMXL +3941, MMMCMXLI +3942, MMMCMXLII +3943, MMMCMXLIII +3944, MMMCMXLIV +3945, MMMCMXLV +3946, MMMCMXLVI +3947, MMMCMXLVII +3948, MMMCMXLVIII +3949, MMMCMXLIX +3950, MMMCML +3951, MMMCMLI +3952, MMMCMLII +3953, MMMCMLIII +3954, MMMCMLIV +3955, MMMCMLV +3956, MMMCMLVI +3957, MMMCMLVII +3958, MMMCMLVIII +3959, MMMCMLIX +3960, MMMCMLX +3961, MMMCMLXI +3962, MMMCMLXII +3963, MMMCMLXIII +3964, MMMCMLXIV +3965, MMMCMLXV +3966, MMMCMLXVI +3967, MMMCMLXVII +3968, MMMCMLXVIII +3969, MMMCMLXIX +3970, MMMCMLXX +3971, MMMCMLXXI +3972, MMMCMLXXII +3973, MMMCMLXXIII +3974, MMMCMLXXIV +3975, MMMCMLXXV +3976, MMMCMLXXVI +3977, MMMCMLXXVII +3978, MMMCMLXXVIII +3979, MMMCMLXXIX +3980, MMMCMLXXX +3981, MMMCMLXXXI +3982, MMMCMLXXXII +3983, MMMCMLXXXIII +3984, MMMCMLXXXIV +3985, MMMCMLXXXV +3986, MMMCMLXXXVI +3987, MMMCMLXXXVII +3988, MMMCMLXXXVIII +3989, MMMCMLXXXIX +3990, MMMCMXC +3991, MMMCMXCI +3992, MMMCMXCII +3993, MMMCMXCIII +3994, MMMCMXCIV +3995, MMMCMXCV +3996, MMMCMXCVI +3997, MMMCMXCVII +3998, MMMCMXCVIII +3999, MMMCMXCIX diff --git a/src/test/resources/cases/roman/numerals.txt b/src/test/resources/cases/roman/numerals.txt deleted file mode 100644 index 54d4d3f5..00000000 --- a/src/test/resources/cases/roman/numerals.txt +++ /dev/null @@ -1,3999 +0,0 @@ -1 I -2 II -3 III -4 IV -5 V -6 VI -7 VII -8 VIII -9 IX -10 X -11 XI -12 XII -13 XIII -14 XIV -15 XV -16 XVI -17 XVII -18 XVIII -19 XIX -20 XX -21 XXI -22 XXII -23 XXIII -24 XXIV -25 XXV -26 XXVI -27 XXVII -28 XXVIII -29 XXIX -30 XXX -31 XXXI -32 XXXII -33 XXXIII -34 XXXIV -35 XXXV -36 XXXVI -37 XXXVII -38 XXXVIII -39 XXXIX -40 XL -41 XLI -42 XLII -43 XLIII -44 XLIV -45 XLV -46 XLVI -47 XLVII -48 XLVIII -49 XLIX -50 L -51 LI -52 LII -53 LIII -54 LIV -55 LV -56 LVI -57 LVII -58 LVIII -59 LIX -60 LX -61 LXI -62 LXII -63 LXIII -64 LXIV -65 LXV -66 LXVI -67 LXVII -68 LXVIII -69 LXIX -70 LXX -71 LXXI -72 LXXII -73 LXXIII -74 LXXIV -75 LXXV -76 LXXVI -77 LXXVII -78 LXXVIII -79 LXXIX -80 LXXX -81 LXXXI -82 LXXXII -83 LXXXIII -84 LXXXIV -85 LXXXV -86 LXXXVI -87 LXXXVII -88 LXXXVIII -89 LXXXIX -90 XC -91 XCI -92 XCII -93 XCIII -94 XCIV -95 XCV -96 XCVI -97 XCVII -98 XCVIII -99 XCIX -100 C -101 CI -102 CII -103 CIII -104 CIV -105 CV -106 CVI -107 CVII -108 CVIII -109 CIX -110 CX -111 CXI -112 CXII -113 CXIII -114 CXIV -115 CXV -116 CXVI -117 CXVII -118 CXVIII -119 CXIX -120 CXX -121 CXXI -122 CXXII -123 CXXIII -124 CXXIV -125 CXXV -126 CXXVI -127 CXXVII -128 CXXVIII -129 CXXIX -130 CXXX -131 CXXXI -132 CXXXII -133 CXXXIII -134 CXXXIV -135 CXXXV -136 CXXXVI -137 CXXXVII -138 CXXXVIII -139 CXXXIX -140 CXL -141 CXLI -142 CXLII -143 CXLIII -144 CXLIV -145 CXLV -146 CXLVI -147 CXLVII -148 CXLVIII -149 CXLIX -150 CL -151 CLI -152 CLII -153 CLIII -154 CLIV -155 CLV -156 CLVI -157 CLVII -158 CLVIII -159 CLIX -160 CLX -161 CLXI -162 CLXII -163 CLXIII -164 CLXIV -165 CLXV -166 CLXVI -167 CLXVII -168 CLXVIII -169 CLXIX -170 CLXX -171 CLXXI -172 CLXXII -173 CLXXIII -174 CLXXIV -175 CLXXV -176 CLXXVI -177 CLXXVII -178 CLXXVIII -179 CLXXIX -180 CLXXX -181 CLXXXI -182 CLXXXII -183 CLXXXIII -184 CLXXXIV -185 CLXXXV -186 CLXXXVI -187 CLXXXVII -188 CLXXXVIII -189 CLXXXIX -190 CXC -191 CXCI -192 CXCII -193 CXCIII -194 CXCIV -195 CXCV -196 CXCVI -197 CXCVII -198 CXCVIII -199 CXCIX -200 CC -201 CCI -202 CCII -203 CCIII -204 CCIV -205 CCV -206 CCVI -207 CCVII -208 CCVIII -209 CCIX -210 CCX -211 CCXI -212 CCXII -213 CCXIII -214 CCXIV -215 CCXV -216 CCXVI -217 CCXVII -218 CCXVIII -219 CCXIX -220 CCXX -221 CCXXI -222 CCXXII -223 CCXXIII -224 CCXXIV -225 CCXXV -226 CCXXVI -227 CCXXVII -228 CCXXVIII -229 CCXXIX -230 CCXXX -231 CCXXXI -232 CCXXXII -233 CCXXXIII -234 CCXXXIV -235 CCXXXV -236 CCXXXVI -237 CCXXXVII -238 CCXXXVIII -239 CCXXXIX -240 CCXL -241 CCXLI -242 CCXLII -243 CCXLIII -244 CCXLIV -245 CCXLV -246 CCXLVI -247 CCXLVII -248 CCXLVIII -249 CCXLIX -250 CCL -251 CCLI -252 CCLII -253 CCLIII -254 CCLIV -255 CCLV -256 CCLVI -257 CCLVII -258 CCLVIII -259 CCLIX -260 CCLX -261 CCLXI -262 CCLXII -263 CCLXIII -264 CCLXIV -265 CCLXV -266 CCLXVI -267 CCLXVII -268 CCLXVIII -269 CCLXIX -270 CCLXX -271 CCLXXI -272 CCLXXII -273 CCLXXIII -274 CCLXXIV -275 CCLXXV -276 CCLXXVI -277 CCLXXVII -278 CCLXXVIII -279 CCLXXIX -280 CCLXXX -281 CCLXXXI -282 CCLXXXII -283 CCLXXXIII -284 CCLXXXIV -285 CCLXXXV -286 CCLXXXVI -287 CCLXXXVII -288 CCLXXXVIII -289 CCLXXXIX -290 CCXC -291 CCXCI -292 CCXCII -293 CCXCIII -294 CCXCIV -295 CCXCV -296 CCXCVI -297 CCXCVII -298 CCXCVIII -299 CCXCIX -300 CCC -301 CCCI -302 CCCII -303 CCCIII -304 CCCIV -305 CCCV -306 CCCVI -307 CCCVII -308 CCCVIII -309 CCCIX -310 CCCX -311 CCCXI -312 CCCXII -313 CCCXIII -314 CCCXIV -315 CCCXV -316 CCCXVI -317 CCCXVII -318 CCCXVIII -319 CCCXIX -320 CCCXX -321 CCCXXI -322 CCCXXII -323 CCCXXIII -324 CCCXXIV -325 CCCXXV -326 CCCXXVI -327 CCCXXVII -328 CCCXXVIII -329 CCCXXIX -330 CCCXXX -331 CCCXXXI -332 CCCXXXII -333 CCCXXXIII -334 CCCXXXIV -335 CCCXXXV -336 CCCXXXVI -337 CCCXXXVII -338 CCCXXXVIII -339 CCCXXXIX -340 CCCXL -341 CCCXLI -342 CCCXLII -343 CCCXLIII -344 CCCXLIV -345 CCCXLV -346 CCCXLVI -347 CCCXLVII -348 CCCXLVIII -349 CCCXLIX -350 CCCL -351 CCCLI -352 CCCLII -353 CCCLIII -354 CCCLIV -355 CCCLV -356 CCCLVI -357 CCCLVII -358 CCCLVIII -359 CCCLIX -360 CCCLX -361 CCCLXI -362 CCCLXII -363 CCCLXIII -364 CCCLXIV -365 CCCLXV -366 CCCLXVI -367 CCCLXVII -368 CCCLXVIII -369 CCCLXIX -370 CCCLXX -371 CCCLXXI -372 CCCLXXII -373 CCCLXXIII -374 CCCLXXIV -375 CCCLXXV -376 CCCLXXVI -377 CCCLXXVII -378 CCCLXXVIII -379 CCCLXXIX -380 CCCLXXX -381 CCCLXXXI -382 CCCLXXXII -383 CCCLXXXIII -384 CCCLXXXIV -385 CCCLXXXV -386 CCCLXXXVI -387 CCCLXXXVII -388 CCCLXXXVIII -389 CCCLXXXIX -390 CCCXC -391 CCCXCI -392 CCCXCII -393 CCCXCIII -394 CCCXCIV -395 CCCXCV -396 CCCXCVI -397 CCCXCVII -398 CCCXCVIII -399 CCCXCIX -400 CD -401 CDI -402 CDII -403 CDIII -404 CDIV -405 CDV -406 CDVI -407 CDVII -408 CDVIII -409 CDIX -410 CDX -411 CDXI -412 CDXII -413 CDXIII -414 CDXIV -415 CDXV -416 CDXVI -417 CDXVII -418 CDXVIII -419 CDXIX -420 CDXX -421 CDXXI -422 CDXXII -423 CDXXIII -424 CDXXIV -425 CDXXV -426 CDXXVI -427 CDXXVII -428 CDXXVIII -429 CDXXIX -430 CDXXX -431 CDXXXI -432 CDXXXII -433 CDXXXIII -434 CDXXXIV -435 CDXXXV -436 CDXXXVI -437 CDXXXVII -438 CDXXXVIII -439 CDXXXIX -440 CDXL -441 CDXLI -442 CDXLII -443 CDXLIII -444 CDXLIV -445 CDXLV -446 CDXLVI -447 CDXLVII -448 CDXLVIII -449 CDXLIX -450 CDL -451 CDLI -452 CDLII -453 CDLIII -454 CDLIV -455 CDLV -456 CDLVI -457 CDLVII -458 CDLVIII -459 CDLIX -460 CDLX -461 CDLXI -462 CDLXII -463 CDLXIII -464 CDLXIV -465 CDLXV -466 CDLXVI -467 CDLXVII -468 CDLXVIII -469 CDLXIX -470 CDLXX -471 CDLXXI -472 CDLXXII -473 CDLXXIII -474 CDLXXIV -475 CDLXXV -476 CDLXXVI -477 CDLXXVII -478 CDLXXVIII -479 CDLXXIX -480 CDLXXX -481 CDLXXXI -482 CDLXXXII -483 CDLXXXIII -484 CDLXXXIV -485 CDLXXXV -486 CDLXXXVI -487 CDLXXXVII -488 CDLXXXVIII -489 CDLXXXIX -490 CDXC -491 CDXCI -492 CDXCII -493 CDXCIII -494 CDXCIV -495 CDXCV -496 CDXCVI -497 CDXCVII -498 CDXCVIII -499 CDXCIX -500 D -501 DI -502 DII -503 DIII -504 DIV -505 DV -506 DVI -507 DVII -508 DVIII -509 DIX -510 DX -511 DXI -512 DXII -513 DXIII -514 DXIV -515 DXV -516 DXVI -517 DXVII -518 DXVIII -519 DXIX -520 DXX -521 DXXI -522 DXXII -523 DXXIII -524 DXXIV -525 DXXV -526 DXXVI -527 DXXVII -528 DXXVIII -529 DXXIX -530 DXXX -531 DXXXI -532 DXXXII -533 DXXXIII -534 DXXXIV -535 DXXXV -536 DXXXVI -537 DXXXVII -538 DXXXVIII -539 DXXXIX -540 DXL -541 DXLI -542 DXLII -543 DXLIII -544 DXLIV -545 DXLV -546 DXLVI -547 DXLVII -548 DXLVIII -549 DXLIX -550 DL -551 DLI -552 DLII -553 DLIII -554 DLIV -555 DLV -556 DLVI -557 DLVII -558 DLVIII -559 DLIX -560 DLX -561 DLXI -562 DLXII -563 DLXIII -564 DLXIV -565 DLXV -566 DLXVI -567 DLXVII -568 DLXVIII -569 DLXIX -570 DLXX -571 DLXXI -572 DLXXII -573 DLXXIII -574 DLXXIV -575 DLXXV -576 DLXXVI -577 DLXXVII -578 DLXXVIII -579 DLXXIX -580 DLXXX -581 DLXXXI -582 DLXXXII -583 DLXXXIII -584 DLXXXIV -585 DLXXXV -586 DLXXXVI -587 DLXXXVII -588 DLXXXVIII -589 DLXXXIX -590 DXC -591 DXCI -592 DXCII -593 DXCIII -594 DXCIV -595 DXCV -596 DXCVI -597 DXCVII -598 DXCVIII -599 DXCIX -600 DC -601 DCI -602 DCII -603 DCIII -604 DCIV -605 DCV -606 DCVI -607 DCVII -608 DCVIII -609 DCIX -610 DCX -611 DCXI -612 DCXII -613 DCXIII -614 DCXIV -615 DCXV -616 DCXVI -617 DCXVII -618 DCXVIII -619 DCXIX -620 DCXX -621 DCXXI -622 DCXXII -623 DCXXIII -624 DCXXIV -625 DCXXV -626 DCXXVI -627 DCXXVII -628 DCXXVIII -629 DCXXIX -630 DCXXX -631 DCXXXI -632 DCXXXII -633 DCXXXIII -634 DCXXXIV -635 DCXXXV -636 DCXXXVI -637 DCXXXVII -638 DCXXXVIII -639 DCXXXIX -640 DCXL -641 DCXLI -642 DCXLII -643 DCXLIII -644 DCXLIV -645 DCXLV -646 DCXLVI -647 DCXLVII -648 DCXLVIII -649 DCXLIX -650 DCL -651 DCLI -652 DCLII -653 DCLIII -654 DCLIV -655 DCLV -656 DCLVI -657 DCLVII -658 DCLVIII -659 DCLIX -660 DCLX -661 DCLXI -662 DCLXII -663 DCLXIII -664 DCLXIV -665 DCLXV -666 DCLXVI -667 DCLXVII -668 DCLXVIII -669 DCLXIX -670 DCLXX -671 DCLXXI -672 DCLXXII -673 DCLXXIII -674 DCLXXIV -675 DCLXXV -676 DCLXXVI -677 DCLXXVII -678 DCLXXVIII -679 DCLXXIX -680 DCLXXX -681 DCLXXXI -682 DCLXXXII -683 DCLXXXIII -684 DCLXXXIV -685 DCLXXXV -686 DCLXXXVI -687 DCLXXXVII -688 DCLXXXVIII -689 DCLXXXIX -690 DCXC -691 DCXCI -692 DCXCII -693 DCXCIII -694 DCXCIV -695 DCXCV -696 DCXCVI -697 DCXCVII -698 DCXCVIII -699 DCXCIX -700 DCC -701 DCCI -702 DCCII -703 DCCIII -704 DCCIV -705 DCCV -706 DCCVI -707 DCCVII -708 DCCVIII -709 DCCIX -710 DCCX -711 DCCXI -712 DCCXII -713 DCCXIII -714 DCCXIV -715 DCCXV -716 DCCXVI -717 DCCXVII -718 DCCXVIII -719 DCCXIX -720 DCCXX -721 DCCXXI -722 DCCXXII -723 DCCXXIII -724 DCCXXIV -725 DCCXXV -726 DCCXXVI -727 DCCXXVII -728 DCCXXVIII -729 DCCXXIX -730 DCCXXX -731 DCCXXXI -732 DCCXXXII -733 DCCXXXIII -734 DCCXXXIV -735 DCCXXXV -736 DCCXXXVI -737 DCCXXXVII -738 DCCXXXVIII -739 DCCXXXIX -740 DCCXL -741 DCCXLI -742 DCCXLII -743 DCCXLIII -744 DCCXLIV -745 DCCXLV -746 DCCXLVI -747 DCCXLVII -748 DCCXLVIII -749 DCCXLIX -750 DCCL -751 DCCLI -752 DCCLII -753 DCCLIII -754 DCCLIV -755 DCCLV -756 DCCLVI -757 DCCLVII -758 DCCLVIII -759 DCCLIX -760 DCCLX -761 DCCLXI -762 DCCLXII -763 DCCLXIII -764 DCCLXIV -765 DCCLXV -766 DCCLXVI -767 DCCLXVII -768 DCCLXVIII -769 DCCLXIX -770 DCCLXX -771 DCCLXXI -772 DCCLXXII -773 DCCLXXIII -774 DCCLXXIV -775 DCCLXXV -776 DCCLXXVI -777 DCCLXXVII -778 DCCLXXVIII -779 DCCLXXIX -780 DCCLXXX -781 DCCLXXXI -782 DCCLXXXII -783 DCCLXXXIII -784 DCCLXXXIV -785 DCCLXXXV -786 DCCLXXXVI -787 DCCLXXXVII -788 DCCLXXXVIII -789 DCCLXXXIX -790 DCCXC -791 DCCXCI -792 DCCXCII -793 DCCXCIII -794 DCCXCIV -795 DCCXCV -796 DCCXCVI -797 DCCXCVII -798 DCCXCVIII -799 DCCXCIX -800 DCCC -801 DCCCI -802 DCCCII -803 DCCCIII -804 DCCCIV -805 DCCCV -806 DCCCVI -807 DCCCVII -808 DCCCVIII -809 DCCCIX -810 DCCCX -811 DCCCXI -812 DCCCXII -813 DCCCXIII -814 DCCCXIV -815 DCCCXV -816 DCCCXVI -817 DCCCXVII -818 DCCCXVIII -819 DCCCXIX -820 DCCCXX -821 DCCCXXI -822 DCCCXXII -823 DCCCXXIII -824 DCCCXXIV -825 DCCCXXV -826 DCCCXXVI -827 DCCCXXVII -828 DCCCXXVIII -829 DCCCXXIX -830 DCCCXXX -831 DCCCXXXI -832 DCCCXXXII -833 DCCCXXXIII -834 DCCCXXXIV -835 DCCCXXXV -836 DCCCXXXVI -837 DCCCXXXVII -838 DCCCXXXVIII -839 DCCCXXXIX -840 DCCCXL -841 DCCCXLI -842 DCCCXLII -843 DCCCXLIII -844 DCCCXLIV -845 DCCCXLV -846 DCCCXLVI -847 DCCCXLVII -848 DCCCXLVIII -849 DCCCXLIX -850 DCCCL -851 DCCCLI -852 DCCCLII -853 DCCCLIII -854 DCCCLIV -855 DCCCLV -856 DCCCLVI -857 DCCCLVII -858 DCCCLVIII -859 DCCCLIX -860 DCCCLX -861 DCCCLXI -862 DCCCLXII -863 DCCCLXIII -864 DCCCLXIV -865 DCCCLXV -866 DCCCLXVI -867 DCCCLXVII -868 DCCCLXVIII -869 DCCCLXIX -870 DCCCLXX -871 DCCCLXXI -872 DCCCLXXII -873 DCCCLXXIII -874 DCCCLXXIV -875 DCCCLXXV -876 DCCCLXXVI -877 DCCCLXXVII -878 DCCCLXXVIII -879 DCCCLXXIX -880 DCCCLXXX -881 DCCCLXXXI -882 DCCCLXXXII -883 DCCCLXXXIII -884 DCCCLXXXIV -885 DCCCLXXXV -886 DCCCLXXXVI -887 DCCCLXXXVII -888 DCCCLXXXVIII -889 DCCCLXXXIX -890 DCCCXC -891 DCCCXCI -892 DCCCXCII -893 DCCCXCIII -894 DCCCXCIV -895 DCCCXCV -896 DCCCXCVI -897 DCCCXCVII -898 DCCCXCVIII -899 DCCCXCIX -900 CM -901 CMI -902 CMII -903 CMIII -904 CMIV -905 CMV -906 CMVI -907 CMVII -908 CMVIII -909 CMIX -910 CMX -911 CMXI -912 CMXII -913 CMXIII -914 CMXIV -915 CMXV -916 CMXVI -917 CMXVII -918 CMXVIII -919 CMXIX -920 CMXX -921 CMXXI -922 CMXXII -923 CMXXIII -924 CMXXIV -925 CMXXV -926 CMXXVI -927 CMXXVII -928 CMXXVIII -929 CMXXIX -930 CMXXX -931 CMXXXI -932 CMXXXII -933 CMXXXIII -934 CMXXXIV -935 CMXXXV -936 CMXXXVI -937 CMXXXVII -938 CMXXXVIII -939 CMXXXIX -940 CMXL -941 CMXLI -942 CMXLII -943 CMXLIII -944 CMXLIV -945 CMXLV -946 CMXLVI -947 CMXLVII -948 CMXLVIII -949 CMXLIX -950 CML -951 CMLI -952 CMLII -953 CMLIII -954 CMLIV -955 CMLV -956 CMLVI -957 CMLVII -958 CMLVIII -959 CMLIX -960 CMLX -961 CMLXI -962 CMLXII -963 CMLXIII -964 CMLXIV -965 CMLXV -966 CMLXVI -967 CMLXVII -968 CMLXVIII -969 CMLXIX -970 CMLXX -971 CMLXXI -972 CMLXXII -973 CMLXXIII -974 CMLXXIV -975 CMLXXV -976 CMLXXVI -977 CMLXXVII -978 CMLXXVIII -979 CMLXXIX -980 CMLXXX -981 CMLXXXI -982 CMLXXXII -983 CMLXXXIII -984 CMLXXXIV -985 CMLXXXV -986 CMLXXXVI -987 CMLXXXVII -988 CMLXXXVIII -989 CMLXXXIX -990 CMXC -991 CMXCI -992 CMXCII -993 CMXCIII -994 CMXCIV -995 CMXCV -996 CMXCVI -997 CMXCVII -998 CMXCVIII -999 CMXCIX -1000 M -1001 MI -1002 MII -1003 MIII -1004 MIV -1005 MV -1006 MVI -1007 MVII -1008 MVIII -1009 MIX -1010 MX -1011 MXI -1012 MXII -1013 MXIII -1014 MXIV -1015 MXV -1016 MXVI -1017 MXVII -1018 MXVIII -1019 MXIX -1020 MXX -1021 MXXI -1022 MXXII -1023 MXXIII -1024 MXXIV -1025 MXXV -1026 MXXVI -1027 MXXVII -1028 MXXVIII -1029 MXXIX -1030 MXXX -1031 MXXXI -1032 MXXXII -1033 MXXXIII -1034 MXXXIV -1035 MXXXV -1036 MXXXVI -1037 MXXXVII -1038 MXXXVIII -1039 MXXXIX -1040 MXL -1041 MXLI -1042 MXLII -1043 MXLIII -1044 MXLIV -1045 MXLV -1046 MXLVI -1047 MXLVII -1048 MXLVIII -1049 MXLIX -1050 ML -1051 MLI -1052 MLII -1053 MLIII -1054 MLIV -1055 MLV -1056 MLVI -1057 MLVII -1058 MLVIII -1059 MLIX -1060 MLX -1061 MLXI -1062 MLXII -1063 MLXIII -1064 MLXIV -1065 MLXV -1066 MLXVI -1067 MLXVII -1068 MLXVIII -1069 MLXIX -1070 MLXX -1071 MLXXI -1072 MLXXII -1073 MLXXIII -1074 MLXXIV -1075 MLXXV -1076 MLXXVI -1077 MLXXVII -1078 MLXXVIII -1079 MLXXIX -1080 MLXXX -1081 MLXXXI -1082 MLXXXII -1083 MLXXXIII -1084 MLXXXIV -1085 MLXXXV -1086 MLXXXVI -1087 MLXXXVII -1088 MLXXXVIII -1089 MLXXXIX -1090 MXC -1091 MXCI -1092 MXCII -1093 MXCIII -1094 MXCIV -1095 MXCV -1096 MXCVI -1097 MXCVII -1098 MXCVIII -1099 MXCIX -1100 MC -1101 MCI -1102 MCII -1103 MCIII -1104 MCIV -1105 MCV -1106 MCVI -1107 MCVII -1108 MCVIII -1109 MCIX -1110 MCX -1111 MCXI -1112 MCXII -1113 MCXIII -1114 MCXIV -1115 MCXV -1116 MCXVI -1117 MCXVII -1118 MCXVIII -1119 MCXIX -1120 MCXX -1121 MCXXI -1122 MCXXII -1123 MCXXIII -1124 MCXXIV -1125 MCXXV -1126 MCXXVI -1127 MCXXVII -1128 MCXXVIII -1129 MCXXIX -1130 MCXXX -1131 MCXXXI -1132 MCXXXII -1133 MCXXXIII -1134 MCXXXIV -1135 MCXXXV -1136 MCXXXVI -1137 MCXXXVII -1138 MCXXXVIII -1139 MCXXXIX -1140 MCXL -1141 MCXLI -1142 MCXLII -1143 MCXLIII -1144 MCXLIV -1145 MCXLV -1146 MCXLVI -1147 MCXLVII -1148 MCXLVIII -1149 MCXLIX -1150 MCL -1151 MCLI -1152 MCLII -1153 MCLIII -1154 MCLIV -1155 MCLV -1156 MCLVI -1157 MCLVII -1158 MCLVIII -1159 MCLIX -1160 MCLX -1161 MCLXI -1162 MCLXII -1163 MCLXIII -1164 MCLXIV -1165 MCLXV -1166 MCLXVI -1167 MCLXVII -1168 MCLXVIII -1169 MCLXIX -1170 MCLXX -1171 MCLXXI -1172 MCLXXII -1173 MCLXXIII -1174 MCLXXIV -1175 MCLXXV -1176 MCLXXVI -1177 MCLXXVII -1178 MCLXXVIII -1179 MCLXXIX -1180 MCLXXX -1181 MCLXXXI -1182 MCLXXXII -1183 MCLXXXIII -1184 MCLXXXIV -1185 MCLXXXV -1186 MCLXXXVI -1187 MCLXXXVII -1188 MCLXXXVIII -1189 MCLXXXIX -1190 MCXC -1191 MCXCI -1192 MCXCII -1193 MCXCIII -1194 MCXCIV -1195 MCXCV -1196 MCXCVI -1197 MCXCVII -1198 MCXCVIII -1199 MCXCIX -1200 MCC -1201 MCCI -1202 MCCII -1203 MCCIII -1204 MCCIV -1205 MCCV -1206 MCCVI -1207 MCCVII -1208 MCCVIII -1209 MCCIX -1210 MCCX -1211 MCCXI -1212 MCCXII -1213 MCCXIII -1214 MCCXIV -1215 MCCXV -1216 MCCXVI -1217 MCCXVII -1218 MCCXVIII -1219 MCCXIX -1220 MCCXX -1221 MCCXXI -1222 MCCXXII -1223 MCCXXIII -1224 MCCXXIV -1225 MCCXXV -1226 MCCXXVI -1227 MCCXXVII -1228 MCCXXVIII -1229 MCCXXIX -1230 MCCXXX -1231 MCCXXXI -1232 MCCXXXII -1233 MCCXXXIII -1234 MCCXXXIV -1235 MCCXXXV -1236 MCCXXXVI -1237 MCCXXXVII -1238 MCCXXXVIII -1239 MCCXXXIX -1240 MCCXL -1241 MCCXLI -1242 MCCXLII -1243 MCCXLIII -1244 MCCXLIV -1245 MCCXLV -1246 MCCXLVI -1247 MCCXLVII -1248 MCCXLVIII -1249 MCCXLIX -1250 MCCL -1251 MCCLI -1252 MCCLII -1253 MCCLIII -1254 MCCLIV -1255 MCCLV -1256 MCCLVI -1257 MCCLVII -1258 MCCLVIII -1259 MCCLIX -1260 MCCLX -1261 MCCLXI -1262 MCCLXII -1263 MCCLXIII -1264 MCCLXIV -1265 MCCLXV -1266 MCCLXVI -1267 MCCLXVII -1268 MCCLXVIII -1269 MCCLXIX -1270 MCCLXX -1271 MCCLXXI -1272 MCCLXXII -1273 MCCLXXIII -1274 MCCLXXIV -1275 MCCLXXV -1276 MCCLXXVI -1277 MCCLXXVII -1278 MCCLXXVIII -1279 MCCLXXIX -1280 MCCLXXX -1281 MCCLXXXI -1282 MCCLXXXII -1283 MCCLXXXIII -1284 MCCLXXXIV -1285 MCCLXXXV -1286 MCCLXXXVI -1287 MCCLXXXVII -1288 MCCLXXXVIII -1289 MCCLXXXIX -1290 MCCXC -1291 MCCXCI -1292 MCCXCII -1293 MCCXCIII -1294 MCCXCIV -1295 MCCXCV -1296 MCCXCVI -1297 MCCXCVII -1298 MCCXCVIII -1299 MCCXCIX -1300 MCCC -1301 MCCCI -1302 MCCCII -1303 MCCCIII -1304 MCCCIV -1305 MCCCV -1306 MCCCVI -1307 MCCCVII -1308 MCCCVIII -1309 MCCCIX -1310 MCCCX -1311 MCCCXI -1312 MCCCXII -1313 MCCCXIII -1314 MCCCXIV -1315 MCCCXV -1316 MCCCXVI -1317 MCCCXVII -1318 MCCCXVIII -1319 MCCCXIX -1320 MCCCXX -1321 MCCCXXI -1322 MCCCXXII -1323 MCCCXXIII -1324 MCCCXXIV -1325 MCCCXXV -1326 MCCCXXVI -1327 MCCCXXVII -1328 MCCCXXVIII -1329 MCCCXXIX -1330 MCCCXXX -1331 MCCCXXXI -1332 MCCCXXXII -1333 MCCCXXXIII -1334 MCCCXXXIV -1335 MCCCXXXV -1336 MCCCXXXVI -1337 MCCCXXXVII -1338 MCCCXXXVIII -1339 MCCCXXXIX -1340 MCCCXL -1341 MCCCXLI -1342 MCCCXLII -1343 MCCCXLIII -1344 MCCCXLIV -1345 MCCCXLV -1346 MCCCXLVI -1347 MCCCXLVII -1348 MCCCXLVIII -1349 MCCCXLIX -1350 MCCCL -1351 MCCCLI -1352 MCCCLII -1353 MCCCLIII -1354 MCCCLIV -1355 MCCCLV -1356 MCCCLVI -1357 MCCCLVII -1358 MCCCLVIII -1359 MCCCLIX -1360 MCCCLX -1361 MCCCLXI -1362 MCCCLXII -1363 MCCCLXIII -1364 MCCCLXIV -1365 MCCCLXV -1366 MCCCLXVI -1367 MCCCLXVII -1368 MCCCLXVIII -1369 MCCCLXIX -1370 MCCCLXX -1371 MCCCLXXI -1372 MCCCLXXII -1373 MCCCLXXIII -1374 MCCCLXXIV -1375 MCCCLXXV -1376 MCCCLXXVI -1377 MCCCLXXVII -1378 MCCCLXXVIII -1379 MCCCLXXIX -1380 MCCCLXXX -1381 MCCCLXXXI -1382 MCCCLXXXII -1383 MCCCLXXXIII -1384 MCCCLXXXIV -1385 MCCCLXXXV -1386 MCCCLXXXVI -1387 MCCCLXXXVII -1388 MCCCLXXXVIII -1389 MCCCLXXXIX -1390 MCCCXC -1391 MCCCXCI -1392 MCCCXCII -1393 MCCCXCIII -1394 MCCCXCIV -1395 MCCCXCV -1396 MCCCXCVI -1397 MCCCXCVII -1398 MCCCXCVIII -1399 MCCCXCIX -1400 MCD -1401 MCDI -1402 MCDII -1403 MCDIII -1404 MCDIV -1405 MCDV -1406 MCDVI -1407 MCDVII -1408 MCDVIII -1409 MCDIX -1410 MCDX -1411 MCDXI -1412 MCDXII -1413 MCDXIII -1414 MCDXIV -1415 MCDXV -1416 MCDXVI -1417 MCDXVII -1418 MCDXVIII -1419 MCDXIX -1420 MCDXX -1421 MCDXXI -1422 MCDXXII -1423 MCDXXIII -1424 MCDXXIV -1425 MCDXXV -1426 MCDXXVI -1427 MCDXXVII -1428 MCDXXVIII -1429 MCDXXIX -1430 MCDXXX -1431 MCDXXXI -1432 MCDXXXII -1433 MCDXXXIII -1434 MCDXXXIV -1435 MCDXXXV -1436 MCDXXXVI -1437 MCDXXXVII -1438 MCDXXXVIII -1439 MCDXXXIX -1440 MCDXL -1441 MCDXLI -1442 MCDXLII -1443 MCDXLIII -1444 MCDXLIV -1445 MCDXLV -1446 MCDXLVI -1447 MCDXLVII -1448 MCDXLVIII -1449 MCDXLIX -1450 MCDL -1451 MCDLI -1452 MCDLII -1453 MCDLIII -1454 MCDLIV -1455 MCDLV -1456 MCDLVI -1457 MCDLVII -1458 MCDLVIII -1459 MCDLIX -1460 MCDLX -1461 MCDLXI -1462 MCDLXII -1463 MCDLXIII -1464 MCDLXIV -1465 MCDLXV -1466 MCDLXVI -1467 MCDLXVII -1468 MCDLXVIII -1469 MCDLXIX -1470 MCDLXX -1471 MCDLXXI -1472 MCDLXXII -1473 MCDLXXIII -1474 MCDLXXIV -1475 MCDLXXV -1476 MCDLXXVI -1477 MCDLXXVII -1478 MCDLXXVIII -1479 MCDLXXIX -1480 MCDLXXX -1481 MCDLXXXI -1482 MCDLXXXII -1483 MCDLXXXIII -1484 MCDLXXXIV -1485 MCDLXXXV -1486 MCDLXXXVI -1487 MCDLXXXVII -1488 MCDLXXXVIII -1489 MCDLXXXIX -1490 MCDXC -1491 MCDXCI -1492 MCDXCII -1493 MCDXCIII -1494 MCDXCIV -1495 MCDXCV -1496 MCDXCVI -1497 MCDXCVII -1498 MCDXCVIII -1499 MCDXCIX -1500 MD -1501 MDI -1502 MDII -1503 MDIII -1504 MDIV -1505 MDV -1506 MDVI -1507 MDVII -1508 MDVIII -1509 MDIX -1510 MDX -1511 MDXI -1512 MDXII -1513 MDXIII -1514 MDXIV -1515 MDXV -1516 MDXVI -1517 MDXVII -1518 MDXVIII -1519 MDXIX -1520 MDXX -1521 MDXXI -1522 MDXXII -1523 MDXXIII -1524 MDXXIV -1525 MDXXV -1526 MDXXVI -1527 MDXXVII -1528 MDXXVIII -1529 MDXXIX -1530 MDXXX -1531 MDXXXI -1532 MDXXXII -1533 MDXXXIII -1534 MDXXXIV -1535 MDXXXV -1536 MDXXXVI -1537 MDXXXVII -1538 MDXXXVIII -1539 MDXXXIX -1540 MDXL -1541 MDXLI -1542 MDXLII -1543 MDXLIII -1544 MDXLIV -1545 MDXLV -1546 MDXLVI -1547 MDXLVII -1548 MDXLVIII -1549 MDXLIX -1550 MDL -1551 MDLI -1552 MDLII -1553 MDLIII -1554 MDLIV -1555 MDLV -1556 MDLVI -1557 MDLVII -1558 MDLVIII -1559 MDLIX -1560 MDLX -1561 MDLXI -1562 MDLXII -1563 MDLXIII -1564 MDLXIV -1565 MDLXV -1566 MDLXVI -1567 MDLXVII -1568 MDLXVIII -1569 MDLXIX -1570 MDLXX -1571 MDLXXI -1572 MDLXXII -1573 MDLXXIII -1574 MDLXXIV -1575 MDLXXV -1576 MDLXXVI -1577 MDLXXVII -1578 MDLXXVIII -1579 MDLXXIX -1580 MDLXXX -1581 MDLXXXI -1582 MDLXXXII -1583 MDLXXXIII -1584 MDLXXXIV -1585 MDLXXXV -1586 MDLXXXVI -1587 MDLXXXVII -1588 MDLXXXVIII -1589 MDLXXXIX -1590 MDXC -1591 MDXCI -1592 MDXCII -1593 MDXCIII -1594 MDXCIV -1595 MDXCV -1596 MDXCVI -1597 MDXCVII -1598 MDXCVIII -1599 MDXCIX -1600 MDC -1601 MDCI -1602 MDCII -1603 MDCIII -1604 MDCIV -1605 MDCV -1606 MDCVI -1607 MDCVII -1608 MDCVIII -1609 MDCIX -1610 MDCX -1611 MDCXI -1612 MDCXII -1613 MDCXIII -1614 MDCXIV -1615 MDCXV -1616 MDCXVI -1617 MDCXVII -1618 MDCXVIII -1619 MDCXIX -1620 MDCXX -1621 MDCXXI -1622 MDCXXII -1623 MDCXXIII -1624 MDCXXIV -1625 MDCXXV -1626 MDCXXVI -1627 MDCXXVII -1628 MDCXXVIII -1629 MDCXXIX -1630 MDCXXX -1631 MDCXXXI -1632 MDCXXXII -1633 MDCXXXIII -1634 MDCXXXIV -1635 MDCXXXV -1636 MDCXXXVI -1637 MDCXXXVII -1638 MDCXXXVIII -1639 MDCXXXIX -1640 MDCXL -1641 MDCXLI -1642 MDCXLII -1643 MDCXLIII -1644 MDCXLIV -1645 MDCXLV -1646 MDCXLVI -1647 MDCXLVII -1648 MDCXLVIII -1649 MDCXLIX -1650 MDCL -1651 MDCLI -1652 MDCLII -1653 MDCLIII -1654 MDCLIV -1655 MDCLV -1656 MDCLVI -1657 MDCLVII -1658 MDCLVIII -1659 MDCLIX -1660 MDCLX -1661 MDCLXI -1662 MDCLXII -1663 MDCLXIII -1664 MDCLXIV -1665 MDCLXV -1666 MDCLXVI -1667 MDCLXVII -1668 MDCLXVIII -1669 MDCLXIX -1670 MDCLXX -1671 MDCLXXI -1672 MDCLXXII -1673 MDCLXXIII -1674 MDCLXXIV -1675 MDCLXXV -1676 MDCLXXVI -1677 MDCLXXVII -1678 MDCLXXVIII -1679 MDCLXXIX -1680 MDCLXXX -1681 MDCLXXXI -1682 MDCLXXXII -1683 MDCLXXXIII -1684 MDCLXXXIV -1685 MDCLXXXV -1686 MDCLXXXVI -1687 MDCLXXXVII -1688 MDCLXXXVIII -1689 MDCLXXXIX -1690 MDCXC -1691 MDCXCI -1692 MDCXCII -1693 MDCXCIII -1694 MDCXCIV -1695 MDCXCV -1696 MDCXCVI -1697 MDCXCVII -1698 MDCXCVIII -1699 MDCXCIX -1700 MDCC -1701 MDCCI -1702 MDCCII -1703 MDCCIII -1704 MDCCIV -1705 MDCCV -1706 MDCCVI -1707 MDCCVII -1708 MDCCVIII -1709 MDCCIX -1710 MDCCX -1711 MDCCXI -1712 MDCCXII -1713 MDCCXIII -1714 MDCCXIV -1715 MDCCXV -1716 MDCCXVI -1717 MDCCXVII -1718 MDCCXVIII -1719 MDCCXIX -1720 MDCCXX -1721 MDCCXXI -1722 MDCCXXII -1723 MDCCXXIII -1724 MDCCXXIV -1725 MDCCXXV -1726 MDCCXXVI -1727 MDCCXXVII -1728 MDCCXXVIII -1729 MDCCXXIX -1730 MDCCXXX -1731 MDCCXXXI -1732 MDCCXXXII -1733 MDCCXXXIII -1734 MDCCXXXIV -1735 MDCCXXXV -1736 MDCCXXXVI -1737 MDCCXXXVII -1738 MDCCXXXVIII -1739 MDCCXXXIX -1740 MDCCXL -1741 MDCCXLI -1742 MDCCXLII -1743 MDCCXLIII -1744 MDCCXLIV -1745 MDCCXLV -1746 MDCCXLVI -1747 MDCCXLVII -1748 MDCCXLVIII -1749 MDCCXLIX -1750 MDCCL -1751 MDCCLI -1752 MDCCLII -1753 MDCCLIII -1754 MDCCLIV -1755 MDCCLV -1756 MDCCLVI -1757 MDCCLVII -1758 MDCCLVIII -1759 MDCCLIX -1760 MDCCLX -1761 MDCCLXI -1762 MDCCLXII -1763 MDCCLXIII -1764 MDCCLXIV -1765 MDCCLXV -1766 MDCCLXVI -1767 MDCCLXVII -1768 MDCCLXVIII -1769 MDCCLXIX -1770 MDCCLXX -1771 MDCCLXXI -1772 MDCCLXXII -1773 MDCCLXXIII -1774 MDCCLXXIV -1775 MDCCLXXV -1776 MDCCLXXVI -1777 MDCCLXXVII -1778 MDCCLXXVIII -1779 MDCCLXXIX -1780 MDCCLXXX -1781 MDCCLXXXI -1782 MDCCLXXXII -1783 MDCCLXXXIII -1784 MDCCLXXXIV -1785 MDCCLXXXV -1786 MDCCLXXXVI -1787 MDCCLXXXVII -1788 MDCCLXXXVIII -1789 MDCCLXXXIX -1790 MDCCXC -1791 MDCCXCI -1792 MDCCXCII -1793 MDCCXCIII -1794 MDCCXCIV -1795 MDCCXCV -1796 MDCCXCVI -1797 MDCCXCVII -1798 MDCCXCVIII -1799 MDCCXCIX -1800 MDCCC -1801 MDCCCI -1802 MDCCCII -1803 MDCCCIII -1804 MDCCCIV -1805 MDCCCV -1806 MDCCCVI -1807 MDCCCVII -1808 MDCCCVIII -1809 MDCCCIX -1810 MDCCCX -1811 MDCCCXI -1812 MDCCCXII -1813 MDCCCXIII -1814 MDCCCXIV -1815 MDCCCXV -1816 MDCCCXVI -1817 MDCCCXVII -1818 MDCCCXVIII -1819 MDCCCXIX -1820 MDCCCXX -1821 MDCCCXXI -1822 MDCCCXXII -1823 MDCCCXXIII -1824 MDCCCXXIV -1825 MDCCCXXV -1826 MDCCCXXVI -1827 MDCCCXXVII -1828 MDCCCXXVIII -1829 MDCCCXXIX -1830 MDCCCXXX -1831 MDCCCXXXI -1832 MDCCCXXXII -1833 MDCCCXXXIII -1834 MDCCCXXXIV -1835 MDCCCXXXV -1836 MDCCCXXXVI -1837 MDCCCXXXVII -1838 MDCCCXXXVIII -1839 MDCCCXXXIX -1840 MDCCCXL -1841 MDCCCXLI -1842 MDCCCXLII -1843 MDCCCXLIII -1844 MDCCCXLIV -1845 MDCCCXLV -1846 MDCCCXLVI -1847 MDCCCXLVII -1848 MDCCCXLVIII -1849 MDCCCXLIX -1850 MDCCCL -1851 MDCCCLI -1852 MDCCCLII -1853 MDCCCLIII -1854 MDCCCLIV -1855 MDCCCLV -1856 MDCCCLVI -1857 MDCCCLVII -1858 MDCCCLVIII -1859 MDCCCLIX -1860 MDCCCLX -1861 MDCCCLXI -1862 MDCCCLXII -1863 MDCCCLXIII -1864 MDCCCLXIV -1865 MDCCCLXV -1866 MDCCCLXVI -1867 MDCCCLXVII -1868 MDCCCLXVIII -1869 MDCCCLXIX -1870 MDCCCLXX -1871 MDCCCLXXI -1872 MDCCCLXXII -1873 MDCCCLXXIII -1874 MDCCCLXXIV -1875 MDCCCLXXV -1876 MDCCCLXXVI -1877 MDCCCLXXVII -1878 MDCCCLXXVIII -1879 MDCCCLXXIX -1880 MDCCCLXXX -1881 MDCCCLXXXI -1882 MDCCCLXXXII -1883 MDCCCLXXXIII -1884 MDCCCLXXXIV -1885 MDCCCLXXXV -1886 MDCCCLXXXVI -1887 MDCCCLXXXVII -1888 MDCCCLXXXVIII -1889 MDCCCLXXXIX -1890 MDCCCXC -1891 MDCCCXCI -1892 MDCCCXCII -1893 MDCCCXCIII -1894 MDCCCXCIV -1895 MDCCCXCV -1896 MDCCCXCVI -1897 MDCCCXCVII -1898 MDCCCXCVIII -1899 MDCCCXCIX -1900 MCM -1901 MCMI -1902 MCMII -1903 MCMIII -1904 MCMIV -1905 MCMV -1906 MCMVI -1907 MCMVII -1908 MCMVIII -1909 MCMIX -1910 MCMX -1911 MCMXI -1912 MCMXII -1913 MCMXIII -1914 MCMXIV -1915 MCMXV -1916 MCMXVI -1917 MCMXVII -1918 MCMXVIII -1919 MCMXIX -1920 MCMXX -1921 MCMXXI -1922 MCMXXII -1923 MCMXXIII -1924 MCMXXIV -1925 MCMXXV -1926 MCMXXVI -1927 MCMXXVII -1928 MCMXXVIII -1929 MCMXXIX -1930 MCMXXX -1931 MCMXXXI -1932 MCMXXXII -1933 MCMXXXIII -1934 MCMXXXIV -1935 MCMXXXV -1936 MCMXXXVI -1937 MCMXXXVII -1938 MCMXXXVIII -1939 MCMXXXIX -1940 MCMXL -1941 MCMXLI -1942 MCMXLII -1943 MCMXLIII -1944 MCMXLIV -1945 MCMXLV -1946 MCMXLVI -1947 MCMXLVII -1948 MCMXLVIII -1949 MCMXLIX -1950 MCML -1951 MCMLI -1952 MCMLII -1953 MCMLIII -1954 MCMLIV -1955 MCMLV -1956 MCMLVI -1957 MCMLVII -1958 MCMLVIII -1959 MCMLIX -1960 MCMLX -1961 MCMLXI -1962 MCMLXII -1963 MCMLXIII -1964 MCMLXIV -1965 MCMLXV -1966 MCMLXVI -1967 MCMLXVII -1968 MCMLXVIII -1969 MCMLXIX -1970 MCMLXX -1971 MCMLXXI -1972 MCMLXXII -1973 MCMLXXIII -1974 MCMLXXIV -1975 MCMLXXV -1976 MCMLXXVI -1977 MCMLXXVII -1978 MCMLXXVIII -1979 MCMLXXIX -1980 MCMLXXX -1981 MCMLXXXI -1982 MCMLXXXII -1983 MCMLXXXIII -1984 MCMLXXXIV -1985 MCMLXXXV -1986 MCMLXXXVI -1987 MCMLXXXVII -1988 MCMLXXXVIII -1989 MCMLXXXIX -1990 MCMXC -1991 MCMXCI -1992 MCMXCII -1993 MCMXCIII -1994 MCMXCIV -1995 MCMXCV -1996 MCMXCVI -1997 MCMXCVII -1998 MCMXCVIII -1999 MCMXCIX -2000 MM -2001 MMI -2002 MMII -2003 MMIII -2004 MMIV -2005 MMV -2006 MMVI -2007 MMVII -2008 MMVIII -2009 MMIX -2010 MMX -2011 MMXI -2012 MMXII -2013 MMXIII -2014 MMXIV -2015 MMXV -2016 MMXVI -2017 MMXVII -2018 MMXVIII -2019 MMXIX -2020 MMXX -2021 MMXXI -2022 MMXXII -2023 MMXXIII -2024 MMXXIV -2025 MMXXV -2026 MMXXVI -2027 MMXXVII -2028 MMXXVIII -2029 MMXXIX -2030 MMXXX -2031 MMXXXI -2032 MMXXXII -2033 MMXXXIII -2034 MMXXXIV -2035 MMXXXV -2036 MMXXXVI -2037 MMXXXVII -2038 MMXXXVIII -2039 MMXXXIX -2040 MMXL -2041 MMXLI -2042 MMXLII -2043 MMXLIII -2044 MMXLIV -2045 MMXLV -2046 MMXLVI -2047 MMXLVII -2048 MMXLVIII -2049 MMXLIX -2050 MML -2051 MMLI -2052 MMLII -2053 MMLIII -2054 MMLIV -2055 MMLV -2056 MMLVI -2057 MMLVII -2058 MMLVIII -2059 MMLIX -2060 MMLX -2061 MMLXI -2062 MMLXII -2063 MMLXIII -2064 MMLXIV -2065 MMLXV -2066 MMLXVI -2067 MMLXVII -2068 MMLXVIII -2069 MMLXIX -2070 MMLXX -2071 MMLXXI -2072 MMLXXII -2073 MMLXXIII -2074 MMLXXIV -2075 MMLXXV -2076 MMLXXVI -2077 MMLXXVII -2078 MMLXXVIII -2079 MMLXXIX -2080 MMLXXX -2081 MMLXXXI -2082 MMLXXXII -2083 MMLXXXIII -2084 MMLXXXIV -2085 MMLXXXV -2086 MMLXXXVI -2087 MMLXXXVII -2088 MMLXXXVIII -2089 MMLXXXIX -2090 MMXC -2091 MMXCI -2092 MMXCII -2093 MMXCIII -2094 MMXCIV -2095 MMXCV -2096 MMXCVI -2097 MMXCVII -2098 MMXCVIII -2099 MMXCIX -2100 MMC -2101 MMCI -2102 MMCII -2103 MMCIII -2104 MMCIV -2105 MMCV -2106 MMCVI -2107 MMCVII -2108 MMCVIII -2109 MMCIX -2110 MMCX -2111 MMCXI -2112 MMCXII -2113 MMCXIII -2114 MMCXIV -2115 MMCXV -2116 MMCXVI -2117 MMCXVII -2118 MMCXVIII -2119 MMCXIX -2120 MMCXX -2121 MMCXXI -2122 MMCXXII -2123 MMCXXIII -2124 MMCXXIV -2125 MMCXXV -2126 MMCXXVI -2127 MMCXXVII -2128 MMCXXVIII -2129 MMCXXIX -2130 MMCXXX -2131 MMCXXXI -2132 MMCXXXII -2133 MMCXXXIII -2134 MMCXXXIV -2135 MMCXXXV -2136 MMCXXXVI -2137 MMCXXXVII -2138 MMCXXXVIII -2139 MMCXXXIX -2140 MMCXL -2141 MMCXLI -2142 MMCXLII -2143 MMCXLIII -2144 MMCXLIV -2145 MMCXLV -2146 MMCXLVI -2147 MMCXLVII -2148 MMCXLVIII -2149 MMCXLIX -2150 MMCL -2151 MMCLI -2152 MMCLII -2153 MMCLIII -2154 MMCLIV -2155 MMCLV -2156 MMCLVI -2157 MMCLVII -2158 MMCLVIII -2159 MMCLIX -2160 MMCLX -2161 MMCLXI -2162 MMCLXII -2163 MMCLXIII -2164 MMCLXIV -2165 MMCLXV -2166 MMCLXVI -2167 MMCLXVII -2168 MMCLXVIII -2169 MMCLXIX -2170 MMCLXX -2171 MMCLXXI -2172 MMCLXXII -2173 MMCLXXIII -2174 MMCLXXIV -2175 MMCLXXV -2176 MMCLXXVI -2177 MMCLXXVII -2178 MMCLXXVIII -2179 MMCLXXIX -2180 MMCLXXX -2181 MMCLXXXI -2182 MMCLXXXII -2183 MMCLXXXIII -2184 MMCLXXXIV -2185 MMCLXXXV -2186 MMCLXXXVI -2187 MMCLXXXVII -2188 MMCLXXXVIII -2189 MMCLXXXIX -2190 MMCXC -2191 MMCXCI -2192 MMCXCII -2193 MMCXCIII -2194 MMCXCIV -2195 MMCXCV -2196 MMCXCVI -2197 MMCXCVII -2198 MMCXCVIII -2199 MMCXCIX -2200 MMCC -2201 MMCCI -2202 MMCCII -2203 MMCCIII -2204 MMCCIV -2205 MMCCV -2206 MMCCVI -2207 MMCCVII -2208 MMCCVIII -2209 MMCCIX -2210 MMCCX -2211 MMCCXI -2212 MMCCXII -2213 MMCCXIII -2214 MMCCXIV -2215 MMCCXV -2216 MMCCXVI -2217 MMCCXVII -2218 MMCCXVIII -2219 MMCCXIX -2220 MMCCXX -2221 MMCCXXI -2222 MMCCXXII -2223 MMCCXXIII -2224 MMCCXXIV -2225 MMCCXXV -2226 MMCCXXVI -2227 MMCCXXVII -2228 MMCCXXVIII -2229 MMCCXXIX -2230 MMCCXXX -2231 MMCCXXXI -2232 MMCCXXXII -2233 MMCCXXXIII -2234 MMCCXXXIV -2235 MMCCXXXV -2236 MMCCXXXVI -2237 MMCCXXXVII -2238 MMCCXXXVIII -2239 MMCCXXXIX -2240 MMCCXL -2241 MMCCXLI -2242 MMCCXLII -2243 MMCCXLIII -2244 MMCCXLIV -2245 MMCCXLV -2246 MMCCXLVI -2247 MMCCXLVII -2248 MMCCXLVIII -2249 MMCCXLIX -2250 MMCCL -2251 MMCCLI -2252 MMCCLII -2253 MMCCLIII -2254 MMCCLIV -2255 MMCCLV -2256 MMCCLVI -2257 MMCCLVII -2258 MMCCLVIII -2259 MMCCLIX -2260 MMCCLX -2261 MMCCLXI -2262 MMCCLXII -2263 MMCCLXIII -2264 MMCCLXIV -2265 MMCCLXV -2266 MMCCLXVI -2267 MMCCLXVII -2268 MMCCLXVIII -2269 MMCCLXIX -2270 MMCCLXX -2271 MMCCLXXI -2272 MMCCLXXII -2273 MMCCLXXIII -2274 MMCCLXXIV -2275 MMCCLXXV -2276 MMCCLXXVI -2277 MMCCLXXVII -2278 MMCCLXXVIII -2279 MMCCLXXIX -2280 MMCCLXXX -2281 MMCCLXXXI -2282 MMCCLXXXII -2283 MMCCLXXXIII -2284 MMCCLXXXIV -2285 MMCCLXXXV -2286 MMCCLXXXVI -2287 MMCCLXXXVII -2288 MMCCLXXXVIII -2289 MMCCLXXXIX -2290 MMCCXC -2291 MMCCXCI -2292 MMCCXCII -2293 MMCCXCIII -2294 MMCCXCIV -2295 MMCCXCV -2296 MMCCXCVI -2297 MMCCXCVII -2298 MMCCXCVIII -2299 MMCCXCIX -2300 MMCCC -2301 MMCCCI -2302 MMCCCII -2303 MMCCCIII -2304 MMCCCIV -2305 MMCCCV -2306 MMCCCVI -2307 MMCCCVII -2308 MMCCCVIII -2309 MMCCCIX -2310 MMCCCX -2311 MMCCCXI -2312 MMCCCXII -2313 MMCCCXIII -2314 MMCCCXIV -2315 MMCCCXV -2316 MMCCCXVI -2317 MMCCCXVII -2318 MMCCCXVIII -2319 MMCCCXIX -2320 MMCCCXX -2321 MMCCCXXI -2322 MMCCCXXII -2323 MMCCCXXIII -2324 MMCCCXXIV -2325 MMCCCXXV -2326 MMCCCXXVI -2327 MMCCCXXVII -2328 MMCCCXXVIII -2329 MMCCCXXIX -2330 MMCCCXXX -2331 MMCCCXXXI -2332 MMCCCXXXII -2333 MMCCCXXXIII -2334 MMCCCXXXIV -2335 MMCCCXXXV -2336 MMCCCXXXVI -2337 MMCCCXXXVII -2338 MMCCCXXXVIII -2339 MMCCCXXXIX -2340 MMCCCXL -2341 MMCCCXLI -2342 MMCCCXLII -2343 MMCCCXLIII -2344 MMCCCXLIV -2345 MMCCCXLV -2346 MMCCCXLVI -2347 MMCCCXLVII -2348 MMCCCXLVIII -2349 MMCCCXLIX -2350 MMCCCL -2351 MMCCCLI -2352 MMCCCLII -2353 MMCCCLIII -2354 MMCCCLIV -2355 MMCCCLV -2356 MMCCCLVI -2357 MMCCCLVII -2358 MMCCCLVIII -2359 MMCCCLIX -2360 MMCCCLX -2361 MMCCCLXI -2362 MMCCCLXII -2363 MMCCCLXIII -2364 MMCCCLXIV -2365 MMCCCLXV -2366 MMCCCLXVI -2367 MMCCCLXVII -2368 MMCCCLXVIII -2369 MMCCCLXIX -2370 MMCCCLXX -2371 MMCCCLXXI -2372 MMCCCLXXII -2373 MMCCCLXXIII -2374 MMCCCLXXIV -2375 MMCCCLXXV -2376 MMCCCLXXVI -2377 MMCCCLXXVII -2378 MMCCCLXXVIII -2379 MMCCCLXXIX -2380 MMCCCLXXX -2381 MMCCCLXXXI -2382 MMCCCLXXXII -2383 MMCCCLXXXIII -2384 MMCCCLXXXIV -2385 MMCCCLXXXV -2386 MMCCCLXXXVI -2387 MMCCCLXXXVII -2388 MMCCCLXXXVIII -2389 MMCCCLXXXIX -2390 MMCCCXC -2391 MMCCCXCI -2392 MMCCCXCII -2393 MMCCCXCIII -2394 MMCCCXCIV -2395 MMCCCXCV -2396 MMCCCXCVI -2397 MMCCCXCVII -2398 MMCCCXCVIII -2399 MMCCCXCIX -2400 MMCD -2401 MMCDI -2402 MMCDII -2403 MMCDIII -2404 MMCDIV -2405 MMCDV -2406 MMCDVI -2407 MMCDVII -2408 MMCDVIII -2409 MMCDIX -2410 MMCDX -2411 MMCDXI -2412 MMCDXII -2413 MMCDXIII -2414 MMCDXIV -2415 MMCDXV -2416 MMCDXVI -2417 MMCDXVII -2418 MMCDXVIII -2419 MMCDXIX -2420 MMCDXX -2421 MMCDXXI -2422 MMCDXXII -2423 MMCDXXIII -2424 MMCDXXIV -2425 MMCDXXV -2426 MMCDXXVI -2427 MMCDXXVII -2428 MMCDXXVIII -2429 MMCDXXIX -2430 MMCDXXX -2431 MMCDXXXI -2432 MMCDXXXII -2433 MMCDXXXIII -2434 MMCDXXXIV -2435 MMCDXXXV -2436 MMCDXXXVI -2437 MMCDXXXVII -2438 MMCDXXXVIII -2439 MMCDXXXIX -2440 MMCDXL -2441 MMCDXLI -2442 MMCDXLII -2443 MMCDXLIII -2444 MMCDXLIV -2445 MMCDXLV -2446 MMCDXLVI -2447 MMCDXLVII -2448 MMCDXLVIII -2449 MMCDXLIX -2450 MMCDL -2451 MMCDLI -2452 MMCDLII -2453 MMCDLIII -2454 MMCDLIV -2455 MMCDLV -2456 MMCDLVI -2457 MMCDLVII -2458 MMCDLVIII -2459 MMCDLIX -2460 MMCDLX -2461 MMCDLXI -2462 MMCDLXII -2463 MMCDLXIII -2464 MMCDLXIV -2465 MMCDLXV -2466 MMCDLXVI -2467 MMCDLXVII -2468 MMCDLXVIII -2469 MMCDLXIX -2470 MMCDLXX -2471 MMCDLXXI -2472 MMCDLXXII -2473 MMCDLXXIII -2474 MMCDLXXIV -2475 MMCDLXXV -2476 MMCDLXXVI -2477 MMCDLXXVII -2478 MMCDLXXVIII -2479 MMCDLXXIX -2480 MMCDLXXX -2481 MMCDLXXXI -2482 MMCDLXXXII -2483 MMCDLXXXIII -2484 MMCDLXXXIV -2485 MMCDLXXXV -2486 MMCDLXXXVI -2487 MMCDLXXXVII -2488 MMCDLXXXVIII -2489 MMCDLXXXIX -2490 MMCDXC -2491 MMCDXCI -2492 MMCDXCII -2493 MMCDXCIII -2494 MMCDXCIV -2495 MMCDXCV -2496 MMCDXCVI -2497 MMCDXCVII -2498 MMCDXCVIII -2499 MMCDXCIX -2500 MMD -2501 MMDI -2502 MMDII -2503 MMDIII -2504 MMDIV -2505 MMDV -2506 MMDVI -2507 MMDVII -2508 MMDVIII -2509 MMDIX -2510 MMDX -2511 MMDXI -2512 MMDXII -2513 MMDXIII -2514 MMDXIV -2515 MMDXV -2516 MMDXVI -2517 MMDXVII -2518 MMDXVIII -2519 MMDXIX -2520 MMDXX -2521 MMDXXI -2522 MMDXXII -2523 MMDXXIII -2524 MMDXXIV -2525 MMDXXV -2526 MMDXXVI -2527 MMDXXVII -2528 MMDXXVIII -2529 MMDXXIX -2530 MMDXXX -2531 MMDXXXI -2532 MMDXXXII -2533 MMDXXXIII -2534 MMDXXXIV -2535 MMDXXXV -2536 MMDXXXVI -2537 MMDXXXVII -2538 MMDXXXVIII -2539 MMDXXXIX -2540 MMDXL -2541 MMDXLI -2542 MMDXLII -2543 MMDXLIII -2544 MMDXLIV -2545 MMDXLV -2546 MMDXLVI -2547 MMDXLVII -2548 MMDXLVIII -2549 MMDXLIX -2550 MMDL -2551 MMDLI -2552 MMDLII -2553 MMDLIII -2554 MMDLIV -2555 MMDLV -2556 MMDLVI -2557 MMDLVII -2558 MMDLVIII -2559 MMDLIX -2560 MMDLX -2561 MMDLXI -2562 MMDLXII -2563 MMDLXIII -2564 MMDLXIV -2565 MMDLXV -2566 MMDLXVI -2567 MMDLXVII -2568 MMDLXVIII -2569 MMDLXIX -2570 MMDLXX -2571 MMDLXXI -2572 MMDLXXII -2573 MMDLXXIII -2574 MMDLXXIV -2575 MMDLXXV -2576 MMDLXXVI -2577 MMDLXXVII -2578 MMDLXXVIII -2579 MMDLXXIX -2580 MMDLXXX -2581 MMDLXXXI -2582 MMDLXXXII -2583 MMDLXXXIII -2584 MMDLXXXIV -2585 MMDLXXXV -2586 MMDLXXXVI -2587 MMDLXXXVII -2588 MMDLXXXVIII -2589 MMDLXXXIX -2590 MMDXC -2591 MMDXCI -2592 MMDXCII -2593 MMDXCIII -2594 MMDXCIV -2595 MMDXCV -2596 MMDXCVI -2597 MMDXCVII -2598 MMDXCVIII -2599 MMDXCIX -2600 MMDC -2601 MMDCI -2602 MMDCII -2603 MMDCIII -2604 MMDCIV -2605 MMDCV -2606 MMDCVI -2607 MMDCVII -2608 MMDCVIII -2609 MMDCIX -2610 MMDCX -2611 MMDCXI -2612 MMDCXII -2613 MMDCXIII -2614 MMDCXIV -2615 MMDCXV -2616 MMDCXVI -2617 MMDCXVII -2618 MMDCXVIII -2619 MMDCXIX -2620 MMDCXX -2621 MMDCXXI -2622 MMDCXXII -2623 MMDCXXIII -2624 MMDCXXIV -2625 MMDCXXV -2626 MMDCXXVI -2627 MMDCXXVII -2628 MMDCXXVIII -2629 MMDCXXIX -2630 MMDCXXX -2631 MMDCXXXI -2632 MMDCXXXII -2633 MMDCXXXIII -2634 MMDCXXXIV -2635 MMDCXXXV -2636 MMDCXXXVI -2637 MMDCXXXVII -2638 MMDCXXXVIII -2639 MMDCXXXIX -2640 MMDCXL -2641 MMDCXLI -2642 MMDCXLII -2643 MMDCXLIII -2644 MMDCXLIV -2645 MMDCXLV -2646 MMDCXLVI -2647 MMDCXLVII -2648 MMDCXLVIII -2649 MMDCXLIX -2650 MMDCL -2651 MMDCLI -2652 MMDCLII -2653 MMDCLIII -2654 MMDCLIV -2655 MMDCLV -2656 MMDCLVI -2657 MMDCLVII -2658 MMDCLVIII -2659 MMDCLIX -2660 MMDCLX -2661 MMDCLXI -2662 MMDCLXII -2663 MMDCLXIII -2664 MMDCLXIV -2665 MMDCLXV -2666 MMDCLXVI -2667 MMDCLXVII -2668 MMDCLXVIII -2669 MMDCLXIX -2670 MMDCLXX -2671 MMDCLXXI -2672 MMDCLXXII -2673 MMDCLXXIII -2674 MMDCLXXIV -2675 MMDCLXXV -2676 MMDCLXXVI -2677 MMDCLXXVII -2678 MMDCLXXVIII -2679 MMDCLXXIX -2680 MMDCLXXX -2681 MMDCLXXXI -2682 MMDCLXXXII -2683 MMDCLXXXIII -2684 MMDCLXXXIV -2685 MMDCLXXXV -2686 MMDCLXXXVI -2687 MMDCLXXXVII -2688 MMDCLXXXVIII -2689 MMDCLXXXIX -2690 MMDCXC -2691 MMDCXCI -2692 MMDCXCII -2693 MMDCXCIII -2694 MMDCXCIV -2695 MMDCXCV -2696 MMDCXCVI -2697 MMDCXCVII -2698 MMDCXCVIII -2699 MMDCXCIX -2700 MMDCC -2701 MMDCCI -2702 MMDCCII -2703 MMDCCIII -2704 MMDCCIV -2705 MMDCCV -2706 MMDCCVI -2707 MMDCCVII -2708 MMDCCVIII -2709 MMDCCIX -2710 MMDCCX -2711 MMDCCXI -2712 MMDCCXII -2713 MMDCCXIII -2714 MMDCCXIV -2715 MMDCCXV -2716 MMDCCXVI -2717 MMDCCXVII -2718 MMDCCXVIII -2719 MMDCCXIX -2720 MMDCCXX -2721 MMDCCXXI -2722 MMDCCXXII -2723 MMDCCXXIII -2724 MMDCCXXIV -2725 MMDCCXXV -2726 MMDCCXXVI -2727 MMDCCXXVII -2728 MMDCCXXVIII -2729 MMDCCXXIX -2730 MMDCCXXX -2731 MMDCCXXXI -2732 MMDCCXXXII -2733 MMDCCXXXIII -2734 MMDCCXXXIV -2735 MMDCCXXXV -2736 MMDCCXXXVI -2737 MMDCCXXXVII -2738 MMDCCXXXVIII -2739 MMDCCXXXIX -2740 MMDCCXL -2741 MMDCCXLI -2742 MMDCCXLII -2743 MMDCCXLIII -2744 MMDCCXLIV -2745 MMDCCXLV -2746 MMDCCXLVI -2747 MMDCCXLVII -2748 MMDCCXLVIII -2749 MMDCCXLIX -2750 MMDCCL -2751 MMDCCLI -2752 MMDCCLII -2753 MMDCCLIII -2754 MMDCCLIV -2755 MMDCCLV -2756 MMDCCLVI -2757 MMDCCLVII -2758 MMDCCLVIII -2759 MMDCCLIX -2760 MMDCCLX -2761 MMDCCLXI -2762 MMDCCLXII -2763 MMDCCLXIII -2764 MMDCCLXIV -2765 MMDCCLXV -2766 MMDCCLXVI -2767 MMDCCLXVII -2768 MMDCCLXVIII -2769 MMDCCLXIX -2770 MMDCCLXX -2771 MMDCCLXXI -2772 MMDCCLXXII -2773 MMDCCLXXIII -2774 MMDCCLXXIV -2775 MMDCCLXXV -2776 MMDCCLXXVI -2777 MMDCCLXXVII -2778 MMDCCLXXVIII -2779 MMDCCLXXIX -2780 MMDCCLXXX -2781 MMDCCLXXXI -2782 MMDCCLXXXII -2783 MMDCCLXXXIII -2784 MMDCCLXXXIV -2785 MMDCCLXXXV -2786 MMDCCLXXXVI -2787 MMDCCLXXXVII -2788 MMDCCLXXXVIII -2789 MMDCCLXXXIX -2790 MMDCCXC -2791 MMDCCXCI -2792 MMDCCXCII -2793 MMDCCXCIII -2794 MMDCCXCIV -2795 MMDCCXCV -2796 MMDCCXCVI -2797 MMDCCXCVII -2798 MMDCCXCVIII -2799 MMDCCXCIX -2800 MMDCCC -2801 MMDCCCI -2802 MMDCCCII -2803 MMDCCCIII -2804 MMDCCCIV -2805 MMDCCCV -2806 MMDCCCVI -2807 MMDCCCVII -2808 MMDCCCVIII -2809 MMDCCCIX -2810 MMDCCCX -2811 MMDCCCXI -2812 MMDCCCXII -2813 MMDCCCXIII -2814 MMDCCCXIV -2815 MMDCCCXV -2816 MMDCCCXVI -2817 MMDCCCXVII -2818 MMDCCCXVIII -2819 MMDCCCXIX -2820 MMDCCCXX -2821 MMDCCCXXI -2822 MMDCCCXXII -2823 MMDCCCXXIII -2824 MMDCCCXXIV -2825 MMDCCCXXV -2826 MMDCCCXXVI -2827 MMDCCCXXVII -2828 MMDCCCXXVIII -2829 MMDCCCXXIX -2830 MMDCCCXXX -2831 MMDCCCXXXI -2832 MMDCCCXXXII -2833 MMDCCCXXXIII -2834 MMDCCCXXXIV -2835 MMDCCCXXXV -2836 MMDCCCXXXVI -2837 MMDCCCXXXVII -2838 MMDCCCXXXVIII -2839 MMDCCCXXXIX -2840 MMDCCCXL -2841 MMDCCCXLI -2842 MMDCCCXLII -2843 MMDCCCXLIII -2844 MMDCCCXLIV -2845 MMDCCCXLV -2846 MMDCCCXLVI -2847 MMDCCCXLVII -2848 MMDCCCXLVIII -2849 MMDCCCXLIX -2850 MMDCCCL -2851 MMDCCCLI -2852 MMDCCCLII -2853 MMDCCCLIII -2854 MMDCCCLIV -2855 MMDCCCLV -2856 MMDCCCLVI -2857 MMDCCCLVII -2858 MMDCCCLVIII -2859 MMDCCCLIX -2860 MMDCCCLX -2861 MMDCCCLXI -2862 MMDCCCLXII -2863 MMDCCCLXIII -2864 MMDCCCLXIV -2865 MMDCCCLXV -2866 MMDCCCLXVI -2867 MMDCCCLXVII -2868 MMDCCCLXVIII -2869 MMDCCCLXIX -2870 MMDCCCLXX -2871 MMDCCCLXXI -2872 MMDCCCLXXII -2873 MMDCCCLXXIII -2874 MMDCCCLXXIV -2875 MMDCCCLXXV -2876 MMDCCCLXXVI -2877 MMDCCCLXXVII -2878 MMDCCCLXXVIII -2879 MMDCCCLXXIX -2880 MMDCCCLXXX -2881 MMDCCCLXXXI -2882 MMDCCCLXXXII -2883 MMDCCCLXXXIII -2884 MMDCCCLXXXIV -2885 MMDCCCLXXXV -2886 MMDCCCLXXXVI -2887 MMDCCCLXXXVII -2888 MMDCCCLXXXVIII -2889 MMDCCCLXXXIX -2890 MMDCCCXC -2891 MMDCCCXCI -2892 MMDCCCXCII -2893 MMDCCCXCIII -2894 MMDCCCXCIV -2895 MMDCCCXCV -2896 MMDCCCXCVI -2897 MMDCCCXCVII -2898 MMDCCCXCVIII -2899 MMDCCCXCIX -2900 MMCM -2901 MMCMI -2902 MMCMII -2903 MMCMIII -2904 MMCMIV -2905 MMCMV -2906 MMCMVI -2907 MMCMVII -2908 MMCMVIII -2909 MMCMIX -2910 MMCMX -2911 MMCMXI -2912 MMCMXII -2913 MMCMXIII -2914 MMCMXIV -2915 MMCMXV -2916 MMCMXVI -2917 MMCMXVII -2918 MMCMXVIII -2919 MMCMXIX -2920 MMCMXX -2921 MMCMXXI -2922 MMCMXXII -2923 MMCMXXIII -2924 MMCMXXIV -2925 MMCMXXV -2926 MMCMXXVI -2927 MMCMXXVII -2928 MMCMXXVIII -2929 MMCMXXIX -2930 MMCMXXX -2931 MMCMXXXI -2932 MMCMXXXII -2933 MMCMXXXIII -2934 MMCMXXXIV -2935 MMCMXXXV -2936 MMCMXXXVI -2937 MMCMXXXVII -2938 MMCMXXXVIII -2939 MMCMXXXIX -2940 MMCMXL -2941 MMCMXLI -2942 MMCMXLII -2943 MMCMXLIII -2944 MMCMXLIV -2945 MMCMXLV -2946 MMCMXLVI -2947 MMCMXLVII -2948 MMCMXLVIII -2949 MMCMXLIX -2950 MMCML -2951 MMCMLI -2952 MMCMLII -2953 MMCMLIII -2954 MMCMLIV -2955 MMCMLV -2956 MMCMLVI -2957 MMCMLVII -2958 MMCMLVIII -2959 MMCMLIX -2960 MMCMLX -2961 MMCMLXI -2962 MMCMLXII -2963 MMCMLXIII -2964 MMCMLXIV -2965 MMCMLXV -2966 MMCMLXVI -2967 MMCMLXVII -2968 MMCMLXVIII -2969 MMCMLXIX -2970 MMCMLXX -2971 MMCMLXXI -2972 MMCMLXXII -2973 MMCMLXXIII -2974 MMCMLXXIV -2975 MMCMLXXV -2976 MMCMLXXVI -2977 MMCMLXXVII -2978 MMCMLXXVIII -2979 MMCMLXXIX -2980 MMCMLXXX -2981 MMCMLXXXI -2982 MMCMLXXXII -2983 MMCMLXXXIII -2984 MMCMLXXXIV -2985 MMCMLXXXV -2986 MMCMLXXXVI -2987 MMCMLXXXVII -2988 MMCMLXXXVIII -2989 MMCMLXXXIX -2990 MMCMXC -2991 MMCMXCI -2992 MMCMXCII -2993 MMCMXCIII -2994 MMCMXCIV -2995 MMCMXCV -2996 MMCMXCVI -2997 MMCMXCVII -2998 MMCMXCVIII -2999 MMCMXCIX -3000 MMM -3001 MMMI -3002 MMMII -3003 MMMIII -3004 MMMIV -3005 MMMV -3006 MMMVI -3007 MMMVII -3008 MMMVIII -3009 MMMIX -3010 MMMX -3011 MMMXI -3012 MMMXII -3013 MMMXIII -3014 MMMXIV -3015 MMMXV -3016 MMMXVI -3017 MMMXVII -3018 MMMXVIII -3019 MMMXIX -3020 MMMXX -3021 MMMXXI -3022 MMMXXII -3023 MMMXXIII -3024 MMMXXIV -3025 MMMXXV -3026 MMMXXVI -3027 MMMXXVII -3028 MMMXXVIII -3029 MMMXXIX -3030 MMMXXX -3031 MMMXXXI -3032 MMMXXXII -3033 MMMXXXIII -3034 MMMXXXIV -3035 MMMXXXV -3036 MMMXXXVI -3037 MMMXXXVII -3038 MMMXXXVIII -3039 MMMXXXIX -3040 MMMXL -3041 MMMXLI -3042 MMMXLII -3043 MMMXLIII -3044 MMMXLIV -3045 MMMXLV -3046 MMMXLVI -3047 MMMXLVII -3048 MMMXLVIII -3049 MMMXLIX -3050 MMML -3051 MMMLI -3052 MMMLII -3053 MMMLIII -3054 MMMLIV -3055 MMMLV -3056 MMMLVI -3057 MMMLVII -3058 MMMLVIII -3059 MMMLIX -3060 MMMLX -3061 MMMLXI -3062 MMMLXII -3063 MMMLXIII -3064 MMMLXIV -3065 MMMLXV -3066 MMMLXVI -3067 MMMLXVII -3068 MMMLXVIII -3069 MMMLXIX -3070 MMMLXX -3071 MMMLXXI -3072 MMMLXXII -3073 MMMLXXIII -3074 MMMLXXIV -3075 MMMLXXV -3076 MMMLXXVI -3077 MMMLXXVII -3078 MMMLXXVIII -3079 MMMLXXIX -3080 MMMLXXX -3081 MMMLXXXI -3082 MMMLXXXII -3083 MMMLXXXIII -3084 MMMLXXXIV -3085 MMMLXXXV -3086 MMMLXXXVI -3087 MMMLXXXVII -3088 MMMLXXXVIII -3089 MMMLXXXIX -3090 MMMXC -3091 MMMXCI -3092 MMMXCII -3093 MMMXCIII -3094 MMMXCIV -3095 MMMXCV -3096 MMMXCVI -3097 MMMXCVII -3098 MMMXCVIII -3099 MMMXCIX -3100 MMMC -3101 MMMCI -3102 MMMCII -3103 MMMCIII -3104 MMMCIV -3105 MMMCV -3106 MMMCVI -3107 MMMCVII -3108 MMMCVIII -3109 MMMCIX -3110 MMMCX -3111 MMMCXI -3112 MMMCXII -3113 MMMCXIII -3114 MMMCXIV -3115 MMMCXV -3116 MMMCXVI -3117 MMMCXVII -3118 MMMCXVIII -3119 MMMCXIX -3120 MMMCXX -3121 MMMCXXI -3122 MMMCXXII -3123 MMMCXXIII -3124 MMMCXXIV -3125 MMMCXXV -3126 MMMCXXVI -3127 MMMCXXVII -3128 MMMCXXVIII -3129 MMMCXXIX -3130 MMMCXXX -3131 MMMCXXXI -3132 MMMCXXXII -3133 MMMCXXXIII -3134 MMMCXXXIV -3135 MMMCXXXV -3136 MMMCXXXVI -3137 MMMCXXXVII -3138 MMMCXXXVIII -3139 MMMCXXXIX -3140 MMMCXL -3141 MMMCXLI -3142 MMMCXLII -3143 MMMCXLIII -3144 MMMCXLIV -3145 MMMCXLV -3146 MMMCXLVI -3147 MMMCXLVII -3148 MMMCXLVIII -3149 MMMCXLIX -3150 MMMCL -3151 MMMCLI -3152 MMMCLII -3153 MMMCLIII -3154 MMMCLIV -3155 MMMCLV -3156 MMMCLVI -3157 MMMCLVII -3158 MMMCLVIII -3159 MMMCLIX -3160 MMMCLX -3161 MMMCLXI -3162 MMMCLXII -3163 MMMCLXIII -3164 MMMCLXIV -3165 MMMCLXV -3166 MMMCLXVI -3167 MMMCLXVII -3168 MMMCLXVIII -3169 MMMCLXIX -3170 MMMCLXX -3171 MMMCLXXI -3172 MMMCLXXII -3173 MMMCLXXIII -3174 MMMCLXXIV -3175 MMMCLXXV -3176 MMMCLXXVI -3177 MMMCLXXVII -3178 MMMCLXXVIII -3179 MMMCLXXIX -3180 MMMCLXXX -3181 MMMCLXXXI -3182 MMMCLXXXII -3183 MMMCLXXXIII -3184 MMMCLXXXIV -3185 MMMCLXXXV -3186 MMMCLXXXVI -3187 MMMCLXXXVII -3188 MMMCLXXXVIII -3189 MMMCLXXXIX -3190 MMMCXC -3191 MMMCXCI -3192 MMMCXCII -3193 MMMCXCIII -3194 MMMCXCIV -3195 MMMCXCV -3196 MMMCXCVI -3197 MMMCXCVII -3198 MMMCXCVIII -3199 MMMCXCIX -3200 MMMCC -3201 MMMCCI -3202 MMMCCII -3203 MMMCCIII -3204 MMMCCIV -3205 MMMCCV -3206 MMMCCVI -3207 MMMCCVII -3208 MMMCCVIII -3209 MMMCCIX -3210 MMMCCX -3211 MMMCCXI -3212 MMMCCXII -3213 MMMCCXIII -3214 MMMCCXIV -3215 MMMCCXV -3216 MMMCCXVI -3217 MMMCCXVII -3218 MMMCCXVIII -3219 MMMCCXIX -3220 MMMCCXX -3221 MMMCCXXI -3222 MMMCCXXII -3223 MMMCCXXIII -3224 MMMCCXXIV -3225 MMMCCXXV -3226 MMMCCXXVI -3227 MMMCCXXVII -3228 MMMCCXXVIII -3229 MMMCCXXIX -3230 MMMCCXXX -3231 MMMCCXXXI -3232 MMMCCXXXII -3233 MMMCCXXXIII -3234 MMMCCXXXIV -3235 MMMCCXXXV -3236 MMMCCXXXVI -3237 MMMCCXXXVII -3238 MMMCCXXXVIII -3239 MMMCCXXXIX -3240 MMMCCXL -3241 MMMCCXLI -3242 MMMCCXLII -3243 MMMCCXLIII -3244 MMMCCXLIV -3245 MMMCCXLV -3246 MMMCCXLVI -3247 MMMCCXLVII -3248 MMMCCXLVIII -3249 MMMCCXLIX -3250 MMMCCL -3251 MMMCCLI -3252 MMMCCLII -3253 MMMCCLIII -3254 MMMCCLIV -3255 MMMCCLV -3256 MMMCCLVI -3257 MMMCCLVII -3258 MMMCCLVIII -3259 MMMCCLIX -3260 MMMCCLX -3261 MMMCCLXI -3262 MMMCCLXII -3263 MMMCCLXIII -3264 MMMCCLXIV -3265 MMMCCLXV -3266 MMMCCLXVI -3267 MMMCCLXVII -3268 MMMCCLXVIII -3269 MMMCCLXIX -3270 MMMCCLXX -3271 MMMCCLXXI -3272 MMMCCLXXII -3273 MMMCCLXXIII -3274 MMMCCLXXIV -3275 MMMCCLXXV -3276 MMMCCLXXVI -3277 MMMCCLXXVII -3278 MMMCCLXXVIII -3279 MMMCCLXXIX -3280 MMMCCLXXX -3281 MMMCCLXXXI -3282 MMMCCLXXXII -3283 MMMCCLXXXIII -3284 MMMCCLXXXIV -3285 MMMCCLXXXV -3286 MMMCCLXXXVI -3287 MMMCCLXXXVII -3288 MMMCCLXXXVIII -3289 MMMCCLXXXIX -3290 MMMCCXC -3291 MMMCCXCI -3292 MMMCCXCII -3293 MMMCCXCIII -3294 MMMCCXCIV -3295 MMMCCXCV -3296 MMMCCXCVI -3297 MMMCCXCVII -3298 MMMCCXCVIII -3299 MMMCCXCIX -3300 MMMCCC -3301 MMMCCCI -3302 MMMCCCII -3303 MMMCCCIII -3304 MMMCCCIV -3305 MMMCCCV -3306 MMMCCCVI -3307 MMMCCCVII -3308 MMMCCCVIII -3309 MMMCCCIX -3310 MMMCCCX -3311 MMMCCCXI -3312 MMMCCCXII -3313 MMMCCCXIII -3314 MMMCCCXIV -3315 MMMCCCXV -3316 MMMCCCXVI -3317 MMMCCCXVII -3318 MMMCCCXVIII -3319 MMMCCCXIX -3320 MMMCCCXX -3321 MMMCCCXXI -3322 MMMCCCXXII -3323 MMMCCCXXIII -3324 MMMCCCXXIV -3325 MMMCCCXXV -3326 MMMCCCXXVI -3327 MMMCCCXXVII -3328 MMMCCCXXVIII -3329 MMMCCCXXIX -3330 MMMCCCXXX -3331 MMMCCCXXXI -3332 MMMCCCXXXII -3333 MMMCCCXXXIII -3334 MMMCCCXXXIV -3335 MMMCCCXXXV -3336 MMMCCCXXXVI -3337 MMMCCCXXXVII -3338 MMMCCCXXXVIII -3339 MMMCCCXXXIX -3340 MMMCCCXL -3341 MMMCCCXLI -3342 MMMCCCXLII -3343 MMMCCCXLIII -3344 MMMCCCXLIV -3345 MMMCCCXLV -3346 MMMCCCXLVI -3347 MMMCCCXLVII -3348 MMMCCCXLVIII -3349 MMMCCCXLIX -3350 MMMCCCL -3351 MMMCCCLI -3352 MMMCCCLII -3353 MMMCCCLIII -3354 MMMCCCLIV -3355 MMMCCCLV -3356 MMMCCCLVI -3357 MMMCCCLVII -3358 MMMCCCLVIII -3359 MMMCCCLIX -3360 MMMCCCLX -3361 MMMCCCLXI -3362 MMMCCCLXII -3363 MMMCCCLXIII -3364 MMMCCCLXIV -3365 MMMCCCLXV -3366 MMMCCCLXVI -3367 MMMCCCLXVII -3368 MMMCCCLXVIII -3369 MMMCCCLXIX -3370 MMMCCCLXX -3371 MMMCCCLXXI -3372 MMMCCCLXXII -3373 MMMCCCLXXIII -3374 MMMCCCLXXIV -3375 MMMCCCLXXV -3376 MMMCCCLXXVI -3377 MMMCCCLXXVII -3378 MMMCCCLXXVIII -3379 MMMCCCLXXIX -3380 MMMCCCLXXX -3381 MMMCCCLXXXI -3382 MMMCCCLXXXII -3383 MMMCCCLXXXIII -3384 MMMCCCLXXXIV -3385 MMMCCCLXXXV -3386 MMMCCCLXXXVI -3387 MMMCCCLXXXVII -3388 MMMCCCLXXXVIII -3389 MMMCCCLXXXIX -3390 MMMCCCXC -3391 MMMCCCXCI -3392 MMMCCCXCII -3393 MMMCCCXCIII -3394 MMMCCCXCIV -3395 MMMCCCXCV -3396 MMMCCCXCVI -3397 MMMCCCXCVII -3398 MMMCCCXCVIII -3399 MMMCCCXCIX -3400 MMMCD -3401 MMMCDI -3402 MMMCDII -3403 MMMCDIII -3404 MMMCDIV -3405 MMMCDV -3406 MMMCDVI -3407 MMMCDVII -3408 MMMCDVIII -3409 MMMCDIX -3410 MMMCDX -3411 MMMCDXI -3412 MMMCDXII -3413 MMMCDXIII -3414 MMMCDXIV -3415 MMMCDXV -3416 MMMCDXVI -3417 MMMCDXVII -3418 MMMCDXVIII -3419 MMMCDXIX -3420 MMMCDXX -3421 MMMCDXXI -3422 MMMCDXXII -3423 MMMCDXXIII -3424 MMMCDXXIV -3425 MMMCDXXV -3426 MMMCDXXVI -3427 MMMCDXXVII -3428 MMMCDXXVIII -3429 MMMCDXXIX -3430 MMMCDXXX -3431 MMMCDXXXI -3432 MMMCDXXXII -3433 MMMCDXXXIII -3434 MMMCDXXXIV -3435 MMMCDXXXV -3436 MMMCDXXXVI -3437 MMMCDXXXVII -3438 MMMCDXXXVIII -3439 MMMCDXXXIX -3440 MMMCDXL -3441 MMMCDXLI -3442 MMMCDXLII -3443 MMMCDXLIII -3444 MMMCDXLIV -3445 MMMCDXLV -3446 MMMCDXLVI -3447 MMMCDXLVII -3448 MMMCDXLVIII -3449 MMMCDXLIX -3450 MMMCDL -3451 MMMCDLI -3452 MMMCDLII -3453 MMMCDLIII -3454 MMMCDLIV -3455 MMMCDLV -3456 MMMCDLVI -3457 MMMCDLVII -3458 MMMCDLVIII -3459 MMMCDLIX -3460 MMMCDLX -3461 MMMCDLXI -3462 MMMCDLXII -3463 MMMCDLXIII -3464 MMMCDLXIV -3465 MMMCDLXV -3466 MMMCDLXVI -3467 MMMCDLXVII -3468 MMMCDLXVIII -3469 MMMCDLXIX -3470 MMMCDLXX -3471 MMMCDLXXI -3472 MMMCDLXXII -3473 MMMCDLXXIII -3474 MMMCDLXXIV -3475 MMMCDLXXV -3476 MMMCDLXXVI -3477 MMMCDLXXVII -3478 MMMCDLXXVIII -3479 MMMCDLXXIX -3480 MMMCDLXXX -3481 MMMCDLXXXI -3482 MMMCDLXXXII -3483 MMMCDLXXXIII -3484 MMMCDLXXXIV -3485 MMMCDLXXXV -3486 MMMCDLXXXVI -3487 MMMCDLXXXVII -3488 MMMCDLXXXVIII -3489 MMMCDLXXXIX -3490 MMMCDXC -3491 MMMCDXCI -3492 MMMCDXCII -3493 MMMCDXCIII -3494 MMMCDXCIV -3495 MMMCDXCV -3496 MMMCDXCVI -3497 MMMCDXCVII -3498 MMMCDXCVIII -3499 MMMCDXCIX -3500 MMMD -3501 MMMDI -3502 MMMDII -3503 MMMDIII -3504 MMMDIV -3505 MMMDV -3506 MMMDVI -3507 MMMDVII -3508 MMMDVIII -3509 MMMDIX -3510 MMMDX -3511 MMMDXI -3512 MMMDXII -3513 MMMDXIII -3514 MMMDXIV -3515 MMMDXV -3516 MMMDXVI -3517 MMMDXVII -3518 MMMDXVIII -3519 MMMDXIX -3520 MMMDXX -3521 MMMDXXI -3522 MMMDXXII -3523 MMMDXXIII -3524 MMMDXXIV -3525 MMMDXXV -3526 MMMDXXVI -3527 MMMDXXVII -3528 MMMDXXVIII -3529 MMMDXXIX -3530 MMMDXXX -3531 MMMDXXXI -3532 MMMDXXXII -3533 MMMDXXXIII -3534 MMMDXXXIV -3535 MMMDXXXV -3536 MMMDXXXVI -3537 MMMDXXXVII -3538 MMMDXXXVIII -3539 MMMDXXXIX -3540 MMMDXL -3541 MMMDXLI -3542 MMMDXLII -3543 MMMDXLIII -3544 MMMDXLIV -3545 MMMDXLV -3546 MMMDXLVI -3547 MMMDXLVII -3548 MMMDXLVIII -3549 MMMDXLIX -3550 MMMDL -3551 MMMDLI -3552 MMMDLII -3553 MMMDLIII -3554 MMMDLIV -3555 MMMDLV -3556 MMMDLVI -3557 MMMDLVII -3558 MMMDLVIII -3559 MMMDLIX -3560 MMMDLX -3561 MMMDLXI -3562 MMMDLXII -3563 MMMDLXIII -3564 MMMDLXIV -3565 MMMDLXV -3566 MMMDLXVI -3567 MMMDLXVII -3568 MMMDLXVIII -3569 MMMDLXIX -3570 MMMDLXX -3571 MMMDLXXI -3572 MMMDLXXII -3573 MMMDLXXIII -3574 MMMDLXXIV -3575 MMMDLXXV -3576 MMMDLXXVI -3577 MMMDLXXVII -3578 MMMDLXXVIII -3579 MMMDLXXIX -3580 MMMDLXXX -3581 MMMDLXXXI -3582 MMMDLXXXII -3583 MMMDLXXXIII -3584 MMMDLXXXIV -3585 MMMDLXXXV -3586 MMMDLXXXVI -3587 MMMDLXXXVII -3588 MMMDLXXXVIII -3589 MMMDLXXXIX -3590 MMMDXC -3591 MMMDXCI -3592 MMMDXCII -3593 MMMDXCIII -3594 MMMDXCIV -3595 MMMDXCV -3596 MMMDXCVI -3597 MMMDXCVII -3598 MMMDXCVIII -3599 MMMDXCIX -3600 MMMDC -3601 MMMDCI -3602 MMMDCII -3603 MMMDCIII -3604 MMMDCIV -3605 MMMDCV -3606 MMMDCVI -3607 MMMDCVII -3608 MMMDCVIII -3609 MMMDCIX -3610 MMMDCX -3611 MMMDCXI -3612 MMMDCXII -3613 MMMDCXIII -3614 MMMDCXIV -3615 MMMDCXV -3616 MMMDCXVI -3617 MMMDCXVII -3618 MMMDCXVIII -3619 MMMDCXIX -3620 MMMDCXX -3621 MMMDCXXI -3622 MMMDCXXII -3623 MMMDCXXIII -3624 MMMDCXXIV -3625 MMMDCXXV -3626 MMMDCXXVI -3627 MMMDCXXVII -3628 MMMDCXXVIII -3629 MMMDCXXIX -3630 MMMDCXXX -3631 MMMDCXXXI -3632 MMMDCXXXII -3633 MMMDCXXXIII -3634 MMMDCXXXIV -3635 MMMDCXXXV -3636 MMMDCXXXVI -3637 MMMDCXXXVII -3638 MMMDCXXXVIII -3639 MMMDCXXXIX -3640 MMMDCXL -3641 MMMDCXLI -3642 MMMDCXLII -3643 MMMDCXLIII -3644 MMMDCXLIV -3645 MMMDCXLV -3646 MMMDCXLVI -3647 MMMDCXLVII -3648 MMMDCXLVIII -3649 MMMDCXLIX -3650 MMMDCL -3651 MMMDCLI -3652 MMMDCLII -3653 MMMDCLIII -3654 MMMDCLIV -3655 MMMDCLV -3656 MMMDCLVI -3657 MMMDCLVII -3658 MMMDCLVIII -3659 MMMDCLIX -3660 MMMDCLX -3661 MMMDCLXI -3662 MMMDCLXII -3663 MMMDCLXIII -3664 MMMDCLXIV -3665 MMMDCLXV -3666 MMMDCLXVI -3667 MMMDCLXVII -3668 MMMDCLXVIII -3669 MMMDCLXIX -3670 MMMDCLXX -3671 MMMDCLXXI -3672 MMMDCLXXII -3673 MMMDCLXXIII -3674 MMMDCLXXIV -3675 MMMDCLXXV -3676 MMMDCLXXVI -3677 MMMDCLXXVII -3678 MMMDCLXXVIII -3679 MMMDCLXXIX -3680 MMMDCLXXX -3681 MMMDCLXXXI -3682 MMMDCLXXXII -3683 MMMDCLXXXIII -3684 MMMDCLXXXIV -3685 MMMDCLXXXV -3686 MMMDCLXXXVI -3687 MMMDCLXXXVII -3688 MMMDCLXXXVIII -3689 MMMDCLXXXIX -3690 MMMDCXC -3691 MMMDCXCI -3692 MMMDCXCII -3693 MMMDCXCIII -3694 MMMDCXCIV -3695 MMMDCXCV -3696 MMMDCXCVI -3697 MMMDCXCVII -3698 MMMDCXCVIII -3699 MMMDCXCIX -3700 MMMDCC -3701 MMMDCCI -3702 MMMDCCII -3703 MMMDCCIII -3704 MMMDCCIV -3705 MMMDCCV -3706 MMMDCCVI -3707 MMMDCCVII -3708 MMMDCCVIII -3709 MMMDCCIX -3710 MMMDCCX -3711 MMMDCCXI -3712 MMMDCCXII -3713 MMMDCCXIII -3714 MMMDCCXIV -3715 MMMDCCXV -3716 MMMDCCXVI -3717 MMMDCCXVII -3718 MMMDCCXVIII -3719 MMMDCCXIX -3720 MMMDCCXX -3721 MMMDCCXXI -3722 MMMDCCXXII -3723 MMMDCCXXIII -3724 MMMDCCXXIV -3725 MMMDCCXXV -3726 MMMDCCXXVI -3727 MMMDCCXXVII -3728 MMMDCCXXVIII -3729 MMMDCCXXIX -3730 MMMDCCXXX -3731 MMMDCCXXXI -3732 MMMDCCXXXII -3733 MMMDCCXXXIII -3734 MMMDCCXXXIV -3735 MMMDCCXXXV -3736 MMMDCCXXXVI -3737 MMMDCCXXXVII -3738 MMMDCCXXXVIII -3739 MMMDCCXXXIX -3740 MMMDCCXL -3741 MMMDCCXLI -3742 MMMDCCXLII -3743 MMMDCCXLIII -3744 MMMDCCXLIV -3745 MMMDCCXLV -3746 MMMDCCXLVI -3747 MMMDCCXLVII -3748 MMMDCCXLVIII -3749 MMMDCCXLIX -3750 MMMDCCL -3751 MMMDCCLI -3752 MMMDCCLII -3753 MMMDCCLIII -3754 MMMDCCLIV -3755 MMMDCCLV -3756 MMMDCCLVI -3757 MMMDCCLVII -3758 MMMDCCLVIII -3759 MMMDCCLIX -3760 MMMDCCLX -3761 MMMDCCLXI -3762 MMMDCCLXII -3763 MMMDCCLXIII -3764 MMMDCCLXIV -3765 MMMDCCLXV -3766 MMMDCCLXVI -3767 MMMDCCLXVII -3768 MMMDCCLXVIII -3769 MMMDCCLXIX -3770 MMMDCCLXX -3771 MMMDCCLXXI -3772 MMMDCCLXXII -3773 MMMDCCLXXIII -3774 MMMDCCLXXIV -3775 MMMDCCLXXV -3776 MMMDCCLXXVI -3777 MMMDCCLXXVII -3778 MMMDCCLXXVIII -3779 MMMDCCLXXIX -3780 MMMDCCLXXX -3781 MMMDCCLXXXI -3782 MMMDCCLXXXII -3783 MMMDCCLXXXIII -3784 MMMDCCLXXXIV -3785 MMMDCCLXXXV -3786 MMMDCCLXXXVI -3787 MMMDCCLXXXVII -3788 MMMDCCLXXXVIII -3789 MMMDCCLXXXIX -3790 MMMDCCXC -3791 MMMDCCXCI -3792 MMMDCCXCII -3793 MMMDCCXCIII -3794 MMMDCCXCIV -3795 MMMDCCXCV -3796 MMMDCCXCVI -3797 MMMDCCXCVII -3798 MMMDCCXCVIII -3799 MMMDCCXCIX -3800 MMMDCCC -3801 MMMDCCCI -3802 MMMDCCCII -3803 MMMDCCCIII -3804 MMMDCCCIV -3805 MMMDCCCV -3806 MMMDCCCVI -3807 MMMDCCCVII -3808 MMMDCCCVIII -3809 MMMDCCCIX -3810 MMMDCCCX -3811 MMMDCCCXI -3812 MMMDCCCXII -3813 MMMDCCCXIII -3814 MMMDCCCXIV -3815 MMMDCCCXV -3816 MMMDCCCXVI -3817 MMMDCCCXVII -3818 MMMDCCCXVIII -3819 MMMDCCCXIX -3820 MMMDCCCXX -3821 MMMDCCCXXI -3822 MMMDCCCXXII -3823 MMMDCCCXXIII -3824 MMMDCCCXXIV -3825 MMMDCCCXXV -3826 MMMDCCCXXVI -3827 MMMDCCCXXVII -3828 MMMDCCCXXVIII -3829 MMMDCCCXXIX -3830 MMMDCCCXXX -3831 MMMDCCCXXXI -3832 MMMDCCCXXXII -3833 MMMDCCCXXXIII -3834 MMMDCCCXXXIV -3835 MMMDCCCXXXV -3836 MMMDCCCXXXVI -3837 MMMDCCCXXXVII -3838 MMMDCCCXXXVIII -3839 MMMDCCCXXXIX -3840 MMMDCCCXL -3841 MMMDCCCXLI -3842 MMMDCCCXLII -3843 MMMDCCCXLIII -3844 MMMDCCCXLIV -3845 MMMDCCCXLV -3846 MMMDCCCXLVI -3847 MMMDCCCXLVII -3848 MMMDCCCXLVIII -3849 MMMDCCCXLIX -3850 MMMDCCCL -3851 MMMDCCCLI -3852 MMMDCCCLII -3853 MMMDCCCLIII -3854 MMMDCCCLIV -3855 MMMDCCCLV -3856 MMMDCCCLVI -3857 MMMDCCCLVII -3858 MMMDCCCLVIII -3859 MMMDCCCLIX -3860 MMMDCCCLX -3861 MMMDCCCLXI -3862 MMMDCCCLXII -3863 MMMDCCCLXIII -3864 MMMDCCCLXIV -3865 MMMDCCCLXV -3866 MMMDCCCLXVI -3867 MMMDCCCLXVII -3868 MMMDCCCLXVIII -3869 MMMDCCCLXIX -3870 MMMDCCCLXX -3871 MMMDCCCLXXI -3872 MMMDCCCLXXII -3873 MMMDCCCLXXIII -3874 MMMDCCCLXXIV -3875 MMMDCCCLXXV -3876 MMMDCCCLXXVI -3877 MMMDCCCLXXVII -3878 MMMDCCCLXXVIII -3879 MMMDCCCLXXIX -3880 MMMDCCCLXXX -3881 MMMDCCCLXXXI -3882 MMMDCCCLXXXII -3883 MMMDCCCLXXXIII -3884 MMMDCCCLXXXIV -3885 MMMDCCCLXXXV -3886 MMMDCCCLXXXVI -3887 MMMDCCCLXXXVII -3888 MMMDCCCLXXXVIII -3889 MMMDCCCLXXXIX -3890 MMMDCCCXC -3891 MMMDCCCXCI -3892 MMMDCCCXCII -3893 MMMDCCCXCIII -3894 MMMDCCCXCIV -3895 MMMDCCCXCV -3896 MMMDCCCXCVI -3897 MMMDCCCXCVII -3898 MMMDCCCXCVIII -3899 MMMDCCCXCIX -3900 MMMCM -3901 MMMCMI -3902 MMMCMII -3903 MMMCMIII -3904 MMMCMIV -3905 MMMCMV -3906 MMMCMVI -3907 MMMCMVII -3908 MMMCMVIII -3909 MMMCMIX -3910 MMMCMX -3911 MMMCMXI -3912 MMMCMXII -3913 MMMCMXIII -3914 MMMCMXIV -3915 MMMCMXV -3916 MMMCMXVI -3917 MMMCMXVII -3918 MMMCMXVIII -3919 MMMCMXIX -3920 MMMCMXX -3921 MMMCMXXI -3922 MMMCMXXII -3923 MMMCMXXIII -3924 MMMCMXXIV -3925 MMMCMXXV -3926 MMMCMXXVI -3927 MMMCMXXVII -3928 MMMCMXXVIII -3929 MMMCMXXIX -3930 MMMCMXXX -3931 MMMCMXXXI -3932 MMMCMXXXII -3933 MMMCMXXXIII -3934 MMMCMXXXIV -3935 MMMCMXXXV -3936 MMMCMXXXVI -3937 MMMCMXXXVII -3938 MMMCMXXXVIII -3939 MMMCMXXXIX -3940 MMMCMXL -3941 MMMCMXLI -3942 MMMCMXLII -3943 MMMCMXLIII -3944 MMMCMXLIV -3945 MMMCMXLV -3946 MMMCMXLVI -3947 MMMCMXLVII -3948 MMMCMXLVIII -3949 MMMCMXLIX -3950 MMMCML -3951 MMMCMLI -3952 MMMCMLII -3953 MMMCMLIII -3954 MMMCMLIV -3955 MMMCMLV -3956 MMMCMLVI -3957 MMMCMLVII -3958 MMMCMLVIII -3959 MMMCMLIX -3960 MMMCMLX -3961 MMMCMLXI -3962 MMMCMLXII -3963 MMMCMLXIII -3964 MMMCMLXIV -3965 MMMCMLXV -3966 MMMCMLXVI -3967 MMMCMLXVII -3968 MMMCMLXVIII -3969 MMMCMLXIX -3970 MMMCMLXX -3971 MMMCMLXXI -3972 MMMCMLXXII -3973 MMMCMLXXIII -3974 MMMCMLXXIV -3975 MMMCMLXXV -3976 MMMCMLXXVI -3977 MMMCMLXXVII -3978 MMMCMLXXVIII -3979 MMMCMLXXIX -3980 MMMCMLXXX -3981 MMMCMLXXXI -3982 MMMCMLXXXII -3983 MMMCMLXXXIII -3984 MMMCMLXXXIV -3985 MMMCMLXXXV -3986 MMMCMLXXXVI -3987 MMMCMLXXXVII -3988 MMMCMLXXXVIII -3989 MMMCMLXXXIX -3990 MMMCMXC -3991 MMMCMXCI -3992 MMMCMXCII -3993 MMMCMXCIII -3994 MMMCMXCIV -3995 MMMCMXCV -3996 MMMCMXCVI -3997 MMMCMXCVII -3998 MMMCMXCVIII -3999 MMMCMXCIX \ No newline at end of file diff --git a/src/test/resources/configuration/fake_config.yml b/src/test/resources/configuration/fake_config.yml new file mode 100644 index 00000000..bf39ef68 --- /dev/null +++ b/src/test/resources/configuration/fake_config.yml @@ -0,0 +1 @@ +test: withParent diff --git a/src/test/resources/fake_config.yml b/src/test/resources/fake_config.yml new file mode 100644 index 00000000..3c2d5a08 --- /dev/null +++ b/src/test/resources/fake_config.yml @@ -0,0 +1 @@ +test: withoutParent diff --git a/src/test/resources/map_image.png b/src/test/resources/map_image.png deleted file mode 100644 index c0a4aae4..00000000 Binary files a/src/test/resources/map_image.png and /dev/null differ diff --git a/src/test/resources/server.conf b/src/test/resources/server.conf deleted file mode 100644 index 2645bcaf..00000000 --- a/src/test/resources/server.conf +++ /dev/null @@ -1,13 +0,0 @@ -server { - port: 25565 - world: world - onlineMode: false - velocity: { - enabled: false - secret: "" - } - bungeeCord: { - enabled: false - secrets: [] - } -} \ No newline at end of file diff --git a/src/test/resources/test_bundle.properties b/src/test/resources/test_bundle.properties index 608302c4..78c88bf7 100644 --- a/src/test/resources/test_bundle.properties +++ b/src/test/resources/test_bundle.properties @@ -1,5 +1,5 @@ -test_plural=Need {0} {0, plural, =0 {player} =1 {player} other {players}}. +test_plural= test1=default_value_1 test2=default_value_2 test_args=default_value {0} -test_undefined=default_value \ No newline at end of file +test_undefined=default_value diff --git a/src/test/resources/test_bundle_de_DE.properties b/src/test/resources/test_bundle_de_DE.properties new file mode 100644 index 00000000..60ff99b7 --- /dev/null +++ b/src/test/resources/test_bundle_de_DE.properties @@ -0,0 +1,3 @@ +test1=deutsch_value_1 +test2=deutsch_value_2 +test_args=deutsch_value {0} diff --git a/src/test/resources/test_bundle_es_ES.properties b/src/test/resources/test_bundle_es_ES.properties new file mode 100644 index 00000000..56fa3743 --- /dev/null +++ b/src/test/resources/test_bundle_es_ES.properties @@ -0,0 +1,3 @@ +test1=español_value_1 +test2=español_value_2 +test_args=español_value {0} diff --git a/src/test/resources/test_bundle_fr_FR.properties b/src/test/resources/test_bundle_fr_FR.properties index 6ef50e69..b041caee 100644 --- a/src/test/resources/test_bundle_fr_FR.properties +++ b/src/test/resources/test_bundle_fr_FR.properties @@ -1,4 +1,3 @@ -test_plural=Besoin de {0} {0, plural, =0 {joueur} =1 {joueur} other {joueurs}}. test1=français_value_1 test2=français_value_2 -test_args=français_value {0} \ No newline at end of file +test_args=français_value {0} diff --git a/src/test/resources/test_bundle_zh_CN.properties b/src/test/resources/test_bundle_zh_CN.properties new file mode 100644 index 00000000..46a3d95e --- /dev/null +++ b/src/test/resources/test_bundle_zh_CN.properties @@ -0,0 +1,3 @@ +test1=chinese_value_1 +test2=chinese_value_2 +test_args=chinese_value {0} diff --git a/src/test/resources/world/DIM-1/data/raids.dat b/src/test/resources/world/DIM-1/data/raids.dat deleted file mode 100644 index 4b510ab4..00000000 Binary files a/src/test/resources/world/DIM-1/data/raids.dat and /dev/null differ diff --git a/src/test/resources/world/DIM1/data/raids_end.dat b/src/test/resources/world/DIM1/data/raids_end.dat deleted file mode 100644 index 4b510ab4..00000000 Binary files a/src/test/resources/world/DIM1/data/raids_end.dat and /dev/null differ diff --git a/src/test/resources/world/data/raids.dat b/src/test/resources/world/data/raids.dat deleted file mode 100644 index 4b510ab4..00000000 Binary files a/src/test/resources/world/data/raids.dat and /dev/null differ diff --git a/src/test/resources/world/entities/r.-1.-1.mca b/src/test/resources/world/entities/r.-1.-1.mca deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/world/entities/r.-1.0.mca b/src/test/resources/world/entities/r.-1.0.mca deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/world/entities/r.0.-1.mca b/src/test/resources/world/entities/r.0.-1.mca deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/world/entities/r.0.0.mca b/src/test/resources/world/entities/r.0.0.mca deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/world/level.dat b/src/test/resources/world/level.dat deleted file mode 100644 index d9af5dfd..00000000 Binary files a/src/test/resources/world/level.dat and /dev/null differ diff --git a/src/test/resources/world/level.dat_old b/src/test/resources/world/level.dat_old deleted file mode 100644 index 822cb27b..00000000 Binary files a/src/test/resources/world/level.dat_old and /dev/null differ diff --git a/src/test/resources/world/region/r.-1.-1.mca b/src/test/resources/world/region/r.-1.-1.mca deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/world/region/r.-1.0.mca b/src/test/resources/world/region/r.-1.0.mca deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/world/region/r.0.-1.mca b/src/test/resources/world/region/r.0.-1.mca deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/world/region/r.0.0.mca b/src/test/resources/world/region/r.0.0.mca deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/world/session.lock b/src/test/resources/world/session.lock deleted file mode 100644 index 0d7e5f85..00000000 --- a/src/test/resources/world/session.lock +++ /dev/null @@ -1 +0,0 @@ -☃ \ No newline at end of file