Merge branch 'MV5' into pr/2975

# Conflicts:
#	src/main/java/com/onarandombox/MultiverseCore/world/SimpleMVWorldManager.java
This commit is contained in:
Ben Woo 2023-09-01 23:28:05 +08:00
commit 55aac81258
No known key found for this signature in database
GPG Key ID: FB2A3645536E12C8
66 changed files with 2282 additions and 366 deletions

2
.github/labeler.yml vendored
View File

@ -1,2 +0,0 @@
prerelease:
- '*'

View File

@ -0,0 +1,34 @@
name: 'Call: GitHub Release'
on:
workflow_call:
inputs:
release_mode:
description: 'Release mode'
required: true
type: string
version_bump:
description: 'Version bump'
required: false
type: string
promote_from:
description: 'Promote from'
required: false
type: string
outputs:
release_created:
description: 'Release created'
value: ${{ jobs.github_release.outputs.release_created }}
tag_name:
description: 'Tag name'
value: ${{ jobs.github_release.outputs.tag_name }}
jobs:
github_release:
uses: ./.github/workflows/generic.github_release.yml
secrets: inherit
with:
plugin_name: multiverse-core
release_mode: ${{ inputs.release_mode }}
version_bump: ${{ inputs.version_bump }}
promote_from: ${{ inputs.promote_from }}

View File

@ -0,0 +1,83 @@
name: 'Call: Platform Uploads'
on:
workflow_call:
inputs:
target_tag:
description: 'Version to upload'
required: true
type: string
upload_modrinth:
description: 'Upload to modrinth.com'
required: true
type: string
upload_dbo:
description: 'Upload to dev.bukkit.org'
required: true
type: string
upload_hangar:
description: 'Upload to hangar.papermc.io'
required: true
type: string
secrets:
MODRINTH_TOKEN:
required: true
DBO_UPLOAD_API_TOKEN:
required: true
HANGAR_UPLOAD_TOKEN:
required: true
jobs:
platform_uploads:
uses: ./.github/workflows/generic.platform_uploads.yml
secrets: inherit
with:
plugin_name: multiverse-core
modrinth_project_id: 3wmN97b8
modrinth_dependencies: >
[
{"project_id": "qvdtDX3s", "dependency_type": "optional"},
{"project_id": "8VMk6P0I", "dependency_type": "optional"},
{"project_id": "vtawPsTo", "dependency_type": "optional"},
{"project_id": "WuErDeI1", "dependency_type": "optional"}
]
dbo_project_id: 30765
dbo_project_relations: >
[
{"slug": "multiverse-inventories", "type": "optionalDependency"},
{"slug": "multiverse-portals", "type": "optionalDependency"},
{"slug": "multiverse-netherportals", "type": "optionalDependency"},
{"slug": "multiverse-signportals", "type": "optionalDependency"},
{"slug": "vault", "type": "optionalDependency"}
]
hangar_slug: Multiverse-Core
hangar_plugin_dependencies: >
{ "PAPER": [
{
"name": "Multiverse-Inventories",
"required": false,
"platforms": ["PAPER"]
},
{
"name": "Multiverse-Portals",
"required": false,
"platforms": ["PAPER"]
},
{
"name": "Multiverse-NetherPortals",
"required": false,
"platforms": ["PAPER"]
},
{
"name": "Multiverse-SignPortals",
"required": false,
"platforms": ["PAPER"]
}
]}
target_tag: ${{ inputs.target_tag }}
upload_modrinth: ${{ inputs.upload_modrinth }}
upload_dbo: ${{ inputs.upload_dbo }}
upload_hangar: ${{ inputs.upload_hangar }}

View File

@ -0,0 +1,31 @@
name: 'Dispatch: Platform Uploads'
on:
workflow_dispatch:
inputs:
target_tag:
description: 'Version to upload'
required: true
type: string
upload_modrinth:
description: 'Upload to modrinth.com'
required: true
type: boolean
upload_dbo:
description: 'Upload to dev.bukkit.org'
required: true
type: boolean
upload_hangar:
description: 'Upload to hangar.papermc.io'
required: true
type: boolean
jobs:
dispatch_platform_uploads:
uses: ./.github/workflows/call.platform_uploads.yml
secrets: inherit
with:
target_tag: ${{ github.event.inputs.target_tag }}
upload_modrinth: ${{ github.event.inputs.upload_modrinth }}
upload_dbo: ${{ github.event.inputs.upload_dbo }}
upload_hangar: ${{ github.event.inputs.upload_hangar }}

View File

@ -0,0 +1,38 @@
name: 'Dispatch: Promote Release'
on:
workflow_dispatch:
inputs:
target_tag:
description: 'Version to promote'
required: true
jobs:
check_version:
runs-on: ubuntu-latest
steps:
- name: Verify input version is prerelease
run: |
if [[ "${{ github.event.inputs.target_tag }}" != *"pre"* ]]; then
echo "Version must be a prerelease"
exit 1
fi
github_release:
needs: check_version
uses: ./.github/workflows/call.github_release.yml
secrets: inherit
with:
release_mode: promote
promote_from: ${{ github.event.inputs.target_tag }}
platform_uploads:
needs: github_release
if: needs.github_release.outputs.release_created == 'true'
uses: ./.github/workflows/call.platform_uploads.yml
secrets: inherit
with:
target_tag: ${{ needs.github_release.outputs.tag_name }}
upload_modrinth: 'true'
upload_dbo: 'true'
upload_hangar: 'true'

View File

@ -0,0 +1,91 @@
name: 'Generic: GitHub Release'
on:
workflow_call:
inputs:
# Plugin specific
plugin_name:
description: 'Plugin name'
required: true
type: string
# Common params
release_mode:
description: 'Release mode'
required: true
type: string
version_bump:
description: 'Version bump'
required: false
type: string
promote_from:
description: 'Promote from'
required: false
type: string
outputs:
release_created:
description: 'Release created'
value: ${{ jobs.github_release.outputs.release_created }}
tag_name:
description: 'Tag name'
value: ${{ jobs.github_release.outputs.tag_name }}
jobs:
github_release:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- name: Echo inputs
run: |
echo "release_mode: ${{ inputs.release_mode }}"
echo "version_bump: ${{ inputs.version_bump }}"
echo "promote_from: ${{ inputs.promote_from }}"
- uses: actions/checkout@v3
with:
ref: ${{ inputs.promote_from }}
- uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt'
cache: gradle
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build and test
uses: gradle/gradle-build-action@v2
with:
arguments: clean build -x assemble -x shadowJar -x checkStyleMain -x checkStyleTest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release
id: release
uses: benwoo1110/semantic-release-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
release_mode: ${{ inputs.release_mode }}
version_bump: ${{ inputs.version_bump }}
promote_from: ${{ inputs.promote_from }}
- name: Publish package
if: steps.release.outputs.release_created == 'true'
uses: gradle/gradle-build-action@v2
with:
arguments: publish -x checkStyleMain -x checkStyleTest -x test
env:
GITHUB_VERSION: ${{ steps.release.outputs.publish_version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release artifact
if: steps.release.outputs.release_created == 'true'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/libs/${{ inputs.plugin_name }}-${{ steps.release.outputs.publish_version }}.jar
asset_name: ${{ inputs.plugin_name }}-${{ steps.release.outputs.tag_name }}.jar
tag: ${{ steps.release.outputs.tag_name }}

View File

@ -0,0 +1,140 @@
name: 'Generic: Platform Uploads'
on:
workflow_call:
inputs:
# Plugin specific params
plugin_name:
description: 'Plugin name'
required: true
type: string
modrinth_project_id:
description: 'modrinth.com project ID'
required: true
type: string
modrinth_dependencies:
description: 'modrinth.com project dependencies'
required: false
type: string
default: '[]'
dbo_project_id:
description: 'dev.bukkit.org project ID'
required: true
type: string
dbo_project_relations:
description: 'dev.bukkit.org project relations'
required: false
type: string
default: '[]'
hangar_slug:
description: 'hangar.papermc.io project slug'
required: true
type: string
hangar_plugin_dependencies:
description: 'hangar.papermc.io project dependencies'
required: false
type: string
default: '{}'
# Common params
target_tag:
description: 'Version to upload'
required: true
type: string
upload_modrinth:
description: 'Upload to modrinth.com'
required: true
type: string
upload_dbo:
description: 'Upload to dev.bukkit.org'
required: true
type: string
upload_hangar:
description: 'Upload to hangar.papermc.io'
required: true
type: string
secrets:
MODRINTH_TOKEN:
required: true
DBO_UPLOAD_API_TOKEN:
required: true
HANGAR_UPLOAD_TOKEN:
required: true
jobs:
platform_uploads:
runs-on: ubuntu-latest
steps:
- name: Get release info
id: release-info
uses: cardinalby/git-get-release-action@1.2.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ inputs.target_tag }}
- name: Download release artifact
id: release-artifact
uses: dsaltares/fetch-gh-release-asset@1.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: tags/${{ steps.release-info.outputs.tag_name }}
file: ${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar
- name: Parse release type
id: parse-release-type
run: |
if [[ "${{ steps.release-info.outputs.prerelease }}" == "true" ]]; then
echo Setting release_type to Beta
echo "release_type=Beta" >> $GITHUB_OUTPUT
else
echo Setting release_type to Release
echo "release_type=Release" >> $GITHUB_OUTPUT
fi
- name: Upload to Modrinth
if: ${{ !cancelled() && inputs.upload_modrinth == 'true' }}
uses: benwoo1110/modrinth-upload-action@v1
with:
api_token: ${{ secrets.MODRINTH_TOKEN }}
project_id: ${{ inputs.modrinth_project_id }}
version_number: ${{ steps.release-info.outputs.tag_name }}
files: '["${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar"]'
name: ${{ steps.release-info.outputs.tag_name }}
changelog: ${{ steps.release-artifact.outputs.body }}
game_versions: 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2, 1.18.1, 1.18, 1.17.1, 1.17, 1.16.5, 1.16.4, 1.16.3, 1.16.2, 1.16.1, 1.16, 1.15.2, 1.15.1, 1.15, 1.14.4, 1.14.3, 1.14.2, 1.14.1, 1.14, 1.13.2, 1.13.1, 1.13
version_type: ${{ steps.parse-release-type.outputs.release_type }}
loaders: bukkit, spigot, paper
dependencies: ${{ inputs.modrinth_dependencies }}
- name: Upload to DBO
if: ${{ !cancelled() && inputs.upload_dbo == 'true' }}
uses: benwoo1110/dbo-upload-action@v1
with:
api_token: ${{ secrets.DBO_UPLOAD_API_TOKEN }}
project_id: ${{ inputs.dbo_project_id }}
changelog: ${{ steps.release-artifact.outputs.body }}
changelog_type: markdown
display_name: ${{ steps.release-info.outputs.tag_name }}
game_versions: 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2, 1.18.1, 1.18, 1.17, 1.16, 1.15, 1.14, 1.13
release_type: ${{ steps.parse-release-type.outputs.release_type }}
project_relations: ${{ inputs.dbo_project_relations }}
file_path: ${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar
- name: Upload to Hangar
if: ${{ !cancelled() && inputs.upload_hangar == 'true' }}
uses: benwoo1110/hangar-upload-action@v1
with:
api_token: ${{ secrets.HANGAR_UPLOAD_TOKEN }}
slug: ${{ inputs.hangar_slug }}
version: ${{ steps.release-info.outputs.tag_name }}
channel: ${{ steps.parse-release-type.outputs.release_type }}
files: '[{"path": "${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar", "platforms": ["PAPER"]}]'
description: ${{ steps.release-artifact.outputs.body }}
platform_dependencies: '{"PAPER": ["1.13-1.20.1"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}

View File

@ -1,8 +1,12 @@
name: Run unit tests against all PRs
name: 'Generic: Test'
on:
pull_request:
types: [opened, synchronize]
workflow_call:
inputs:
plugin_name:
description: 'Plugin name'
required: true
type: string
jobs:
test:
@ -32,5 +36,5 @@ jobs:
- name: Artifact output
uses: actions/upload-artifact@v3
with:
name: multiverse-core-pr${{ github.event.pull_request.number }}
path: build/libs/multiverse-core-pr${{ github.event.pull_request.number }}.jar
name: ${{ inputs.plugin_name }}-pr${{ github.event.pull_request.number }}
path: build/libs/${{ inputs.plugin_name }}-pr${{ github.event.pull_request.number }}.jar

24
.github/workflows/main.prerelease.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: 'Main: Prerelease'
on:
push:
branches: [main]
jobs:
github_release_on_push:
uses: ./.github/workflows/call.github_release.yml
secrets: inherit
with:
release_mode: prerelease
version_bump: prlabel
platform_uploads_on_push:
needs: github_release_on_push
if: needs.github_release_on_push.outputs.release_created == 'true'
uses: ./.github/workflows/call.platform_uploads.yml
secrets: inherit
with:
target_tag: ${{ needs.github_release_on_push.outputs.tag_name }}
upload_modrinth: 'true'
upload_dbo: 'false'
upload_hangar: 'false'

View File

@ -1,4 +1,4 @@
name: Require PR Labels
name: 'PR: Require Label'
on:
pull_request:
@ -15,4 +15,4 @@ jobs:
with:
mode: exactly
count: 1
labels: "release:major, release:minor, release:patch, no version bump"
labels: "release:major, release:minor, release:patch, no release"

11
.github/workflows/pr.test.yml vendored Normal file
View File

@ -0,0 +1,11 @@
name: 'PR: Test'
on:
pull_request:
types: [opened, synchronize]
jobs:
test:
uses: ./.github/workflows/generic.test.yml
with:
plugin_name: multiverse-core

View File

@ -1,17 +0,0 @@
name: "Pull Request Labeler"
on:
pull_request_target:
types: [opened]
branches: [main]
jobs:
prerelease_labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -1,90 +0,0 @@
name: Manually promote last prerelease to release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to promote'
required: true
version-bump:
description: 'Version bump to apply - should usually match the version bump used for the prerelease since last release'
required: true
type: choice
options:
- 'patch'
- 'minor'
- 'major'
jobs:
manually_promote_release:
runs-on: ubuntu-latest
steps:
- name: Verify input version is prerelease
run: |
if [[ "${{ github.event.inputs.version }}" != *"pre"* ]]; then
echo "Version must be a prerelease"
exit 1
fi
- name: Get release info
id: get-release
uses: cardinalby/git-get-release-action@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
tag: ${{ github.event.inputs.version }}
- uses: actions/checkout@v3
with:
ref: ${{ steps.get-release.outputs.tag_name }}
- uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Remove prerelease tag
run: |
echo "Removing prerelease tag from version"
echo "VERSION=$(echo ${{ steps.get-release.outputs.tag_name }} | sed -E 's/-pre.*//')" >> $GITHUB_ENV
- name: Build
uses: gradle/gradle-build-action@v2
with:
arguments: clean build -x test -x checkstyleMain -x checkstyleTest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_VERSION: ${{ env.VERSION }}
- name: Create release
id: release
uses: Multiverse/release-on-push-action@skip_prs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
bump_version_scheme: ${{ github.event.inputs.version-bump }}
tag_prefix: ''
release_name: "<RELEASE_VERSION>"
use_github_release_notes: true
ref: ${{ steps.get-release.outputs.target_commitish }}
skip_prs: true
- name: Publish package
uses: gradle/gradle-build-action@v2
with:
arguments: publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_VERSION: ${{ env.VERSION }}
- name: Upload release artifact
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/libs/multiverse-core-${{ env.VERSION }}.jar
asset_name: multiverse-core-${{ steps.release.outputs.tag_name }}.jar
tag: ${{ steps.release.outputs.tag_name }}

View File

@ -1,64 +0,0 @@
name: Create Release Version & Publish Package
on:
push:
branches: [main]
jobs:
release_on_push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Test & Build
uses: gradle/gradle-build-action@v2
with:
arguments: clean build -x assemble -x shadowJar
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release
id: release
uses: Multiverse/release-on-push-action@support_prerelease
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
bump_version_scheme: norelease
tag_prefix: ''
release_name: "<RELEASE_VERSION>"
use_github_release_notes: true
- name: Modify version scheme
run: |
if [[ "${{ steps.release.outputs.tag_name }}" == *"pre"* ]]; then
echo "Replacing prerelease version scheme with SNAPSHOT"
echo "VERSION=$(echo ${{ steps.release.outputs.tag_name }} | sed -E 's/-pre.*/-SNAPSHOT/')" >> $GITHUB_ENV
else
echo "Using release version scheme"
echo "VERSION=${{ steps.release.outputs.tag_name }}" >> $GITHUB_ENV
fi
- name: Publish package
uses: gradle/gradle-build-action@v2
with:
arguments: publish
env:
GITHUB_VERSION: ${{ env.VERSION }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release artifact
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/libs/multiverse-core-${{ env.VERSION }}.jar
asset_name: multiverse-core-${{ steps.release.outputs.tag_name }}.jar
tag: ${{ steps.release.outputs.tag_name }}

View File

@ -1,26 +1,33 @@
![Multiverse 2](config/multiverse2-long.png)
<p align="center">
<img src="config/multiverse2-long.png" alt="Multiverse Logo">
</p>
[![Modrinth](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/modrinth_vector.svg)](https://modrinth.com/plugin/multiverse-core)
[![hangar](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/hangar_vector.svg)](https://hangar.papermc.io/Multiverse/Multiverse-Core)
[![bukkit](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/supported/bukkit_vector.svg)](https://dev.bukkit.org/projects/multiverse-core)
[![Spigot](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/supported/spigot_vector.svg)](https://www.spigotmc.org/resources/multiverse-core.390/)
[![Maven CI/CD](https://github.com/Multiverse/Multiverse-Core/actions/workflows/build.yml/badge.svg)](https://github.com/Multiverse/Multiverse-Core/actions/workflows/build.yml)
[![Release](https://img.shields.io/nexus/r/com.onarandombox.multiversecore/Multiverse-Core?label=release&server=https%3A%2F%2Frepo.onarandombox.com%2F)](https://dev.bukkit.org/projects/multiverse-core)
[![Dev builds](https://img.shields.io/nexus/s/com.onarandombox.multiversecore/Multiverse-Core?label=dev%20builds&server=http%3A%2F%2Frepo.onarandombox.com%2F)](https://ci.onarandombox.com/job/Multiverse-Core/)
[![Discord](https://img.shields.io/discord/325459248047980545?label=discord&logo=discord)](https://discord.gg/NZtfKky)
[![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Ddumptruckman%26type%3Dpatrons&style=flat)](https://patreon.com/dumptruckman)
[![Support us on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Ddumptruckman%26type%3Dpatrons&style=flat)](https://patreon.com/dumptruckman)
[![License](https://img.shields.io/github/license/Multiverse/Multiverse-Core)](LICENSE.md)
About
========
# About
[Multiverse](https://dev.bukkit.org/projects/multiverse-core) was created at the dawn of Bukkit multiworld support. It has since then grown into a **complete world management solution!** Multiverse provides the easiest to use world management solution for your Minecraft server, big or small, and with great addons like [Portals](https://dev.bukkit.org/projects/multiverse-portals) and [NetherPortals](https://dev.bukkit.org/projects/multiverse-netherportals/), what's not to love!
<br><br>
Now it's time to create your very own Multiverse server, do check out our [Wiki](https://github.com/Multiverse/Multiverse-Core/wiki) and [Usage Guide](https://github.com/Multiverse/Multiverse-Core/wiki/Basics) to get started. Feel free to hop onto our [Discord](https://discord.gg/NZtfKky) if you have any question or just want to have a chat with us!
### Amazing sub-modules available:
## Amazing sub-modules available:
* [Multiverse-NetherPortals](https://github.com/Multiverse/Multiverse-NetherPortals) -> Have separate nether and end worlds for each of your overworlds!
* [Multiverse-Portals](https://github.com/Multiverse/Multiverse-Portals) -> Make custom portals to go to any destination!
* [Multiverse-Inventories](https://github.com/Multiverse/Multiverse-Inventories) -> Have separated players stats and inventories per world or per group of worlds.
* [Multiverse-SignPortals](https://github.com/Multiverse/Multiverse-SignPortals) -> Signs as teleprompters!
* [Multiverse-SignPortals](https://github.com/Multiverse/Multiverse-SignPortals) -> Signs as teleporters!
Building
========
## Building
Simply build the source with Gradle:
```
./gradlew build
@ -28,17 +35,16 @@ Simply build the source with Gradle:
More details are available on the [build instructions wiki page](https://github.com/Multiverse/Multiverse-Core/wiki/Building).
Contributing
=======
## Contributing
**Want to help improve Multiverse?** There are several ways you can support and contribute to the project.
* Take a look at our "Bug: Unconfirmed" issues, where you can find issues that need extra testing and investigation.
* Want others to love Multiverse too? You can join the [Multiverse Discord community](https://discord.gg/NZtfKky) and help others with issues and setup!
* A Multiverse guru? You can update our [Wiki](https://github.com/Multiverse/Multiverse-Core/wiki) with your latest tip, tricks and guides! The wiki open for all to edit and improve.
* Love coding? You could look at ["State: Open to PR"](https://github.com/Multiverse/Multiverse-Core/labels/State%3A%20Open%20to%20PR) and ["Resolution: Accepted"](https://github.com/Multiverse/Multiverse-Core/labels/Resolution%3A%20Accepted) issues. We're always happy to receive bug fixes and feature additions as [pull requests](https://www.freecodecamp.org/news/how-to-make-your-first-pull-request-on-github-3/).
* If you'd like to make a financial contribution to the project, do consider joining our [patreon](https://www.patreon.com/dumptruckman) or make a one-time donation [here](https://paypal.me/dumptruckman)!
* If you'd like to make a financial contribution to the project, do consider joining our [Patreon](https://www.patreon.com/dumptruckman) or make a one-time donation [here](https://paypal.me/dumptruckman)!
Additionally, we would like to give a big thanks to everyone that has supported Multiverse over the years, as well as those in the years to come. Thank you!
License
=======
## License
Multiverse-Core is licensed under BSD-3-Clause License. Please see [LICENSE.md](LICENSE.md) for more info.

View File

@ -62,50 +62,58 @@ repositories {
}
configurations {
// Add configuration for server API
compileOnly.extendsFrom serverApi
runtimeClasspath.extendsFrom serverApi
// Add configuration for external plugins
implementation.extendsFrom externalPlugin
// Add configuration for dependencies that will be included in fat jar
compileClasspath.extendsFrom shadowed
testCompileClasspath.extendsFrom shadowed
testRuntimeClasspath.extendsFrom shadowed
oldTestCompileClasspath.extendsFrom shadowed
oldTestRuntimeClasspath.extendsFrom shadowed
// Add configuration for old tests
oldTestImplementation.extendsFrom implementation
oldTestRuntime.extendsFrom runtime
relocatedApi
compileClasspath.extendsFrom relocatedApi
runtimeClasspath.extendsFrom relocatedApi
testCompileClasspath.extendsFrom relocatedApi
testRuntimeClasspath.extendsFrom relocatedApi
oldTestCompileClasspath.extendsFrom relocatedApi
oldTestRuntimeClasspath.extendsFrom relocatedApi
}
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
serverApi 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
// Economy
implementation('com.github.MilkBowl:VaultAPI:1.7.1') {
externalPlugin('com.github.MilkBowl:VaultAPI:1.7.1') {
exclude group: 'org.bukkit', module: 'bukkit'
}
// PlaceholderAPI
implementation 'me.clip:placeholderapi:2.11.3'
externalPlugin 'me.clip:placeholderapi:2.11.3'
// Command Framework
relocatedApi 'co.aikar:acf-paper:0.5.1-SNAPSHOT'
shadowed 'co.aikar:acf-paper:0.5.1-SNAPSHOT'
// Config
relocatedApi('me.main__.util:SerializationConfig:1.7') {
shadowed('me.main__.util:SerializationConfig:1.7') {
exclude group: 'org.bukkit', module: 'bukkit'
}
relocatedApi('io.github.townyadvanced.commentedconfiguration:CommentedConfiguration:1.0.1') {
shadowed('io.github.townyadvanced.commentedconfiguration:CommentedConfiguration:1.0.1') {
exclude group: 'org.spigotmc', module: 'spigot-api'
}
// Utils
relocatedApi 'io.vavr:vavr:0.10.4'
relocatedApi 'org.glassfish.hk2:hk2-locator:3.0.3'
relocatedApi('com.dumptruckman.minecraft:Logging:1.1.1') {
shadowed 'io.vavr:vavr:0.10.4'
shadowed 'org.glassfish.hk2:hk2-locator:3.0.3'
shadowed('com.dumptruckman.minecraft:Logging:1.1.1') {
exclude group: 'junit', module: 'junit'
}
relocatedApi 'de.themoep.idconverter:mappings:1.2-SNAPSHOT'
relocatedApi 'org.bstats:bstats-bukkit:2.2.1'
relocatedApi 'net.minidev:json-smart:2.4.8'
relocatedApi 'org.jetbrains:annotations:22.0.0'
relocatedApi 'io.papermc:paperlib:1.0.8'
shadowed 'de.themoep.idconverter:mappings:1.2-SNAPSHOT'
shadowed 'org.bstats:bstats-bukkit:2.2.1'
shadowed 'net.minidev:json-smart:2.4.8'
shadowed 'org.jetbrains:annotations:22.0.0'
shadowed 'io.papermc:paperlib:1.0.8'
// Tests
testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10'
@ -170,6 +178,34 @@ publishing {
publications {
maven(MavenPublication) {
from components.java
pom.withXml {
Node pomNode = asNode()
// Remove Kotlin dependency
pomNode.dependencies.'*'.findAll() {
it.groupId.text() == 'org.jetbrains.kotlin'
}.each() {
it.parent().remove(it)
}
// Switch runtime deps to provided
pomNode.dependencies.'*'.findAll() {
it.scope.text() == 'runtime'
}.each() {
it.scope*.value = 'provided'
}
// Add spigot api to pom
project.configurations.serverApi.allDependencies.each { dependency ->
pomNode.dependencies[0].appendNode("dependency").with {
it.appendNode("groupId", dependency.group)
it.appendNode("artifactId", dependency.name)
it.appendNode("version", dependency.version)
it.appendNode("scope", "provided")
}
}
}
}
}
repositories {
@ -243,7 +279,7 @@ javadoc {
project.configurations.api.canBeResolved = true
shadowJar {
relocate 'co.aikar.commands', 'com.onarandombox.acf'
relocate 'co.aikar', 'com.onarandombox.acf'
relocate 'com.dumptruckman.minecraft.util.Logging', 'com.onarandombox.MultiverseCore.utils.CoreLogging'
relocate 'com.dumptruckman.minecraft.util.DebugLog', 'com.onarandombox.MultiverseCore.utils.DebugFileLogger'
relocate 'de.themoep.idconverter', 'com.onarandombox.idconverter'
@ -263,7 +299,7 @@ shadowJar {
relocate 'org.jetbrains', 'com.onarandombox.jetbrains'
relocate 'io.papermc.lib', 'com.onarandombox.paperlib'
configurations = [project.configurations.api, project.configurations.relocatedApi]
configurations = [project.configurations.shadowed]
archiveFileName = "$baseName-$version.$extension"
@ -272,6 +308,7 @@ shadowJar {
it.moduleGroup == 'org.jetbrains.kotlin'
})
}
//classifier = ''
}
build.dependsOn shadowJar

View File

@ -2,4 +2,10 @@
* This file was generated by the Gradle 'init' task.
*/
pluginManagement {
repositories {
gradlePluginPortal()
}
}
rootProject.name = 'multiverse-core'

View File

@ -25,6 +25,7 @@ import com.onarandombox.MultiverseCore.placeholders.MultiverseCorePlaceholders;
import com.onarandombox.MultiverseCore.utils.TestingMode;
import com.onarandombox.MultiverseCore.utils.metrics.MetricsConfigurator;
import com.onarandombox.MultiverseCore.world.WorldProperties;
import com.onarandombox.MultiverseCore.worldnew.WorldManager;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
@ -54,10 +55,12 @@ public class MultiverseCore extends JavaPlugin implements MVCore {
private ServiceLocator serviceLocator;
@Inject
private MVCoreConfig config;
private Provider<MVCoreConfig> configProvider;
@Inject
private Provider<MVWorldManager> worldManagerProvider;
@Inject
private Provider<WorldManager> newWorldManagerProvider;
@Inject
private Provider<AnchorManager> anchorManagerProvider;
@Inject
private Provider<MVCommandManager> commandManagerProvider;
@ -101,7 +104,8 @@ public class MultiverseCore extends JavaPlugin implements MVCore {
initializeDependencyInjection();
// Load our configs first as we need them for everything else.
if (config == null || !config.isLoaded()) {
var config = configProvider.get();
if (!config.isLoaded()) {
Logging.severe("Your configs were not loaded.");
Logging.severe("Please check your configs and restart the server.");
this.getServer().getPluginManager().disablePlugin(this);
@ -124,6 +128,9 @@ public class MultiverseCore extends JavaPlugin implements MVCore {
config.setFirstSpawnLocation(firstSpawnWorld.getName());
}
var newWorldManager = newWorldManagerProvider.get();
newWorldManager.loadAllWorlds(); // TODO: Possibly move this to constructor of WorldManager
//Setup economy here so vault is loaded
this.loadEconomist();
@ -169,7 +176,7 @@ public class MultiverseCore extends JavaPlugin implements MVCore {
}
private boolean shouldShowConfig() {
return !config.getSilentStart();
return !configProvider.get().getSilentStart();
}
private void loadEconomist() {
@ -273,14 +280,14 @@ public class MultiverseCore extends JavaPlugin implements MVCore {
private void logEnableMessage() {
Logging.config("Version %s (API v%s) Enabled - By %s", this.getDescription().getVersion(), PROTOCOL, getAuthors());
if (config.isShowingDonateMessage()) {
if (configProvider.get().isShowingDonateMessage()) {
Logging.config("Help dumptruckman keep this project alive. Become a patron! https://www.patreon.com/dumptruckman");
Logging.config("One time donations are also appreciated: https://www.paypal.me/dumptruckman");
}
}
private void loadPlaceholderAPIIntegration() {
if (config.isRegisterPapiHook()
if (configProvider.get().isRegisterPapiHook()
&& getServer().getPluginManager().getPlugin("PlaceholderAPI") != null) {
Try.run(() -> serviceLocator.createAndInitialize(MultiverseCorePlaceholders.class))
.onFailure(e -> {
@ -367,7 +374,7 @@ public class MultiverseCore extends JavaPlugin implements MVCore {
*/
@Override
public boolean saveAllConfigs() {
return config.save()
return configProvider.get().save()
&& worldManagerProvider.get().saveWorldsConfig()
&& anchorManagerProvider.get().saveAnchors();
}

View File

@ -36,13 +36,13 @@ public class CloneCommand extends MultiverseCommand {
@Description("{@@mv-core.clone.description}")
public void onCloneCommand(CommandIssuer issuer,
@Conditions("validWorldName:scope=both")
@Conditions("worldname:scope=both")
@Syntax("<world>")
@Description("{@@mv-core.clone.world.description}")
String worldName,
@Single
@Conditions("validWorldName:scope=new")
@Conditions("worldname:scope=new")
@Syntax("<new world name>")
@Description("{@@mv-core.clone.newWorld.description}")
String newWorldName

View File

@ -67,22 +67,8 @@ public class CreateCommand extends MultiverseCommand {
.map(genplugin -> genplugin.getDescription().getName())
.collect(Collectors.toList()))
.build())
.add(CommandValueFlag.builder("--world-type", WorldType.class)
.add(CommandValueFlag.enumBuilder("--world-type", WorldType.class)
.addAlias("-t")
.context((value) -> {
try {
return WorldType.valueOf(value.toUpperCase());
} catch (IllegalArgumentException e) {
throw new InvalidCommandArgument("Invalid world type: " + value);
}
})
.completion(() -> {
List<String> types = new ArrayList<>();
for (WorldType type : WorldType.values()) {
types.add(type.name().toLowerCase());
}
return types;
})
.build())
.add(CommandFlag.builder("--adjust-spawn")
.addAlias("-n")
@ -100,7 +86,7 @@ public class CreateCommand extends MultiverseCommand {
@Description("{@@mv-core.create.description}")
public void onCreateCommand(BukkitCommandIssuer issuer,
@Conditions("validWorldName:scope=new")
@Conditions("worldname:scope=new")
@Syntax("<name>")
@Description("{@@mv-core.create.name.description}")
String worldName,

View File

@ -39,7 +39,7 @@ public class DeleteCommand extends MultiverseCommand {
public void onDeleteCommand(BukkitCommandIssuer issuer,
@Single
@Conditions("validWorldName:scope=both")
@Conditions("worldname:scope=both")
@Syntax("<world>")
@Description("The world you want to delete.")
String worldName

View File

@ -69,7 +69,7 @@ public class ImportCommand extends MultiverseCommand {
@Description("{@@mv-core.import.description")
public void onImportCommand(BukkitCommandIssuer issuer,
@Conditions("validWorldName:scope=new")
@Conditions("worldname:scope=new")
@Syntax("<name>")
@Description("{@@mv-core.import.name.description}")
String worldName,

View File

@ -0,0 +1,145 @@
package com.onarandombox.MultiverseCore.commands;
import java.util.ArrayList;
import java.util.List;
import co.aikar.commands.BukkitCommandIssuer;
import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.onarandombox.MultiverseCore.MultiverseCore;
import com.onarandombox.MultiverseCore.api.MVWorld;
import com.onarandombox.MultiverseCore.api.MVWorldManager;
import com.onarandombox.MultiverseCore.commandtools.MVCommandManager;
import com.onarandombox.MultiverseCore.commandtools.MultiverseCommand;
import com.onarandombox.MultiverseCore.commandtools.flags.CommandFlagGroup;
import com.onarandombox.MultiverseCore.commandtools.flags.CommandValueFlag;
import com.onarandombox.MultiverseCore.commandtools.flags.ParsedCommandFlags;
import com.onarandombox.MultiverseCore.display.ContentDisplay;
import com.onarandombox.MultiverseCore.display.filters.ContentFilter;
import com.onarandombox.MultiverseCore.display.filters.DefaultContentFilter;
import com.onarandombox.MultiverseCore.display.filters.RegexContentFilter;
import com.onarandombox.MultiverseCore.display.handlers.PagedSendHandler;
import com.onarandombox.MultiverseCore.display.parsers.ListContentProvider;
import com.onarandombox.MultiverseCore.utils.UnsafeCallWrapper;
import com.onarandombox.MultiverseCore.world.entrycheck.WorldEntryCheckerProvider;
import jakarta.inject.Inject;
import org.bukkit.ChatColor;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@Service
@CommandAlias("mv")
public class ListCommand extends MultiverseCommand {
private final MVWorldManager worldManager;
private final WorldEntryCheckerProvider worldEntryCheckerProvider;
@Inject
public ListCommand(
@NotNull MVCommandManager commandManager,
@NotNull MVWorldManager worldManager,
@NotNull WorldEntryCheckerProvider worldEntryCheckerProvider
) {
super(commandManager);
this.worldManager = worldManager;
this.worldEntryCheckerProvider = worldEntryCheckerProvider;
registerFlagGroup(CommandFlagGroup.builder("mvlist")
.add(CommandValueFlag.builder("--filter", ContentFilter.class)
.addAlias("-f")
.context((value) -> {
try {
return RegexContentFilter.fromString(value);
} catch (IllegalArgumentException e) {
throw new InvalidCommandArgument("Invalid filter: " + value);
}
})
.build())
.add(CommandValueFlag.builder("--page", Integer.class)
.addAlias("-p")
.context((value) -> {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new InvalidCommandArgument("Invalid page number: " + value);
}
})
.build())
.build());
}
@Subcommand("list")
@CommandPermission("multiverse.core.list.worlds")
@CommandCompletion("@flags:groupName=mvlist")
@Syntax("--filter [filter] --page [page]")
@Description("Displays a listing of all worlds that you can enter.")
public void onListCommand(BukkitCommandIssuer issuer,
@Syntax("--filter [filter] --page [page]")
@Description("Filters the list of worlds by the given regex and displays the given page.")
String[] flags
) {
ParsedCommandFlags parsedFlags = parseFlags(flags);
ContentDisplay.create()
.addContent(ListContentProvider.forContent(getListContents(issuer)))
.withSendHandler(PagedSendHandler.create()
.withHeader("%s====[ Multiverse World List ]====", ChatColor.GOLD)
.withTargetPage(parsedFlags.flagValue("--page", 1, Integer.class))
.withFilter(parsedFlags.flagValue("--filter", DefaultContentFilter.get(), ContentFilter.class))
.withLinesPerPage(4)) //TODO Change back after testing
.send(issuer);
}
private List<String> getListContents(BukkitCommandIssuer issuer) {
Player player = issuer.isPlayer() ? issuer.getPlayer() : null;
List<String> worldList = new ArrayList<>();
worldManager.getMVWorlds().stream()
.filter(world -> player == null || worldEntryCheckerProvider.forSender(player).canAccessWorld(world).isSuccess())
.filter(world -> canSeeWorld(player, world))
.map(world -> hiddenText(world) + world.getColoredWorldString() + " - " + parseColouredEnvironment(world.getEnvironment()))
.sorted()
.forEach(worldList::add);
worldManager.getUnloadedWorlds().stream()
.filter(world -> issuer.hasPermission("multiverse.access." + world)) // TODO: Refactor stray permission check
.map(world -> ChatColor.GRAY + world + " - UNLOADED")
.sorted()
.forEach(worldList::add);
return worldList;
}
private boolean canSeeWorld(Player player, MVWorld world) {
return !world.isHidden()
|| player == null
|| player.hasPermission("multiverse.core.modify"); // TODO: Refactor stray permission check
}
private String hiddenText(MVWorld world) {
return (world.isHidden()) ? String.format("%s[H] ", ChatColor.GRAY) : "";
}
private String parseColouredEnvironment(World.Environment env) {
ChatColor color = ChatColor.GOLD;
switch (env) {
case NETHER:
color = ChatColor.RED;
break;
case NORMAL:
color = ChatColor.GREEN;
break;
case THE_END:
color = ChatColor.AQUA;
break;
}
return color + env.toString();
}
}

View File

@ -37,7 +37,7 @@ public class LoadCommand extends MultiverseCommand {
public void onLoadCommand(BukkitCommandIssuer issuer,
@Single
@Conditions("validWorldName:scope=unloaded")
@Conditions("worldname:scope=unloaded")
@Syntax("<world>")
@Description("{@@mv-core.load.world.description}")
String worldName

View File

@ -56,7 +56,7 @@ public class RegenCommand extends MultiverseCommand {
@Description("{@@mv-core.regen.description}")
public void onRegenCommand(BukkitCommandIssuer issuer,
@Conditions("validWorldName:scope=both")
@Conditions("worldname:scope=both")
@Syntax("<world>")
@Description("{@@mv-core.regen.world.description}")
String worldName,

View File

@ -11,6 +11,7 @@ import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Flags;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.commandtools.MVCommandManager;
import com.onarandombox.MultiverseCore.commandtools.MultiverseCommand;
import com.onarandombox.MultiverseCore.destination.DestinationsProvider;
@ -49,17 +50,17 @@ public class TeleportCommand extends MultiverseCommand {
) {
// TODO Add warning if teleporting too many players at once.
String playerName = players.length == 1
? issuer.getPlayer() == players[0] ? "you" : players[0].getName()
: players.length + " players";
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
"{player}", playerName, "{destination}", destination.toString());
CompletableFuture.allOf(Arrays.stream(players)
.map(player -> this.destinationsProvider.playerTeleportAsync(issuer, player, destination))
.toArray(CompletableFuture[]::new))
.thenRun(() -> {
String playerName = players.length == 1
? issuer.getPlayer() == players[0] ? "you" : players[0].getName()
: players.length + " players";
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
"{player}", playerName, "{destination}", destination.toString());
});
.thenRun(() -> Logging.finer("Async teleport completed."));
}
@Override

View File

@ -8,7 +8,6 @@ import co.aikar.commands.ConditionContext;
import co.aikar.commands.ConditionFailedException;
import com.onarandombox.MultiverseCore.api.MVWorldManager;
import com.onarandombox.MultiverseCore.world.WorldNameChecker;
import jakarta.annotation.PostConstruct;
import org.jetbrains.annotations.NotNull;
public class MVCommandConditions {
@ -22,19 +21,19 @@ public class MVCommandConditions {
private MVCommandConditions(@NotNull MVCommandManager commandManager, @NotNull MVWorldManager worldManager) {
this.worldManager = worldManager;
this.commandManager = commandManager;
registerConditions();
}
@PostConstruct
private void registerConditions() {
CommandConditions<BukkitCommandIssuer, BukkitCommandExecutionContext, BukkitConditionContext> conditions
= commandManager.getCommandConditions();
conditions.addCondition(String.class, "validWorldName", this::checkValidWorldName);
conditions.addCondition(String.class, "worldname", this::checkWorldname);
}
private void checkValidWorldName(ConditionContext<BukkitCommandIssuer> context,
BukkitCommandExecutionContext executionContext,
String worldName
private void checkWorldname(ConditionContext<BukkitCommandIssuer> context,
BukkitCommandExecutionContext executionContext,
String worldName
) {
String scope = context.getConfigValue("scope", "loaded");

View File

@ -26,11 +26,12 @@ public class CommandFlag {
/**
* Creates a new flag.
*
* @param builder The builder.
* @param key The key for the new flag.
* @param aliases The aliases that also refer to this flag.
*/
protected CommandFlag(@NotNull Builder<?> builder) {
key = builder.key;
aliases = builder.aliases;
protected CommandFlag(@NotNull String key, @NotNull List<String> aliases) {
this.key = key;
this.aliases = aliases;
}
/**
@ -57,8 +58,8 @@ public class CommandFlag {
* @param <S> The type of the builder.
*/
public static class Builder<S extends Builder<?>> {
private final String key;
private final List<String> aliases;
protected final String key;
protected final List<String> aliases;
/**
* Create a new builder.
@ -87,7 +88,7 @@ public class CommandFlag {
* @return The flag.
*/
public @NotNull CommandFlag build(){
return new CommandFlag(this);
return new CommandFlag(key, aliases);
}
}
}

View File

@ -1,12 +1,16 @@
package com.onarandombox.MultiverseCore.commandtools.flags;
import java.util.Collection;
import java.util.function.Function;
import java.util.function.Supplier;
import co.aikar.commands.InvalidCommandArgument;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Represents a flag with a value.
*
@ -16,14 +20,25 @@ public class CommandValueFlag<T> extends CommandFlag {
/**
* A builder for a flag.
*
* @param key The key for the new flag.
* @param type The type of the value.
* @param key The key for the new flag.
* @param type The type of the value.
* @return The builder.
*/
public static @NotNull <T> Builder<T, ?> builder(@NotNull String key, @NotNull Class<T> type) {
return new Builder<>(key, type);
}
/**
* A builder for a flag with enum value.
*
* @param key The key for the new flag.
* @param type The type of the value, must be enum.
* @return The builder.
*/
public static @NotNull <T extends Enum<T>> EnumBuilder<T, ?> enumBuilder(@NotNull String key, @NotNull Class<T> type) {
return new EnumBuilder<>(key, type);
}
private final Class<T> type;
private final boolean optional;
private final T defaultValue;
@ -33,15 +48,29 @@ public class CommandValueFlag<T> extends CommandFlag {
/**
* Creates a new flag.
*
* @param builder The builder.
* @param key The key for the new flag.
* @param aliases The aliases that also refer to this flag.
* @param type The type of the value.
* @param optional Allow for flag without value.
* @param defaultValue The default value if optional is true and user does not specify a value.
* @param context Function to parse string into value type.
* @param completion Function to get completion for this flag.
*/
protected CommandValueFlag(@NotNull Builder<T, ?> builder) {
super(builder);
type = builder.type;
optional = builder.optional;
defaultValue = builder.defaultValue;
context = builder.context;
completion = builder.completion;
protected CommandValueFlag(
@NotNull String key,
@NotNull List<String> aliases,
@NotNull Class<T> type,
boolean optional,
@Nullable T defaultValue,
@Nullable Function<String, T> context,
@Nullable Supplier<Collection<String>> completion
) {
super(key, aliases);
this.type = type;
this.optional = optional;
this.defaultValue = defaultValue;
this.context = context;
this.completion = completion;
}
/**
@ -96,11 +125,11 @@ public class CommandValueFlag<T> extends CommandFlag {
* @param <S> The type of the builder.
*/
public static class Builder<T, S extends Builder<T, S>> extends CommandFlag.Builder<S> {
private final Class<T> type;
private boolean optional = false;
private T defaultValue = null;
private Function<String, T> context = null;
private Supplier<Collection<String>> completion = null;
protected final Class<T> type;
protected boolean optional = false;
protected T defaultValue = null;
protected Function<String, T> context = null;
protected Supplier<Collection<String>> completion = null;
/**
* Create a new builder.
@ -166,7 +195,77 @@ public class CommandValueFlag<T> extends CommandFlag {
if (context == null && !String.class.equals(type)) {
throw new IllegalStateException("Context is required for none-string value flags");
}
return new CommandValueFlag<>(this);
return new CommandValueFlag<>(key, aliases, type, optional, defaultValue, context, completion);
}
}
/**
* Specific builder for a flag with enum value.
*
* @param <T> The type of the value.
* @param <S> The type of the builder.
*/
public static class EnumBuilder<T extends Enum<T>, S extends EnumBuilder<T, S>> extends CommandFlag.Builder<S> {
protected final Class<T> type;
protected boolean optional = false;
protected T defaultValue = null;
protected Function<String, T> context = null;
protected Supplier<Collection<String>> completion = null;
public EnumBuilder(@NotNull String key, @NotNull Class<T> type) {
super(key);
this.type = type;
setEnumContext();
setEnumCompletion();
}
private void setEnumContext() {
this.context = (String value) -> {
try {
return Enum.valueOf(type, value.toUpperCase());
} catch (IllegalArgumentException e) {
throw new InvalidCommandArgument("Invalid value for argument " + key + ": " + value);
}
};
}
private void setEnumCompletion() {
List<String> types = Arrays.stream(type.getEnumConstants())
.map(type -> type.name().toLowerCase())
.collect(Collectors.toList());
this.completion = () -> types;
}
/**
* Set the flag as optional for users to specify a value.
*
* @return The builder.
*/
public @NotNull S optional() {
this.optional = true;
return (S) this;
}
/**
* Set the default value. Used if optional is true and user does not specify a value.
*
* @param defaultValue The default value.
* @return The builder.
*/
public @NotNull S defaultValue(@NotNull T defaultValue) {
this.defaultValue = defaultValue;
return (S) this;
}
/**
* Build the flag.
*
* @return The flag.
*/
@Override
public @NotNull CommandFlag build() {
return new CommandValueFlag<>(key, aliases, type, optional, defaultValue, context, completion);
}
}
}

View File

@ -23,7 +23,6 @@ abstract class FileConfigHandle<C extends FileConfiguration> extends GenericConf
protected final @NotNull Path configPath;
protected final @NotNull File configFile;
protected FileConfigHandle(@NotNull Path configPath, @Nullable Logger logger, @Nullable NodeGroup nodes, @Nullable ConfigMigrator migrator) {
super(logger, nodes, migrator);
this.configPath = configPath;
@ -107,9 +106,6 @@ abstract class FileConfigHandle<C extends FileConfiguration> extends GenericConf
public static abstract class Builder<C extends FileConfiguration, B extends Builder<C, B>> extends GenericConfigHandle.Builder<C, B> {
protected @NotNull Path configPath;
protected @Nullable Logger logger;
protected @Nullable NodeGroup nodes;
protected @Nullable ConfigMigrator migrator;
protected Builder(@NotNull Path configPath) {
this.configPath = configPath;

View File

@ -3,7 +3,9 @@ package com.onarandombox.MultiverseCore.configuration.handle;
import com.onarandombox.MultiverseCore.configuration.migration.ConfigMigrator;
import com.onarandombox.MultiverseCore.configuration.node.ConfigNodeNotFoundException;
import com.onarandombox.MultiverseCore.configuration.node.NodeGroup;
import com.onarandombox.MultiverseCore.configuration.node.NodeSerializer;
import com.onarandombox.MultiverseCore.configuration.node.ValueNode;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.Plugin;
@ -58,7 +60,7 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
nodes.forEach(node -> {
if (node instanceof ValueNode valueNode) {
set(valueNode, config.getObject(valueNode.getPath(), valueNode.getType(), valueNode.getDefaultValue()));
set(valueNode, get(valueNode));
}
});
}
@ -81,7 +83,10 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
* @return The value of the node.
*/
public <T> T get(@NotNull ValueNode<T> node) {
return config.getObject(node.getPath(), node.getType(), node.getDefaultValue());
if (node.getSerializer() == null) {
return config.getObject(node.getPath(), node.getType(), node.getDefaultValue());
}
return node.getSerializer().deserialize(config.get(node.getPath(), node.getDefaultValue()), node.getType());
}
/**
@ -108,7 +113,12 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
public <T> Try<Void> set(@NotNull ValueNode<T> node, T value) {
return node.validate(value).map(ignore -> {
T oldValue = get(node);
config.set(node.getPath(), value);
if (node.getSerializer() != null) {
var serialized = node.getSerializer().serialize(value, node.getType());
config.set(node.getPath(), serialized);
} else {
config.set(node.getPath(), value);
}
node.onSetValue(oldValue, get(node));
return null;
});

View File

@ -32,6 +32,7 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected final @Nullable String name;
protected final @NotNull Class<T> type;
protected final @Nullable T defaultValue;
protected final @Nullable NodeSerializer<T> serializer;
protected final @Nullable Function<T, Try<Void>> validator;
protected final @Nullable BiConsumer<T, T> onSetValue;
@ -41,6 +42,7 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Nullable String name,
@NotNull Class<T> type,
@Nullable T defaultValue,
@Nullable NodeSerializer<T> serializer,
@Nullable Function<T, Try<Void>> validator,
@Nullable BiConsumer<T, T> onSetValue
) {
@ -48,6 +50,7 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
this.name = name;
this.type = type;
this.defaultValue = defaultValue;
this.serializer = serializer;
this.validator = validator;
this.onSetValue = onSetValue;
}
@ -76,6 +79,10 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return defaultValue;
}
public @Nullable NodeSerializer<T> getSerializer() {
return serializer;
}
/**
* {@inheritDoc}
*/
@ -104,10 +111,12 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* @param <B> The type of the builder.
*/
public static class Builder<T, B extends ConfigNode.Builder<T, B>> extends ConfigHeaderNode.Builder<B> {
private static final NodeSerializer<?> ENUM_NODE_SERIALIZER = new EnumNodeSerializer<>();
protected @Nullable String name;
protected @NotNull final Class<T> type;
protected @Nullable T defaultValue;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
@ -121,6 +130,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
super(path);
this.name = path;
this.type = type;
if (type.isEnum()) {
this.serializer = (NodeSerializer<T>) ENUM_NODE_SERIALIZER;
}
}
/**
@ -145,6 +157,11 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return (B) this;
}
public @NotNull B serializer(@NotNull NodeSerializer<T> serializer) {
this.serializer = serializer;
return (B) this;
}
public @NotNull B validator(@NotNull Function<T, Try<Void>> validator) {
this.validator = validator;
return (B) this;
@ -166,7 +183,7 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
*/
@Override
public @NotNull ConfigNode<T> build() {
return new ConfigNode<>(path, comments.toArray(new String[0]), name, type, defaultValue, validator, onSetValue);
return new ConfigNode<>(path, comments.toArray(new String[0]), name, type, defaultValue, serializer, validator, onSetValue);
}
}
}

View File

@ -0,0 +1,16 @@
package com.onarandombox.MultiverseCore.configuration.node;
import com.dumptruckman.minecraft.util.Logging;
public class EnumNodeSerializer<T extends Enum<T>> implements NodeSerializer<T> {
@Override
public T deserialize(Object object, Class<T> type) {
return Enum.valueOf(type, object.toString().toUpperCase());
}
@Override
public Object serialize(T object, Class<T> type) {
return object.toString();
}
}

View File

@ -0,0 +1,6 @@
package com.onarandombox.MultiverseCore.configuration.node;
public interface NodeSerializer<T> {
T deserialize(Object object, Class<T> type);
Object serialize(T object, Class<T> type);
}

View File

@ -28,6 +28,13 @@ public interface ValueNode<T> extends Node {
*/
@Nullable T getDefaultValue();
/**
* Gets the serializer for this node.
*
* @return The serializer for this node.
*/
@Nullable NodeSerializer<T> getSerializer();
/**
* Validates the value of this node.
*

View File

@ -1,5 +1,6 @@
package com.onarandombox.MultiverseCore.economy;
import com.onarandombox.MultiverseCore.api.MVWorld;
import jakarta.inject.Inject;
import org.bukkit.Material;
import org.bukkit.World;
@ -91,6 +92,35 @@ public class MVEconomist {
return "Sorry, you don't have enough " + (isItemCurrency(currency) ? "items" : "funds") + ". " + message;
}
/**
* Pays for a given amount of currency either from the player's economy account or inventory if the currency.
*
* @param player the player to deposit currency into.
* @param world the world to take entry fee from.
*/
public void payEntryFee(Player player, MVWorld world) {
payEntryFee(player, world.getPrice(), world.getCurrency());
}
/**
* Pays for a given amount of currency either from the player's economy account or inventory if the currency
*
* @param player the player to take currency from.
* @param price the amount to take.
* @param currency the type of currency.
*/
public void payEntryFee(Player player, double price, Material currency) {
if (price == 0D) {
return;
}
if (price < 0) {
this.deposit(player, -price, currency);
} else {
this.withdraw(player, price, currency);
}
}
/**
* Deposits a given amount of currency either into the player's economy account or inventory if the currency
* is not null.

View File

@ -16,12 +16,17 @@ import com.onarandombox.MultiverseCore.MultiverseCore;
import com.onarandombox.MultiverseCore.api.MVWorld;
import com.onarandombox.MultiverseCore.api.MVWorldManager;
import com.onarandombox.MultiverseCore.api.SafeTTeleporter;
import com.onarandombox.MultiverseCore.commandtools.MVCommandManager;
import com.onarandombox.MultiverseCore.config.MVCoreConfig;
import com.onarandombox.MultiverseCore.economy.MVEconomist;
import com.onarandombox.MultiverseCore.event.MVRespawnEvent;
import com.onarandombox.MultiverseCore.inject.InjectableListener;
import com.onarandombox.MultiverseCore.teleportation.TeleportQueue;
import com.onarandombox.MultiverseCore.utils.MVPermissions;
import com.onarandombox.MultiverseCore.utils.PermissionTools;
import com.onarandombox.MultiverseCore.utils.result.ResultChain;
import com.onarandombox.MultiverseCore.world.entrycheck.EntryFeeResult;
import com.onarandombox.MultiverseCore.world.entrycheck.WorldEntryCheckerProvider;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.GameMode;
@ -54,6 +59,9 @@ public class MVPlayerListener implements InjectableListener {
private final SafeTTeleporter safeTTeleporter;
private final Server server;
private final TeleportQueue teleportQueue;
private final MVEconomist economist;
private final WorldEntryCheckerProvider worldEntryCheckerProvider;
private final Provider<MVCommandManager> commandManagerProvider;
private final Map<String, String> playerWorld = new ConcurrentHashMap<String, String>();
@ -66,8 +74,10 @@ public class MVPlayerListener implements InjectableListener {
Provider<MVPermissions> mvPermsProvider,
SafeTTeleporter safeTTeleporter,
Server server,
TeleportQueue teleportQueue
) {
TeleportQueue teleportQueue,
MVEconomist economist,
WorldEntryCheckerProvider worldEntryCheckerProvider,
Provider<MVCommandManager> commandManagerProvider) {
this.plugin = plugin;
this.config = config;
this.worldManagerProvider = worldManagerProvider;
@ -76,12 +86,19 @@ public class MVPlayerListener implements InjectableListener {
this.safeTTeleporter = safeTTeleporter;
this.server = server;
this.teleportQueue = teleportQueue;
this.economist = economist;
this.worldEntryCheckerProvider = worldEntryCheckerProvider;
this.commandManagerProvider = commandManagerProvider;
}
private MVWorldManager getWorldManager() {
return worldManagerProvider.get();
}
private MVCommandManager getCommandManager() {
return commandManagerProvider.get();
}
private MVPermissions getMVPerms() {
return mvPermsProvider.get();
}
@ -189,7 +206,7 @@ public class MVPlayerListener implements InjectableListener {
return;
}
Player teleportee = event.getPlayer();
CommandSender teleporter = null;
CommandSender teleporter;
Optional<String> teleporterName = teleportQueue.popFromQueue(teleportee.getName());
if (teleporterName.isPresent()) {
if (teleporterName.equals("CONSOLE")) {
@ -198,6 +215,8 @@ public class MVPlayerListener implements InjectableListener {
} else {
teleporter = this.server.getPlayerExact(teleporterName.get());
}
} else {
teleporter = teleportee;
}
Logging.finer("Inferred sender '" + teleporter + "' from name '"
+ teleporterName + "', fetched from name '" + teleportee.getName() + "'");
@ -215,50 +234,20 @@ public class MVPlayerListener implements InjectableListener {
this.stateSuccess(teleportee.getName(), toWorld.getAlias());
return;
}
// TODO: Refactor these lines.
// Charge the teleporter
event.setCancelled(!pt.playerHasMoneyToEnter(fromWorld, toWorld, teleporter, teleportee, true));
if (event.isCancelled() && teleporter != null) {
Logging.fine("Player '" + teleportee.getName()
+ "' was DENIED ACCESS to '" + toWorld.getAlias()
+ "' because '" + teleporter.getName()
+ "' don't have the FUNDS required to enter it.");
return;
}
// Check if player is allowed to enter the world if we're enforcing permissions
if (config.getEnforceAccess()) {
event.setCancelled(!pt.playerCanGoFromTo(fromWorld, toWorld, teleporter, teleportee));
if (event.isCancelled() && teleporter != null) {
Logging.fine("Player '" + teleportee.getName()
+ "' was DENIED ACCESS to '" + toWorld.getAlias()
+ "' because '" + teleporter.getName()
+ "' don't have: multiverse.access." + event.getTo().getWorld().getName());
return;
}
} else {
Logging.fine("Player '" + teleportee.getName()
+ "' was allowed to go to '" + toWorld.getAlias() + "' because enforceaccess is off.");
}
// Does a limit actually exist?
if (toWorld.getPlayerLimit() > -1) {
// Are there equal or more people on the world than the limit?
if (toWorld.getCBWorld().getPlayers().size() >= toWorld.getPlayerLimit()) {
// Ouch the world is full, lets see if the player can bypass that limitation
if (!pt.playerCanBypassPlayerLimit(toWorld, teleporter, teleportee)) {
Logging.fine("Player '" + teleportee.getName()
+ "' was DENIED ACCESS to '" + toWorld.getAlias()
+ "' because the world is full and '" + teleporter.getName()
+ "' doesn't have: mv.bypass.playerlimit." + event.getTo().getWorld().getName());
ResultChain entryResult = worldEntryCheckerProvider.forSender(teleporter).canEnterWorld(fromWorld, toWorld)
.onSuccessReason(EntryFeeResult.Success.class, reason -> {
if (reason == EntryFeeResult.Success.ENOUGH_MONEY) {
economist.payEntryFee((Player) teleporter, toWorld);
// Send payment receipt
}
})
.onFailure(results -> {
event.setCancelled(true);
return;
}
}
}
getCommandManager().getCommandIssuer(teleporter).sendError(results.getLastResultMessage());
});
// By this point anything cancelling the event has returned on the method, meaning the teleport is a success \o/
this.stateSuccess(teleportee.getName(), toWorld.getAlias());
Logging.fine("Teleport result: %s", entryResult);
}
private void stateSuccess(String playerName, String worldName) {
@ -305,6 +294,10 @@ public class MVPlayerListener implements InjectableListener {
if (event.getTo() == null) {
return;
}
if (config.isUsingCustomPortalSearch()) {
event.setSearchRadius(config.getCustomPortalSearchRadius());
}
MVWorld fromWorld = getWorldManager().getMVWorld(event.getFrom().getWorld().getName());
MVWorld toWorld = getWorldManager().getMVWorld(event.getTo().getWorld().getName());
if (event.getFrom().getWorld().equals(event.getTo().getWorld())) {
@ -312,28 +305,14 @@ public class MVPlayerListener implements InjectableListener {
Logging.finer("Player '" + event.getPlayer().getName() + "' is portaling to the same world.");
return;
}
event.setCancelled(!pt.playerHasMoneyToEnter(fromWorld, toWorld, event.getPlayer(), event.getPlayer(), true));
if (event.isCancelled()) {
Logging.fine("Player '" + event.getPlayer().getName()
+ "' was DENIED ACCESS to '" + event.getTo().getWorld().getName()
+ "' because they don't have the FUNDS required to enter.");
return;
}
if (config.getEnforceAccess()) {
event.setCancelled(!pt.playerCanGoFromTo(fromWorld, toWorld, event.getPlayer(), event.getPlayer()));
if (event.isCancelled()) {
Logging.fine("Player '" + event.getPlayer().getName()
+ "' was DENIED ACCESS to '" + event.getTo().getWorld().getName()
+ "' because they don't have: multiverse.access." + event.getTo().getWorld().getName());
}
} else {
Logging.fine("Player '" + event.getPlayer().getName()
+ "' was allowed to go to '" + event.getTo().getWorld().getName()
+ "' because enforceaccess is off.");
}
if (!config.isUsingCustomPortalSearch()) {
event.setSearchRadius(config.getCustomPortalSearchRadius());
}
ResultChain entryResult = worldEntryCheckerProvider.forSender(event.getPlayer()).canEnterWorld(fromWorld, toWorld)
.onFailure(results -> {
event.setCancelled(true);
getCommandManager().getCommandIssuer(event.getPlayer()).sendError(results.getLastResultMessage());
});
Logging.fine("Teleport result: %s", entryResult);
}
private void sendPlayerToDefaultWorld(final Player player) {

View File

@ -0,0 +1,8 @@
package com.onarandombox.MultiverseCore.permissions;
public class CorePermissions {
public static String WORLD_ACCESS = "multiverse.access";
public static String WORLD_EXEMPT = "multiverse.exempt";
public static String GAMEMODE_BYPASS = "mv.bypass.gamemode";
public static String PLAYERLIMIT_BYPASS = "mv.bypass.playerlimit";
}

View File

@ -0,0 +1,39 @@
package com.onarandombox.MultiverseCore.permissions;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.api.MVWorld;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@Service
public class CorePermissionsChecker {
public boolean hasWorldAccessPermission(@NotNull CommandSender sender, @NotNull MVWorld world) {
return hasPermission(sender, concatPermission(CorePermissions.WORLD_ACCESS, world.getName()));
}
public boolean hasWorldExemptPermission(@NotNull CommandSender sender, @NotNull MVWorld world) {
return hasPermission(sender, concatPermission(CorePermissions.WORLD_EXEMPT, world.getName()));
}
public boolean hasPlayerLimitBypassPermission(@NotNull CommandSender sender, @NotNull MVWorld world) {
return hasPermission(sender, concatPermission(CorePermissions.PLAYERLIMIT_BYPASS, world.getName()));
}
public boolean hasGameModeBypassPermission(@NotNull CommandSender sender, @NotNull MVWorld world) {
return hasPermission(sender, concatPermission(CorePermissions.GAMEMODE_BYPASS, world.getName()));
}
private String concatPermission(String permission, String...child) {
return permission + "." + String.join(".", child);
}
private boolean hasPermission(CommandSender sender, String permission) {
if (sender.hasPermission(permission)) {
Logging.finer("Checking to see if sender [%s] has permission [%s]... YES", sender.getName(), permission);
return true;
}
Logging.finer("Checking to see if sender [%s] has permission [%s]... NO", sender.getName(), permission);
return false;
}
}

View File

@ -84,7 +84,20 @@ public enum MVCorei18n implements MessageKeyProvider {
// debug command
DEBUG_INFO_OFF,
DEBUG_INFO_ON;
DEBUG_INFO_ON,
// entry check
ENTRYCHECK_BLACKLISTED,
ENTRYCHECK_NOTENOUGHMONEY,
ENTRYCHECK_CANNOTPAYENTRYFEE,
ENTRYCHECK_EXCEEDPLAYERLIMIT,
ENTRYCHECK_NOWORLDACCESS,
// generic
GENERIC_SUCCESS,
GENERIC_FAILURE
;
private final MessageKey key = MessageKey.of("mv-core." + this.name().replace('_', '.').toLowerCase());

View File

@ -7,7 +7,7 @@ import org.jetbrains.annotations.Nullable;
/**
* Captures string replacements for {@link Message}s.
*/
public final class MessageReplacement {
public final class MessageReplacement {
/**
* Creates a replacement key for the given key string.

View File

@ -0,0 +1,11 @@
package com.onarandombox.MultiverseCore.utils.result;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import com.onarandombox.MultiverseCore.utils.MVCorei18n;
public interface FailureReason extends MessageKeyProvider {
default MessageKey getMessageKey() {
return MVCorei18n.GENERIC_FAILURE.getMessageKey();
}
}

View File

@ -0,0 +1,140 @@
package com.onarandombox.MultiverseCore.utils.result;
import com.onarandombox.MultiverseCore.utils.message.Message;
import com.onarandombox.MultiverseCore.utils.message.MessageReplacement;
import org.jetbrains.annotations.NotNull;
import java.util.NoSuchElementException;
import java.util.function.Consumer;
public sealed interface Result<S extends SuccessReason, F extends FailureReason> permits Result.Success, Result.Failure {
static <F extends FailureReason, S extends SuccessReason> Result<S, F> success(S successReason, MessageReplacement...replacements) {
return new Success<>(successReason, replacements);
}
static <F extends FailureReason, S extends SuccessReason> Result<S, F> failure(F failureReason, MessageReplacement...replacements) {
return new Failure<>(failureReason, replacements);
}
boolean isSuccess();
boolean isFailure();
S getSuccessReason();
F getFailureReason();
@NotNull Message getReasonMessage();
default Result<S, F> onSuccess(Consumer<S> consumer) {
if (this.isSuccess()) {
consumer.accept(this.getSuccessReason());
}
return this;
}
default Result<S, F> onFailure(Consumer<F> consumer) {
if (this.isFailure()) {
consumer.accept(this.getFailureReason());
}
return this;
}
default Result<S, F> onSuccessReason(S successReason, Consumer<S> consumer) {
if (this.isSuccess() && this.getSuccessReason() == successReason) {
consumer.accept(this.getSuccessReason());
}
return this;
}
default Result<S, F> onFailureReason(F failureReason, Consumer<F> consumer) {
if (this.isFailure() && this.getFailureReason() == failureReason) {
consumer.accept(this.getFailureReason());
}
return this;
}
final class Success<F extends FailureReason, S extends SuccessReason> implements Result<S, F> {
private final S successReason;
private final MessageReplacement[] replacements;
public Success(S successReason, MessageReplacement[] replacements) {
this.successReason = successReason;
this.replacements = replacements;
}
@Override
public boolean isSuccess() {
return true;
}
@Override
public boolean isFailure() {
return false;
}
@Override
public S getSuccessReason() {
return successReason;
}
@Override
public F getFailureReason() {
throw new NoSuchElementException("No reason for failure");
}
@Override
public @NotNull Message getReasonMessage() {
return Message.of(successReason, "Success!", replacements);
}
@Override
public String toString() {
return "Success{" +
"reason=" + successReason +
'}';
}
}
final class Failure<S extends SuccessReason, F extends FailureReason> implements Result<S, F> {
private final F failureReason;
private final MessageReplacement[] replacements;
public Failure(F failureReason, MessageReplacement[] replacements) {
this.failureReason = failureReason;
this.replacements = replacements;
}
@Override
public boolean isSuccess() {
return false;
}
@Override
public boolean isFailure() {
return true;
}
@Override
public S getSuccessReason() {
throw new NoSuchElementException("No reason for success");
}
@Override
public F getFailureReason() {
return failureReason;
}
@Override
public @NotNull Message getReasonMessage() {
return Message.of(failureReason, "Success!", replacements);
}
@Override
public String toString() {
return "Failure{" +
"reason=" + failureReason +
'}';
}
}
}

View File

@ -0,0 +1,143 @@
package com.onarandombox.MultiverseCore.utils.result;
import com.google.common.collect.Iterables;
import com.onarandombox.MultiverseCore.utils.message.Message;
import io.vavr.control.Option;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class ResultChain {
public static Builder builder() {
return new Builder(true);
}
public static Builder builder(boolean stopOnFailure) {
return new Builder(stopOnFailure);
}
private final boolean isSuccess;
private final List<Result<?, ?>> results;
ResultChain(boolean isSuccess, List<Result<?, ?>> results) {
this.isSuccess = isSuccess;
this.results = results;
}
public boolean isSuccess() {
return isSuccess;
}
public boolean isFailure() {
return !isSuccess;
}
public ResultChain onSuccess(Runnable successRunnable) {
if (isSuccess) {
successRunnable.run();
}
return this;
}
public ResultChain onFailure(Runnable failureRunnable) {
if (isFailure()) {
failureRunnable.run();
}
return this;
}
public ResultChain onSuccess(Consumer<ResultChain> successRunnable) {
if (isSuccess) {
successRunnable.accept(this);
}
return this;
}
public ResultChain onFailure(Consumer<ResultChain> failureRunnable) {
if (isFailure()) {
failureRunnable.accept(this);
}
return this;
}
public <S extends SuccessReason> ResultChain onSuccessReason(Class<S> successReasonClass, Consumer<S> successConsumer) {
getSuccessReason(successReasonClass).peek(successConsumer);
return this;
}
public <F extends FailureReason> ResultChain onFailureReason(Class<F> failureReasonClass, Consumer<F> failureConsumer) {
getFailureReason(failureReasonClass).peek(failureConsumer);
return this;
}
public <S extends SuccessReason> ResultChain onSuccessReason(S successReason, Runnable successRunnable) {
getSuccessReason(successReason.getClass()).filter(successReason::equals).peek(reason -> successRunnable.run());
return this;
}
public <S extends SuccessReason> Option<S> getSuccessReason(Class<S> successReasonClass) {
if (isFailure()) {
return Option.none();
}
return Option.ofOptional(results.stream()
.map(Result::getSuccessReason)
.filter(successReasonClass::isInstance)
.map(successReasonClass::cast)
.findFirst());
}
public <F extends FailureReason> Option<F> getFailureReason(Class<F> failureReasonClass) {
if (isSuccess()) {
return Option.none();
}
return Option.ofOptional(results.stream()
.map(Result::getFailureReason)
.filter(failureReasonClass::isInstance)
.map(failureReasonClass::cast)
.findFirst());
}
public Message getLastResultMessage() {
return Iterables.getLast(results).getReasonMessage();
}
@Override
public String toString() {
return "ResultGroup{" +
"isSuccess=" + isSuccess +
", results={" + results.stream().map(Objects::toString).collect(Collectors.joining(", ")) + "}" +
'}';
}
public static class Builder {
private final boolean stopOnFailure;
private final List<Result<?, ?>> results;
private boolean isSuccess = true;
public Builder(boolean stopOnFailure) {
this.stopOnFailure = stopOnFailure;
this.results = new ArrayList<>();
}
public Builder then(Supplier<Result<?, ?>> resultSupplier) {
if (!isSuccess && stopOnFailure) {
return this;
}
Result<?, ?> result = resultSupplier.get();
if (result.isFailure()) {
isSuccess = false;
}
results.add(result);
return this;
}
public ResultChain build() {
return new ResultChain(isSuccess, results);
}
}
}

View File

@ -0,0 +1,11 @@
package com.onarandombox.MultiverseCore.utils.result;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import com.onarandombox.MultiverseCore.utils.MVCorei18n;
public interface SuccessReason extends MessageKeyProvider {
default MessageKey getMessageKey() {
return MVCorei18n.GENERIC_SUCCESS.getMessageKey();
}
}

View File

@ -22,7 +22,7 @@ public class EntryFee extends SerializationConfig {
@Nullable
private Material currency;
private final Material DISABLED_MATERIAL = Material.AIR;
public static final Material DISABLED_MATERIAL = Material.AIR;
public EntryFee() {
super();

View File

@ -0,0 +1,30 @@
package com.onarandombox.MultiverseCore.world.entrycheck;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import com.onarandombox.MultiverseCore.utils.MVCorei18n;
import com.onarandombox.MultiverseCore.utils.result.FailureReason;
import com.onarandombox.MultiverseCore.utils.result.SuccessReason;
public class BlacklistResult {
public enum Success implements SuccessReason {
UNKNOWN_FROM_WORLD,
BYPASSED_BLACKLISTED,
NOT_BLACKLISTED
}
public enum Failure implements FailureReason {
BLACKLISTED(MVCorei18n.ENTRYCHECK_BLACKLISTED);
private final MessageKeyProvider message;
Failure(MessageKeyProvider message) {
this.message = message;
}
@Override
public MessageKey getMessageKey() {
return message.getMessageKey();
}
}
}

View File

@ -0,0 +1,33 @@
package com.onarandombox.MultiverseCore.world.entrycheck;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import com.onarandombox.MultiverseCore.utils.MVCorei18n;
import com.onarandombox.MultiverseCore.utils.result.FailureReason;
import com.onarandombox.MultiverseCore.utils.result.SuccessReason;
public class EntryFeeResult {
public enum Success implements SuccessReason {
FREE_ENTRY,
ENOUGH_MONEY,
EXEMPT_FROM_ENTRY_FEE,
CONSOLE_OR_BLOCK_COMMAND_SENDER
}
public enum Failure implements FailureReason {
NOT_ENOUGH_MONEY(MVCorei18n.ENTRYCHECK_NOTENOUGHMONEY),
CANNOT_PAY_ENTRY_FEE(MVCorei18n.ENTRYCHECK_CANNOTPAYENTRYFEE);
private final MessageKeyProvider message;
Failure(MessageKeyProvider message) {
this.message = message;
}
@Override
public MessageKey getMessageKey() {
return message.getMessageKey();
}
}
}

View File

@ -0,0 +1,30 @@
package com.onarandombox.MultiverseCore.world.entrycheck;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import com.onarandombox.MultiverseCore.utils.MVCorei18n;
import com.onarandombox.MultiverseCore.utils.result.FailureReason;
import com.onarandombox.MultiverseCore.utils.result.SuccessReason;
public class PlayerLimitResult {
public enum Success implements SuccessReason {
NO_PLAYERLIMIT,
WITHIN_PLAYERLIMIT,
BYPASS_PLAYERLIMIT
}
public enum Failure implements FailureReason {
EXCEED_PLAYERLIMIT(MVCorei18n.ENTRYCHECK_EXCEEDPLAYERLIMIT);
private final MessageKeyProvider message;
Failure(MessageKeyProvider message) {
this.message = message;
}
@Override
public MessageKey getMessageKey() {
return message.getMessageKey();
}
}
}

View File

@ -0,0 +1,29 @@
package com.onarandombox.MultiverseCore.world.entrycheck;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import com.onarandombox.MultiverseCore.utils.MVCorei18n;
import com.onarandombox.MultiverseCore.utils.result.FailureReason;
import com.onarandombox.MultiverseCore.utils.result.SuccessReason;
public class WorldAccessResult {
public enum Success implements SuccessReason {
NO_ENFORCE_WORLD_ACCESS,
HAS_WORLD_ACCESS
}
public enum Failure implements FailureReason {
NO_WORLD_ACCESS(MVCorei18n.ENTRYCHECK_NOWORLDACCESS);
private final MessageKeyProvider message;
Failure(MessageKeyProvider message) {
this.message = message;
}
@Override
public MessageKey getMessageKey() {
return message.getMessageKey();
}
}
}

View File

@ -0,0 +1,102 @@
package com.onarandombox.MultiverseCore.world.entrycheck;
import com.onarandombox.MultiverseCore.api.MVWorld;
import com.onarandombox.MultiverseCore.config.MVCoreConfig;
import com.onarandombox.MultiverseCore.economy.MVEconomist;
import com.onarandombox.MultiverseCore.permissions.CorePermissionsChecker;
import com.onarandombox.MultiverseCore.utils.result.Result;
import com.onarandombox.MultiverseCore.utils.result.ResultChain;
import com.onarandombox.MultiverseCore.world.configuration.EntryFee;
import org.bukkit.Material;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.onarandombox.MultiverseCore.utils.message.MessageReplacement.replace;
public class WorldEntryChecker {
private final @NotNull MVCoreConfig config;
private final @NotNull MVEconomist economist;
private final @NotNull CorePermissionsChecker permissionsChecker;
private final @NotNull CommandSender sender;
public WorldEntryChecker(
@NotNull MVCoreConfig config,
@NotNull CorePermissionsChecker permissionsChecker,
@NotNull MVEconomist economist,
@NotNull CommandSender sender
) {
this.config = config;
this.permissionsChecker = permissionsChecker;
this.economist = economist;
this.sender = sender;
}
public ResultChain canStayInWorld(@NotNull MVWorld world) {
return canEnterWorld(null, world);
}
public ResultChain canEnterWorld(@Nullable MVWorld fromWorld, @NotNull MVWorld toWorld) {
return ResultChain.builder()
.then(() -> canAccessWorld(toWorld))
.then(() -> isWithinPlayerLimit(toWorld))
.then(() -> isNotBlacklisted(fromWorld, toWorld))
.then(() -> canPayEntryFee(toWorld))
.build();
}
public Result<WorldAccessResult.Success, WorldAccessResult.Failure> canAccessWorld(@NotNull MVWorld world) {
if (!config.getEnforceAccess()) {
return Result.success(WorldAccessResult.Success.NO_ENFORCE_WORLD_ACCESS);
}
return permissionsChecker.hasWorldAccessPermission(this.sender, world)
? Result.success(WorldAccessResult.Success.HAS_WORLD_ACCESS)
: Result.failure(WorldAccessResult.Failure.NO_WORLD_ACCESS);
}
public Result<PlayerLimitResult.Success, PlayerLimitResult.Failure> isWithinPlayerLimit(@NotNull MVWorld world) {
final int playerLimit = world.getPlayerLimit();
if (playerLimit <= -1) {
return Result.success(PlayerLimitResult.Success.NO_PLAYERLIMIT);
}
if (permissionsChecker.hasPlayerLimitBypassPermission(sender, world)) {
return Result.success(PlayerLimitResult.Success.BYPASS_PLAYERLIMIT);
}
return playerLimit > world.getCBWorld().getPlayers().size()
? Result.success(PlayerLimitResult.Success.WITHIN_PLAYERLIMIT)
: Result.failure(PlayerLimitResult.Failure.EXCEED_PLAYERLIMIT);
}
public Result<BlacklistResult.Success, BlacklistResult.Failure> isNotBlacklisted(@Nullable MVWorld fromWorld, @NotNull MVWorld toWorld) {
if (fromWorld == null) {
return Result.success(BlacklistResult.Success.UNKNOWN_FROM_WORLD);
}
return toWorld.getWorldBlacklist().contains(fromWorld.getName())
? Result.failure(BlacklistResult.Failure.BLACKLISTED, replace("{world}").with(fromWorld.getAlias()))
: Result.success(BlacklistResult.Success.NOT_BLACKLISTED);
}
public Result<EntryFeeResult.Success, EntryFeeResult.Failure> canPayEntryFee(MVWorld world) {
double price = world.getPrice();
Material currency = world.getCurrency();
if (price == 0D && (currency == null || currency == EntryFee.DISABLED_MATERIAL)) {
return Result.success(EntryFeeResult.Success.FREE_ENTRY);
}
if (sender instanceof ConsoleCommandSender || sender instanceof BlockCommandSender) {
return Result.success(EntryFeeResult.Success.CONSOLE_OR_BLOCK_COMMAND_SENDER);
}
if (permissionsChecker.hasWorldExemptPermission(sender, world)) {
return Result.success(EntryFeeResult.Success.EXEMPT_FROM_ENTRY_FEE);
}
if (!(sender instanceof Player player)) {
return Result.failure(EntryFeeResult.Failure.CANNOT_PAY_ENTRY_FEE);
}
return economist.isPlayerWealthyEnough(player, price, currency)
? Result.success(EntryFeeResult.Success.ENOUGH_MONEY)
: Result.failure(EntryFeeResult.Failure.NOT_ENOUGH_MONEY, replace("{amount}").with("$##")); //TODO
}
}

View File

@ -0,0 +1,32 @@
package com.onarandombox.MultiverseCore.world.entrycheck;
import com.onarandombox.MultiverseCore.config.MVCoreConfig;
import com.onarandombox.MultiverseCore.economy.MVEconomist;
import com.onarandombox.MultiverseCore.permissions.CorePermissionsChecker;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@Service
public class WorldEntryCheckerProvider {
private final @NotNull MVCoreConfig config;
private final @NotNull MVEconomist economist;
private final @NotNull CorePermissionsChecker permissionsChecker;
@Inject
WorldEntryCheckerProvider(
@NotNull MVCoreConfig config,
@NotNull MVEconomist economist,
@NotNull CorePermissionsChecker permissionsChecker
) {
this.config = config;
this.economist = economist;
this.permissionsChecker = permissionsChecker;
}
public @NotNull WorldEntryChecker forSender(@NotNull CommandSender sender) {
return new WorldEntryChecker(config, permissionsChecker, economist, sender);
}
}

View File

@ -0,0 +1,71 @@
package com.onarandombox.MultiverseCore.worldnew;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.worldnew.config.WorldConfig;
import com.onarandombox.MultiverseCore.worldnew.config.WorldsConfigFile;
import jakarta.inject.Inject;
import org.bukkit.configuration.ConfigurationSection;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import java.util.List;
@Service
public class WorldManager {
private final WorldsConfigFile worldsConfigFile;
@Inject
WorldManager(@NotNull WorldsConfigFile worldsConfigFile) {
this.worldsConfigFile = worldsConfigFile;
this.worldsConfigFile.load();
}
public void loadAllWorlds() {
for (String worldName : worldsConfigFile.getAllWorldsInConfig()) {
Logging.fine("Loading world: " + worldName);
loadWorld(worldName);
}
saveWorldsConfig();
}
public void addWorld(String worldName) {
ConfigurationSection worldConfigSection = worldsConfigFile.getWorldConfigSection(worldName);
WorldConfig worldConfig = new WorldConfig(worldConfigSection);
//todo
saveWorldsConfig();
}
public void loadWorld(String worldName) {
ConfigurationSection worldConfigSection = worldsConfigFile.getWorldConfigSection(worldName);
WorldConfig worldConfig = new WorldConfig(worldConfigSection);
//todo
}
public void unloadWorld() {
//todo
}
public void removeWorld(String worldName) {
//todo
worldsConfigFile.deleteWorldConfigSection(worldName);
saveWorldsConfig();
}
public void deleteWorld(String worldName) {
//todo
worldsConfigFile.deleteWorldConfigSection(worldName);
saveWorldsConfig();
}
public void getMVWorld(String worldName) {
//todo
}
public void getUnloadedWorld(String worldName) {
//todo
}
public void saveWorldsConfig() {
worldsConfigFile.save();
}
}

View File

@ -0,0 +1,57 @@
package com.onarandombox.MultiverseCore.worldnew.config;
import com.dumptruckman.minecraft.util.Logging;
import com.onarandombox.MultiverseCore.configuration.handle.ConfigurationSectionHandle;
import io.vavr.control.Try;
import org.bukkit.configuration.ConfigurationSection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class WorldConfig {
private final WorldConfigNodes configNodes;
private final ConfigurationSectionHandle configHandle;
public WorldConfig(@NotNull final ConfigurationSection configSection) {
this.configNodes = new WorldConfigNodes();
//todo: Config migration and version
this.configHandle = ConfigurationSectionHandle.builder(configSection)
.logger(Logging.getLogger())
.nodes(configNodes.getNodes())
.build();
this.configHandle.load();
}
public Try<Object> getProperty(String name) {
return configHandle.get(name);
}
public Try<Void> setProperty(String name, Object value) {
return configHandle.set(name, value);
}
public void setAlias(String alias) {
configHandle.set(configNodes.ALIAS, alias);
}
public @Nullable String getAlias() {
return configHandle.get(configNodes.ALIAS);
}
public void setHidden(boolean hidden) {
configHandle.set(configNodes.HIDDEN, hidden);
}
public boolean isHidden() {
return configHandle.get(configNodes.HIDDEN);
}
public List<String> getWorldBlacklist() {
return (List<String>) configHandle.get(configNodes.WORLD_BLACKLIST);
}
public void setWorldBlacklist(List<String> worldBlacklist) {
configHandle.set(configNodes.WORLD_BLACKLIST, worldBlacklist);
}
}

View File

@ -0,0 +1,134 @@
package com.onarandombox.MultiverseCore.worldnew.config;
import com.onarandombox.MultiverseCore.configuration.node.ConfigNode;
import com.onarandombox.MultiverseCore.configuration.node.Node;
import com.onarandombox.MultiverseCore.configuration.node.NodeGroup;
import com.onarandombox.MultiverseCore.world.configuration.AllowedPortalType;
import org.bukkit.Difficulty;
import org.bukkit.GameMode;
import org.bukkit.World;
import java.util.ArrayList;
import java.util.List;
public class WorldConfigNodes {
private final NodeGroup nodes = new NodeGroup();
WorldConfigNodes() {
}
public NodeGroup getNodes() {
return nodes;
}
private <N extends Node> N node(N node) {
nodes.add(node);
return node;
}
public final ConfigNode<Boolean> ADJUST_SPAWN = node(ConfigNode.builder("adjust-spawn", Boolean.class)
.defaultValue(false)
.name("adjust-spawn")
.build());
public final ConfigNode<String> ALIAS = node(ConfigNode.builder("alias", String.class)
.defaultValue("")
.name("alias")
.build());
public final ConfigNode<Boolean> ALLOW_FLIGHT = node(ConfigNode.builder("allow-flight", Boolean.class)
.defaultValue(false)
.name("allow-flight")
.build());
public final ConfigNode<Boolean> ALLOW_WEATHER = node(ConfigNode.builder("allow-weather", Boolean.class)
.defaultValue(true)
.name("allow-weather")
.build());
public final ConfigNode<Boolean> AUTO_HEAL = node(ConfigNode.builder("auto-heal", Boolean.class)
.defaultValue(true)
.name("auto-heal")
.build());
public final ConfigNode<Boolean> AUTO_LOAD = node(ConfigNode.builder("auto-load", Boolean.class)
.defaultValue(true)
.name("auto-load")
.build());
public final ConfigNode<Difficulty> DIFFICULTY = node(ConfigNode.builder("difficulty", Difficulty.class)
.defaultValue(Difficulty.NORMAL)
.name("difficulty")
.build());
public final ConfigNode<World.Environment> ENVIRONMENT = node(ConfigNode.builder("environment", World.Environment.class)
.defaultValue(World.Environment.NORMAL)
.name("environment")
.build());
public final ConfigNode<GameMode> GAMEMODE = node(ConfigNode.builder("gamemode", GameMode.class)
.defaultValue(GameMode.SURVIVAL)
.name("gamemode")
.build());
public final ConfigNode<String> GENERATOR = node(ConfigNode.builder("generator", String.class)
.defaultValue("")
.name("generator")
.build());
public final ConfigNode<Boolean> HIDDEN = node(ConfigNode.builder("hidden", Boolean.class)
.defaultValue(false)
.name("hidden")
.build());
public final ConfigNode<Boolean> HUNGER = node(ConfigNode.builder("hunger", Boolean.class)
.defaultValue(true)
.name("hunger")
.build());
public final ConfigNode<Boolean> KEEP_SPAWN_IN_MEMORY = node(ConfigNode.builder("keep-spawn-in-memory", Boolean.class)
.defaultValue(true)
.name("keep-spawn-in-memory")
.build());
public final ConfigNode<Integer> PLAYER_LIMIT = node(ConfigNode.builder("player-limit", Integer.class)
.defaultValue(-1)
.name("player-limit")
.build());
public final ConfigNode<AllowedPortalType> PORTAL_FORM = node(ConfigNode.builder("portal-form", AllowedPortalType.class)
.defaultValue(AllowedPortalType.ALL)
.name("portal-form")
.build());
public final ConfigNode<Boolean> PVP = node(ConfigNode.builder("pvp", Boolean.class)
.defaultValue(true)
.name("pvp")
.build());
public final ConfigNode<String> RESPAWN_WORLD = node(ConfigNode.builder("respawn-world", String.class)
.defaultValue("")
.name("respawn-world")
.build());
public final ConfigNode<Double> SCALE = node(ConfigNode.builder("scale", Double.class)
.defaultValue(1.0)
.name("scale")
.build());
public final ConfigNode<String> SEED = node(ConfigNode.builder("seed", String.class)
.defaultValue("")
.name("seed")
.build());
public final ConfigNode<List> WORLD_BLACKLIST = node(ConfigNode.builder("world-blacklist", List.class)
.defaultValue(new ArrayList<>())
.name("world-blacklist")
.build());
//todo: color and style
//todo: spawning
//todo: entryfee
//todo: spawnLocation
//todo: worldBlacklist
}

View File

@ -0,0 +1,55 @@
package com.onarandombox.MultiverseCore.worldnew.config;
import com.onarandombox.MultiverseCore.MultiverseCore;
import jakarta.inject.Inject;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
@Service
public class WorldsConfigFile {
private static final String CONFIG_FILENAME = "worlds2.yml";
private final File worldConfigFile;
private YamlConfiguration worldConfig;
@Inject
public WorldsConfigFile(@NotNull MultiverseCore core) {
worldConfigFile = core.getDataFolder().toPath().resolve(CONFIG_FILENAME).toFile();
}
public void load() {
//todo: Migration from old worlds.yml
worldConfig = YamlConfiguration.loadConfiguration(worldConfigFile);
}
public boolean isLoaded() {
return worldConfig != null;
}
public void save() {
try {
worldConfig.save(worldConfigFile);
} catch (IOException e) {
e.printStackTrace();
}
}
public Collection<String> getAllWorldsInConfig() {
return worldConfig.getKeys(false);
}
public ConfigurationSection getWorldConfigSection(String worldName) {
return worldConfig.isConfigurationSection(worldName)
? worldConfig.getConfigurationSection(worldName) : worldConfig.createSection(worldName);
}
public void deleteWorldConfigSection(String worldName) {
worldConfig.set(worldName, null);
}
}

View File

@ -118,3 +118,14 @@ mv-core.unload.success=&aUnloaded world '{world}'!
# /mv usage
mv-core.usage.description=Show Multiverse-Core command usage.
# entry check
mv-core.entrycheck.blacklisted='{world}' is blacklisted.
mv-core.entrycheck.notenoughmoney=you do not have enough money to pay entry fee. You are required to pay {amount}.
mv-core.entrycheck.cannotpayentryfee=you do not have the ability to pay entry fee.
mv-core.entrycheck.exceedplayerlimit=the world has reached its player limit.
mv-core.entrycheck.noworldaccess=you do not have permissions to access the world.
# generic
mv-core.generic.success=Success!
mv-core.generic.failure=Failed!

View File

@ -39,7 +39,7 @@ class ConfigTest : TestWithMockBukkit() {
fun `Old config is migrated`() {
val oldConfig = getResourceAsText("/old_config.yml")
assertNotNull(oldConfig)
File(Path.of(multiverseCore.dataFolder.absolutePath, "config.yml").absolutePathString()).writeText(oldConfig)
multiverseCore.dataFolder.toPath().resolve("config.yml").toFile().writeText(oldConfig)
assertTrue(config.load())
assertTrue(config.save())

View File

@ -127,7 +127,7 @@ class InjectionTest : TestWithMockBukkit() {
fun `Commands are available as services`() {
val commands = multiverseCore.getAllServices(MultiverseCommand::class.java)
// TODO come up with a better way to test this like via actually testing the effect of calling each command
assertEquals(17, commands.size)
assertEquals(18, commands.size)
}
@Test

View File

@ -0,0 +1,69 @@
package org.mvplugins.multiverse.core.world
import com.onarandombox.MultiverseCore.worldnew.config.WorldConfig
import com.onarandombox.MultiverseCore.worldnew.config.WorldsConfigFile
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.mvplugins.multiverse.core.TestWithMockBukkit
import java.io.File
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertNotNull
class WorldConfigFileTest : TestWithMockBukkit() {
private lateinit var worldConfigFile : WorldsConfigFile
@BeforeTest
fun setUp() {
val defaultConfig = getResourceAsText("/default_worlds.yml")
assertNotNull(defaultConfig)
File(Path.of(multiverseCore.dataFolder.absolutePath, "worlds2.yml").absolutePathString()).writeText(defaultConfig)
worldConfigFile = WorldsConfigFile(multiverseCore)
worldConfigFile.load()
}
@Test
fun `World config is loaded`() {
assertTrue(worldConfigFile.isLoaded)
}
@Test
fun `Old world config is migrated`() {
// TODO: When logic is implemented, check that the old config is migrated
}
@Test
fun `Add a new world to config`() {
val worldConfig = WorldConfig(worldConfigFile.getWorldConfigSection("newworld"))
worldConfigFile.save()
compareConfigFile("worlds2.yml", "/newworld_worlds.yml")
}
@Test
fun `Updating existing world properties`() {
val worldConfig = WorldConfig(worldConfigFile.getWorldConfigSection("world"))
worldConfig.setProperty("adjust-spawn", true)
worldConfig.setProperty("alias", "newalias")
worldConfigFile.save()
compareConfigFile("worlds2.yml", "/properties_worlds.yml")
}
@Test
fun `Delete world section from config`() {
worldConfigFile.deleteWorldConfigSection("world")
worldConfigFile.save()
compareConfigFile("worlds2.yml", "/delete_worlds.yml")
}
private fun compareConfigFile(configPath: String, comparePath: String) {
// TODO: Map keys may not guaranteed order. Potentially look at Hamkrest and assertThat.
val config = multiverseCore.dataFolder.toPath().resolve(configPath).toFile().readText()
val configCompare = getResourceAsText(comparePath)
assertNotNull(configCompare)
assertEquals(configCompare, config)
}
}

View File

@ -0,0 +1,70 @@
package org.mvplugins.multiverse.core.world
import com.onarandombox.MultiverseCore.worldnew.config.WorldConfig
import com.onarandombox.MultiverseCore.worldnew.config.WorldsConfigFile
import org.mvplugins.multiverse.core.TestWithMockBukkit
import java.io.File
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class WorldConfigTest : TestWithMockBukkit() {
private lateinit var worldConfigFile : WorldsConfigFile
private lateinit var worldConfig : WorldConfig
@BeforeTest
fun setUp() {
val defaultConfig = getResourceAsText("/default_worlds.yml")
assertNotNull(defaultConfig)
File(Path.of(multiverseCore.dataFolder.absolutePath, "worlds2.yml").absolutePathString()).writeText(defaultConfig)
worldConfigFile = WorldsConfigFile(multiverseCore)
worldConfigFile.load()
worldConfig = WorldConfig(worldConfigFile.getWorldConfigSection("world"))
}
@Test
fun `Getting existing world property with getProperty returns expected value`() {
assertEquals("my world", worldConfig.getProperty("alias").get())
assertEquals(false, worldConfig.getProperty("hidden").get())
}
@Test
fun `Getting non-existing world property with getProperty returns null`() {
assertTrue(worldConfig.getProperty("invalid-property").isFailure)
assertTrue(worldConfig.getProperty("version").isFailure)
}
@Test
fun `Getting existing world property by getter returns expected value`() {
assertEquals("my world", worldConfig.alias)
assertEquals(false, worldConfig.isHidden)
}
@Test
fun `Updating an existing world property with setProperty reflects the changes in getProperty`() {
assertTrue(worldConfig.setProperty("adjust-spawn", true).isSuccess)
assertEquals(true, worldConfig.getProperty("adjust-spawn").get())
assertTrue(worldConfig.setProperty("alias", "abc").isSuccess)
assertEquals("abc", worldConfig.getProperty("alias").get())
assertTrue(worldConfig.setProperty("scale", 2.0).isSuccess)
assertEquals(2.0, worldConfig.getProperty("scale").get())
val blacklists = listOf("a", "b", "c")
assertTrue(worldConfig.setProperty("world-blacklist", blacklists).isSuccess)
assertEquals(blacklists, worldConfig.getProperty("world-blacklist").get())
}
@Test
fun `Updating a non-existing property with setProperty returns false`() {
assertTrue(worldConfig.setProperty("invalid-property", false).isFailure)
assertTrue(worldConfig.setProperty("version", 1.1).isFailure)
}
}

View File

@ -0,0 +1,29 @@
package org.mvplugins.multiverse.core.world
import com.onarandombox.MultiverseCore.worldnew.WorldManager
import org.mvplugins.multiverse.core.TestWithMockBukkit
import kotlin.test.BeforeTest
import kotlin.test.Test
class WorldManagerTest : TestWithMockBukkit() {
private lateinit var worldManager: WorldManager
@BeforeTest
fun setUp() {
worldManager = multiverseCore.getService(WorldManager::class.java).takeIf { it != null } ?: run {
throw IllegalStateException("WorldManager is not available as a service") }
}
@Test
fun `Add world`() {
worldManager.addWorld("world")
// TODO: When logic is implemented, check that the world is added
}
@Test
fun `Delete world`() {
worldManager.deleteWorld("world")
// TODO: When logic is implemented, check that the world is removed
}
}

View File

@ -0,0 +1,42 @@
world:
adjust-spawn: false
alias: my world
allow-flight: false
allow-weather: true
auto-heal: true
auto-load: true
difficulty: NORMAL
environment: NORMAL
gamemode: SURVIVAL
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: ALL
pvp: true
respawn-world: ''
scale: 1.0
seed: ''
world-blacklist: []
world_nether:
adjust-spawn: false
alias: ''
allow-flight: false
allow-weather: true
auto-heal: true
auto-load: true
difficulty: NORMAL
environment: NETHER
gamemode: SURVIVAL
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: ALL
pvp: true
respawn-world: ''
scale: 1.0
seed: ''
world-blacklist: []

View File

@ -0,0 +1,21 @@
world_nether:
adjust-spawn: false
alias: ''
allow-flight: false
allow-weather: true
auto-heal: true
auto-load: true
difficulty: NORMAL
environment: NETHER
gamemode: SURVIVAL
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: ALL
pvp: true
respawn-world: ''
scale: 1.0
seed: ''
world-blacklist: []

View File

@ -0,0 +1,63 @@
world:
adjust-spawn: false
alias: my world
allow-flight: false
allow-weather: true
auto-heal: true
auto-load: true
difficulty: NORMAL
environment: NORMAL
gamemode: SURVIVAL
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: ALL
pvp: true
respawn-world: ''
scale: 1.0
seed: ''
world-blacklist: []
world_nether:
adjust-spawn: false
alias: ''
allow-flight: false
allow-weather: true
auto-heal: true
auto-load: true
difficulty: NORMAL
environment: NETHER
gamemode: SURVIVAL
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: ALL
pvp: true
respawn-world: ''
scale: 1.0
seed: ''
world-blacklist: []
newworld:
adjust-spawn: false
alias: ''
allow-flight: false
allow-weather: true
auto-heal: true
auto-load: true
difficulty: NORMAL
environment: NORMAL
gamemode: SURVIVAL
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: ALL
pvp: true
respawn-world: ''
scale: 1.0
seed: ''
world-blacklist: []

View File

@ -0,0 +1,42 @@
world:
adjust-spawn: true
alias: newalias
allow-flight: false
allow-weather: true
auto-heal: true
auto-load: true
difficulty: NORMAL
environment: NORMAL
gamemode: SURVIVAL
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: ALL
pvp: true
respawn-world: ''
scale: 1.0
seed: ''
world-blacklist: []
world_nether:
adjust-spawn: false
alias: ''
allow-flight: false
allow-weather: true
auto-heal: true
auto-load: true
difficulty: NORMAL
environment: NETHER
gamemode: SURVIVAL
generator: ''
hidden: false
hunger: true
keep-spawn-in-memory: true
player-limit: -1
portal-form: ALL
pvp: true
respawn-world: ''
scale: 1.0
seed: ''
world-blacklist: []